Compare commits

58 Commits

Author SHA1 Message Date
f2ba3494b1 remove no slip
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m35s
2026-03-03 10:32:38 +00:00
d56bd5783d eliminate the wretched hiding console.log, filthy log......
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m20s
2026-02-24 22:48:13 +00:00
f60636942f eliminate the wretched hiding console.log, filthy log......
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m31s
2026-02-24 22:40:55 +00:00
b087172bb1 slowed down the whole animation industry
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m30s
2026-02-24 22:31:54 +00:00
0c93c6bc27 make colors match
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m41s
2026-02-23 16:16:09 +00:00
48ae2f59ea adding important context
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m37s
2026-02-23 16:06:05 +00:00
c9faa90abd adding important context
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m43s
2026-02-23 16:03:52 +00:00
ef78974744 stamps width fix, also screw mobile even more
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m39s
2026-02-23 15:56:52 +00:00
49499052b0 add link to feed
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m49s
2026-02-23 15:51:56 +00:00
dbb4914745 fix mobile, nobody likes mobile......... hate mobile
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m28s
2026-02-23 15:49:04 +00:00
34fa96ddab adding git feed
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
2026-02-23 15:45:46 +00:00
8a9f3c373d Update links
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m27s
2026-02-23 14:23:43 +00:00
dc05ade798 update readme
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m29s
2026-02-23 14:15:32 +00:00
1e47919a40 update readme
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-23 13:23:50 +00:00
8e9734fca7 update cv
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m27s
2026-02-23 13:14:41 +00:00
da9a083f2d eddy fix
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m34s
2026-02-21 20:06:59 +00:00
3c40eb9f08 I should seriously look at what I'm commiting before I commit it.... >_< dw I won't do this in professional context
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m26s
2026-02-20 16:28:16 +00:00
e016e3af46 fix infinite loop :( shouldn't have existed, silly programmer
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 50s
2026-02-20 16:26:22 +00:00
0c91f512b4 adding more detail to cv
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m23s
2026-02-20 16:24:15 +00:00
f63b61431b maybe bedroom is not professional
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
2026-02-20 16:22:39 +00:00
f3ea83c477 changed cv
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m28s
2026-02-20 16:13:01 +00:00
4b5ed4787a center text and fullwidth button
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m30s
2026-02-19 21:41:49 +00:00
747a403bcb idk what this changes
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-18 21:45:17 +00:00
fe16ccab97 add ignore to webpack future content
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 10s
2026-02-18 21:43:51 +00:00
7bcb485fc6 move radio and add to homepage
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
2026-02-18 21:42:37 +00:00
a3d73b12f4 Adding gitea link
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m28s
2026-02-18 21:29:30 +00:00
47a8e6c35e Adding gitea link
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m22s
2026-02-18 21:27:43 +00:00
f885ff9175 Adding gitea link
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m25s
2026-02-18 21:22:41 +00:00
d574fa7692 change intro
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m20s
2026-02-18 21:13:32 +00:00
ac171f7846 yes
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
Merge branch 'main' of ssh://adam-french.co.uk:2222/adamf/web_server
2026-02-18 21:09:20 +00:00
b5b86a2a37 revert to old website intro 2026-02-18 21:08:33 +00:00
cfdb5b4d50 Update .gitea/workflows/deploy.yaml
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-18 16:38:06 +00:00
37580cdc42 Stop da haxxers
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-18 16:26:52 +00:00
711236b776 fix actions
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m35s
2026-02-18 16:21:52 +00:00
75454c2ed8 fix actions
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 5s
2026-02-18 16:19:58 +00:00
78c824c4c8 remove mountpoint
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2m56s
2026-02-17 18:37:46 +00:00
ba3b933068 remove mountpoint
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 5s
2026-02-17 18:36:23 +00:00
14c430bbad adding gitea actions
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s
2026-02-17 18:34:55 +00:00
26c7422e34 adding gitea-runner service 2026-02-17 18:10:19 +00:00
470b1c79d8 fix ssh domain for gitea 2026-02-16 16:57:20 +00:00
d849b606ec fix certbot env vars 2026-02-16 15:07:44 +00:00
46a9da4c90 gitea runner 2026-02-16 14:52:33 +00:00
398a610cb2 fix root of gitea 2026-02-16 13:58:09 +00:00
b506bae515 fix gitea 2026-02-16 13:57:28 +00:00
11ad0b5a83 reverse proxy to gitea 2026-02-16 13:21:11 +00:00
d7393e1419 Revert "Revert "idk what I changed""
This reverts commit 0d32333c0c.
2026-02-16 13:03:13 +00:00
0d32333c0c Revert "idk what I changed"
This reverts commit 7dc3f49273.
2026-02-16 13:02:31 +00:00
050a38a76f Merge branch 'main' of /home/adamf/repos/web_server 2026-02-16 12:56:23 +00:00
bc43e9ed02 add gitea 2026-02-16 12:52:08 +00:00
75b8b02825 ignore gitea volume 2026-02-16 12:51:48 +00:00
5c69a1d0a7 adding gitea 2026-02-16 11:46:49 +00:00
aa915e1071 Merge branch 'main' of 192.168.178.24:~/repos/web_server
ye
2026-02-14 22:03:47 +00:00
7dc3f49273 idk what I changed 2026-02-14 22:03:32 +00:00
21d3997a16 remove ts 2026-02-10 17:04:52 +00:00
c56ba217dd remove tsconfig 2026-02-10 17:01:26 +00:00
91804f1fe7 adding vueuse 2026-02-10 17:00:59 +00:00
7e74ce5a2a adding typescript 2026-02-10 16:57:00 +00:00
e92ac49140 new components 2026-02-10 16:46:49 +00:00
33 changed files with 2136 additions and 844 deletions

View File

@@ -0,0 +1,19 @@
name: Deploy with Docker Compose
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Pull changes
working-directory: /home/adamf/deploy/web_server
run: git pull gitea main
- name: Run docker compose up
working-directory: /home/adamf/deploy/web_server
env:
DOCKER_API_VERSION: "1.41"
run: docker compose up -d --build --remove-orphans

6
.gitignore vendored
View File

@@ -3,6 +3,12 @@ certbot/www
backend/token/ backend/token/
.env .env
gitea/data/*
gitea-runner/data/*
# Will add in future (webpack)
nginx/vue/crates/
# Logs # Logs
logs logs
*.log *.log

View File

@@ -14,7 +14,6 @@ import (
) )
func main() { func main() {
logsDir := "/backend/logs" logsDir := "/backend/logs"
logFile, err := os.OpenFile(logsDir+"/go.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) logFile, err := os.OpenFile(logsDir+"/go.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil { if err != nil {

View File

@@ -12,10 +12,11 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: nginx container_name: nginx
env_file: ./.env env_file: ./.env
restart: unless-stopped restart: always
depends_on: depends_on:
- backend - backend
- icecast2 - icecast2
- gitea
networks: networks:
- app-network - app-network
ports: ports:
@@ -33,6 +34,8 @@ services:
- ./certbot/conf:/etc/letsencrypt - ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot - ./certbot/www:/var/www/certbot
entrypoint: ["/entrypoint.sh"] entrypoint: ["/entrypoint.sh"]
env_file:
- .env
networks: networks:
- app-network - app-network
@@ -41,7 +44,7 @@ services:
context: ./backend context: ./backend
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: "${BACKEND_HOST}" container_name: "${BACKEND_HOST}"
restart: unless-stopped restart: always
depends_on: depends_on:
- db - db
networks: networks:
@@ -56,13 +59,15 @@ services:
db: db:
image: postgres:16 image: postgres:16
container_name: "${POSTGRES_HOST}" container_name: "${POSTGRES_HOST}"
restart: unless-stopped restart: always
env_file: env_file:
- ./.env - ./.env
networks: networks:
- app-network - app-network
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
ports:
- 5432:5432
icecast2: icecast2:
build: build:
@@ -76,3 +81,43 @@ services:
- ./.env - ./.env
ports: ports:
- "${ICECAST_PORT}:${ICECAST_PORT}" - "${ICECAST_PORT}:${ICECAST_PORT}"
gitea-runner:
image: gitea/act_runner:latest
container_name: "${GITEA_RUNNER_HOST}"
environment:
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME}
CONFIG_FILE: /config.yaml
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
GITEA_INSTANCE_URL: "http://${GITEA_HOST}:3000"
GITEA_RUNNER_LABELS: "self-hosted:host"
volumes:
- ./gitea-runner/config.yaml:/config.yaml
- ./gitea-runner/data:/data
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
networks:
- app-network
gitea:
image: docker.gitea.com/gitea:1.25.4-rootless
container_name: "${GITEA_HOST}"
networks:
- app-network
environment:
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=${POSTGRES_HOST}
- GITEA__database__NAME=${POSTGRES_GITEA_DB}
- GITEA__database__USER=${POSTGRES_USER}
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
restart: always
volumes:
- ./gitea/data:/var/lib/gitea
- ./gitea/config:/etc/gitea
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2222:2222"
depends_on:
- db

110
gitea-runner/config.yaml Normal file
View File

@@ -0,0 +1,110 @@
# Example configuration file, it's safe to copy this as the default config file without any modification.
# You don't have to copy this file to your instance,
# just run `./act_runner generate-config > config.yaml` to generate a config file.
log:
# The level of logging, can be trace, debug, info, warn, error, fatal
level: info
runner:
# Where to store the registration result.
file: .runner
# Execute how many tasks concurrently at the same time.
capacity: 1
# Extra environment variables to run jobs.
envs:
A_TEST_ENV_NAME_1: a_test_env_value_1
A_TEST_ENV_NAME_2: a_test_env_value_2
# Extra environment variables to run jobs from a file.
# It will be ignored if it's empty or the file doesn't exist.
env_file: .env
# The timeout for a job to be finished.
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
timeout: 3h
# The timeout for the runner to wait for running jobs to finish when shutting down.
# Any running jobs that haven't finished after this timeout will be cancelled.
shutdown_timeout: 0s
# Whether skip verifying the TLS certificate of the Gitea instance.
insecure: false
# The timeout for fetching the job from the Gitea instance.
fetch_timeout: 5s
# The interval for fetching the job from the Gitea instance.
fetch_interval: 2s
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
# and github_mirror is not empty. In this case,
# it replaces https://github.com with the value here, which is useful for some special network environments.
github_mirror: ''
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
# Find more images provided by Gitea at https://gitea.com/docker.gitea.com/runner-images .
# If it's empty when registering, it will ask for inputting labels.
# If it's empty when execute `daemon`, will use labels in `.runner` file.
labels:
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
- "ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04"
cache:
# Enable cache server to use actions/cache.
enabled: true
# The directory to store the cache data.
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
dir: ""
# The host of the cache server.
# It's not for the address to listen, but the address to connect from job containers.
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
host: ""
# The port of the cache server.
# 0 means to use a random available port.
port: 0
# The external cache server URL. Valid only when enable is true.
# If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
# The URL should generally end with "/".
external_server: ""
container:
# Specifies the network to which the container will connect.
# Could be host, bridge or the name of a custom network.
# If it's empty, act_runner will create a network automatically.
network: ""
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
privileged: false
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
options:
# The parent directory of a job's working directory.
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
# If the path starts with '/', the '/' will be trimmed.
# For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
# If it's empty, /workspace will be used.
workdir_parent:
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
# valid_volumes:
# - data
# - /src/*.json
# If you want to allow any volume, please use the following configuration:
# valid_volumes:
# - '**'
valid_volumes: []
# overrides the docker client host with the specified one.
# If it's empty, act_runner will find an available docker host automatically.
# If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
docker_host: ""
# Pull docker image(s) even if already present
force_pull: true
# Rebuild docker image(s) even if already present
force_rebuild: false
# Always require a reachable docker daemon, even if not required by act_runner
require_docker: false
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
docker_timeout: 0s
host:
# The parent directory of a job's working directory.
# If it's empty, $HOME/.cache/act/ will be used.
workdir_parent:

98
gitea/config/app.ini Normal file
View File

@@ -0,0 +1,98 @@
APP_NAME = Gitea: Git with a cup of tea
RUN_USER = git
RUN_MODE = prod
WORK_PATH = /var/lib/gitea
[repository]
ROOT = /var/lib/gitea/git/repositories
[repository.local]
LOCAL_COPY_PATH = /tmp/gitea/local-repo
[repository.upload]
TEMP_PATH = /tmp/gitea/uploads
[server]
APP_DATA_PATH = /var/lib/gitea
SSH_DOMAIN = adam-french.co.uk
HTTP_PORT = 3000
ROOT_URL = https://adam-french.co.uk/gitea/
DISABLE_SSH = false
; In rootless gitea container only internal ssh server is supported
START_SSH_SERVER = true
SSH_PORT = 2222
SSH_LISTEN_PORT = 2222
BUILTIN_SSH_SERVER_USER = git
LFS_START_SERVER = true
DOMAIN = stppi.local
LFS_JWT_SECRET = XHIJprS_aMv0tizioZpUD38GGqTtNMFXMz1R6LuPvjU
OFFLINE_MODE = true
[database]
PATH = /var/lib/gitea/data/gitea.db
DB_TYPE = postgres
HOST = db
NAME = gitea
USER = postgres
PASSWD = password
SCHEMA =
SSL_MODE = disable
LOG_SQL = false
[session]
PROVIDER_CONFIG = /var/lib/gitea/data/sessions
PROVIDER = file
[picture]
AVATAR_UPLOAD_PATH = /var/lib/gitea/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = /var/lib/gitea/data/repo-avatars
[attachment]
PATH = /var/lib/gitea/data/attachments
[log]
ROOT_PATH = /var/lib/gitea/data/log
MODE = console
LEVEL = info
[security]
INSTALL_LOCK = true
SECRET_KEY =
REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = *
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NzEyNDMyMTd9.yHsgFcEwDNWmZebftpe8tpWRFa5aR5tkpQuVYybeVaY
PASSWORD_HASH_ALGO = pbkdf2
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
[lfs]
PATH = /var/lib/gitea/git/lfs
[mailer]
ENABLED = false
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
[cron.update_checker]
ENABLED = false
[repository.pull-request]
DEFAULT_MERGE_STYLE = merge
[repository.signing]
DEFAULT_TRUST_MODEL = committer
[oauth2]
JWT_SECRET = pYiwW8xxGi23gysl2pa-02Cf567Z5ERvR6DDFGIn2iQ

View File

@@ -4,7 +4,7 @@ set -e
# Check if certificate exists # Check if certificate exists
if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then
echo "Certificates found. Using production nginx config." echo "Certificates found. Using production nginx config."
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT}' \ envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \
< /etc/nginx/nginx.conf.template \ < /etc/nginx/nginx.conf.template \
> /etc/nginx/nginx.conf > /etc/nginx/nginx.conf
else else

View File

@@ -98,6 +98,18 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location /gitea {
return 301 /gitea/;
}
location /gitea/ {
proxy_pass http://$GITEA_HOST:$GITEA_PORT/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
} }
} }

View File

@@ -10,12 +10,14 @@
"dependencies": { "dependencies": {
"@mdit/plugin-katex": "^0.24.1", "@mdit/plugin-katex": "^0.24.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"katex": "^0.16.27", "katex": "^0.16.27",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-wikilinks": "^1.4.0", "markdown-it-wikilinks": "^1.4.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },
@@ -1631,6 +1633,12 @@
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
@@ -1916,6 +1924,44 @@
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vueuse/core": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
"integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.2.1",
"@vueuse/shared": "14.2.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
"integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
"integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/ansis": { "node_modules/ansis": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
@@ -1939,13 +1985,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.2", "version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -3382,6 +3428,19 @@
"utf8-byte-length": "^1.0.1" "utf8-byte-length": "^1.0.1"
} }
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uc.micro": { "node_modules/uc.micro": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",

View File

@@ -14,12 +14,14 @@
"dependencies": { "dependencies": {
"@mdit/plugin-katex": "^0.24.1", "@mdit/plugin-katex": "^0.24.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"katex": "^0.16.27", "katex": "^0.16.27",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-wikilinks": "^1.4.0", "markdown-it-wikilinks": "^1.4.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.6.3" "vue-router": "^4.6.3"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

View File

@@ -279,8 +279,8 @@ td {
background-position: 0 0; background-position: 0 0;
mask-image: linear-gradient( mask-image: linear-gradient(
30deg, -180deg,
rgba(1, 1, 1, 1) 0%, rgba(1, 1, 1, 1) 0%,
rgba(1, 1, 1, 0.9) 100% rgba(1, 1, 1, 0.92) 100%
); );
} }

View File

@@ -49,14 +49,10 @@ const faces_string = faces.join(" ");
<RouterLink class="bdr-2 bg-bg_primary" to="/" v-if="!inHome"> <RouterLink class="bdr-2 bg-bg_primary" to="/" v-if="!inHome">
<a>HOME</a> <a>HOME</a>
</RouterLink> </RouterLink>
<RouterLink <RouterLink class="bdr-2 bg-bg_primary" v-if="parentPath" :to="parentPath">
class="bdr-2 bg-bg_primary"
v-if="parentPath"
:to="parentPath"
>
<a>UP</a> <a>UP</a>
</RouterLink> </RouterLink>
<Headline class="border flex-1"> <Headline class="border flex-1 max-w-full">
<code class="whitespace-pre">{{ faces_string }}</code> <code class="whitespace-pre">{{ faces_string }}</code>
</Headline> </Headline>
</nav> </nav>

View File

@@ -1,9 +1,8 @@
<script setup> <script setup>
import { ref, onMounted, useTemplateRef, onUnmounted } from "vue"; import { onMounted, useTemplateRef, onUnmounted } from "vue";
const container = useTemplateRef("container"); const container = useTemplateRef("container");
const item1 = useTemplateRef("item1"); const item1 = useTemplateRef("item1");
const item2 = useTemplateRef("item2");
let offset = 0; let offset = 0;
@@ -14,7 +13,6 @@ const speed = 0.5; // pixels per frame
function animate() { function animate() {
const ctnr = container.value; const ctnr = container.value;
const it1 = item1.value; const it1 = item1.value;
const it2 = item2.value;
const width = Math.max(ctnr.offsetWidth, it1.scrollWidth); const width = Math.max(ctnr.offsetWidth, it1.scrollWidth);
@@ -24,8 +22,7 @@ function animate() {
offset += width; offset += width;
} }
it1.style.transform = `translateX(${offset}px)`; ctnr.style.transform = `translateX(${offset}px)`;
it2.style.transform = `translateX(${width + offset}px)`;
rafId = requestAnimationFrame(animate); rafId = requestAnimationFrame(animate);
} }
@@ -40,40 +37,32 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<div class="marquee"> <div class="root">
<div class="container" ref="container"> <div class="container" ref="container">
<div class="item" ref="item1"><slot /></div> <div ref="item1">
<div class="item item2" ref="item2"><slot /></div> <slot />
</div>
<div>
<slot />
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.marquee { .root {
overflow: hidden; overflow: hidden;
width: 100%;
} }
.container { .container {
width: 100%;
height: fit-content;
position: relative;
will-change: transform;
}
.item {
height: fit-content;
top: 0px;
padding-right: 3em;
width: fit-content; width: fit-content;
white-space: nowrap; height: fit-content;
} display: grid;
grid-auto-flow: column;
.item1 { grid-auto-columns: max-content;
left: 0px; /* Each column fits its content */
} overflow-x: visible;
will-change: transform;
.item2 { gap: 10em;
position: absolute;
} }
</style> </style>

View File

@@ -9,9 +9,10 @@ import { useTemplateRef, onMounted, onBeforeUnmount } from "vue";
const container = useTemplateRef("container"); const container = useTemplateRef("container");
const SPEED = 1; // px per frame const SPEED = 0.0005; // % per frame
const PAUSE = 2000; // ms at top/bottom const PAUSE = 2000; // ms at top/bottom
let pos = 0;
let direction = 1; // 1 = down, -1 = up let direction = 1; // 1 = down, -1 = up
let timeoutId; let timeoutId;
let timeoutId2; let timeoutId2;
@@ -27,17 +28,26 @@ function handleHover() {
function tick() { function tick() {
const el = container.value; const el = container.value;
el.scrollTop += SPEED * direction;
const reachedBottom = el.scrollTop + el.clientHeight >= el.scrollHeight; const reachedBottom = pos <= 0;
const reachedTop = el.scrollTop <= 0; const reachedTop = pos >= 1;
if (reachedBottom || reachedTop) { if (reachedBottom) {
direction *= -1; pos = 0.001;
direction = 1;
handleHover();
return;
} else if (reachedTop) {
pos = 0.999;
direction = -1;
handleHover(); handleHover();
return; return;
} }
pos += direction * SPEED;
el.scrollTop = pos * el.scrollHeight;
timeoutId = requestAnimationFrame(tick); timeoutId = requestAnimationFrame(tick);
} }

View File

@@ -0,0 +1,58 @@
<script setup>
import axios from "axios";
import { ref, onMounted } from "vue";
const url =
"https://www.adam-french.co.uk/gitea/api/v1/users/adamf/activities/feeds?limit=1";
const feed = ref(null);
const isLoading = ref(true);
const hasError = ref(false);
async function checkFeed() {
try {
const res = await axios.get(url);
feed.value = res.data[0] || null;
hasError.value = false;
} catch (err) {
hasError.value = true;
} finally {
isLoading.value = false;
}
}
onMounted(() => {
checkFeed();
});
</script>
<template>
<div class="justify-center text-center">
<div v-if="isLoading">
<p>Loading latest activity...</p>
</div>
<div v-else-if="hasError">
<p>Could not fetch feed. Please try again later.</p>
</div>
<div v-else-if="feed" class="flex-col justify-center flex">
<h3>Last git activity</h3>
<img
:src="feed.act_user.avatar_url"
alt="User avatar"
class="avatar"
/>
<a :href="feed.repo.html_url">
<h3>repo: {{ feed.repo.full_name }}</h3>
</a>
<p>Action: {{ feed.op_type }}</p>
<p>Message: {{ JSON.parse(feed.content).Commits[0].Message }}</p>
<small> {{ new Date(feed.created).toLocaleString() }}</small>
</div>
<div v-else>
<p>No activity found.</p>
</div>
</div>
</template>

View File

@@ -13,7 +13,7 @@ const keys = ["name", "link"];
<template> <template>
<a v-for="(row, rowIndex) in linkArr" :key="rowIndex" :href="row.link"> <a v-for="(row, rowIndex) in linkArr" :key="rowIndex" :href="row.link">
<p class="bdr-2 bg-bg_primary"> <p class="bdr-2 bg-bg_tertiary">
{{ row.name }} {{ row.name }}
</p> </p>
</a> </a>

View File

@@ -6,8 +6,8 @@
<div v-else> <div v-else>
<img src="/img/tmpen31z3pe.PNG" /> <img src="/img/tmpen31z3pe.PNG" />
<div class="m-1"> <div class="m-1">
<p>Stream is offline. Tune in Fridays @ 6:00pm, Monday @ 8:00am</p> <p>Radio is offline. Message for info!</p>
<Button @click="checkStream()">Check Stream</Button> <Button class="w-full" @click="checkStream()">Check Stream</Button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -12,7 +12,7 @@ const router = createRouter({
{ {
path: "/cv", path: "/cv",
name: "cv", name: "cv",
component: () => import("../views/CV.vue"), component: () => import("../views/CV/CV.vue"),
}, },
{ {
path: "/admin", path: "/admin",

View File

@@ -1,603 +0,0 @@
<template>
<main>
<div class="a4page">
<div class="contact">
<h1>Adam French</h1>
<!-- <a href="covers.html"><img width=25 height=50 src="img/rune.png"></a> -->
<div class="contact-details">
<p>+447563266931</p>
<p>adam.a.french@outlook.com</p>
<p>www.adam-french.co.uk</p>
</div>
</div>
<h2>Profile</h2>
<p>
Recently graduated from the University of Leeds with a BSc
Computer Science with Mathematics (International) degree.
Currently self-studying and building projects aligned with the
type of roles I am seeking. I have a strong background across a
variety of programming languages and will be able to quickly get
on board with any codebase.
</p>
<p>
I am most keen to work for a company with altruistic values and
a focus on durable solutions. Looking forward to learning from
experts and collaborating with motivated individuals.
</p>
<h2>Personal Projects</h2>
<table>
<thead>
<tr>
<th>Project</th>
<th>Skills</th>
<th>Date</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Personal Websites</td>
<td>Nginx, Vue, Postgres, Docker, Go, Python</td>
<td>Ongoing</td>
<td class="row-leftalign">
My personal site, Currently
<b>self hosted</b>
using <b>listed skills</b>. In the past, I have used
Svelte, React/Redux, SQLite, Rust and Deno.
</td>
</tr>
<tr>
<td>Computer Graphics</td>
<td>Rust, Linear Algebra, Multithreading</td>
<td>2023</td>
<td class="row-leftalign">
A multithreaded, recursive ray tracer implemented in
Rust.
</td>
</tr>
<tr>
<td>Mobile Automata</td>
<td>Mathematica, JS</td>
<td>2024</td>
<td class="row-leftalign">
Investigated properties of cellular automata by
observing emergent behaviors through custom
simulations.
</td>
</tr>
<tr>
<td>Arduino Programming & Circuits</td>
<td>C++, Soldering, Embedded Systems</td>
<td>2022 - 2025</td>
<td class="row-leftalign">
Created decorations using salvaged components from
discarded electronics.
</td>
</tr>
<tr>
<td>Memory Palace Website</td>
<td>TS, Rust, React, Redux, SQLite</td>
<td>2025</td>
<td class="row-leftalign">
Full-stack web application aiming to make the
memory palace memorization technique easy.
</td>
</tr>
<tr>
<td>3D Printing</td>
<td>FreeCAD</td>
<td>Ongoing</td>
<td class="row-leftalign">
Designing quality of life objects using FreeCAD and
printing with a BambuLab A1.
</td>
</tr>
</tbody>
</table>
<h2>Education</h2>
<table>
<thead>
<tr>
<th>Location</th>
<th>Date</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>The University of Leeds</td>
<td>
<!-- <div style="display: flex; flex-direction: column; align-items: center;"> -->
<!-- <span>2021</span> -->
<!-- <span>to</span> -->
<!-- <span>2025</span> -->
<!-- </div> -->
2021-2025
</td>
<td class="row-leftalign">
<strong
>BSc Computer Science with Mathematics
(International)</strong
><br />
<strong
>Average:
81.1%&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;&ensp;(First
Class Honours) </strong
><br />
<strong>Relevant Courses: </strong>
Procedural Programming, Object Oriented Programming,
Algorithms and Data Structures I & II, Databases,
Computer Processors, Compiler Design and
Construction, Formal Languages and Finite Automata,
Probability and Statistics I, Machine Learning,
Graph Algorithms & Complexity Theory
</td>
</tr>
<tr>
<td>The University of Waterloo</td>
<td>
<!-- <div style="display: flex; flex-direction: column; align-items: center;"> -->
<!-- <span>2023</span> -->
<!-- <span>to</span> -->
<!-- <span>2024</span> -->
<!-- </div> -->
2023-2024
</td>
<td class="row-leftalign">
<strong>Average: 74.5%</strong>
<br />
<strong>Relevant Courses:</strong>
Applied Cryptography, Introduction to Computer
Graphics, Introduction to Rings and Fields with
Applications<br /><br />
</td>
</tr>
</tbody>
</table>
</div>
<div class="a4page">
<h2>Experience</h2>
<table>
<thead>
<tr>
<th>Role</th>
<th>Location</th>
<th>Date</th>
<th>Duties</th>
</tr>
</thead>
<tbody>
<tr>
<td>Student</td>
<td>Wolfram Summer School</td>
<td>2024</td>
<td class="row-leftalign">
Designed and completed a time-constrained research
project exploring Mobile Automata and conditions for
computational reversibility. Communicated findings
through visualizations and presentations.
</td>
</tr>
<tr>
<td>Bartender, Waiter, Cashier</td>
<td>Hospitality Venues</td>
<td>2018-2023</td>
<td class="row-leftalign">
Delivered heartfelt customer service in various
fast-paced, high-pressure hospitality environments.
</td>
</tr>
</tbody>
</table>
<h2>Commitments</h2>
<table>
<thead>
<tr>
<th>Activity</th>
<th>Date</th>
<th>Details</th>
</tr>
</thead>
<tbody>
<tr>
<td>Learning Mandarin</td>
<td>Ongoing</td>
<td class="row-leftalign">
Aiming to complete HSK 3 proficiency exam by
December 2026
</td>
</tr>
<!-- <tr> -->
<!-- <td>Cybersecurity Training</td> -->
<!-- <td>Ongoing</td> -->
<!-- <td class="row-leftalign"> -->
<!-- Using <em>pwn.college, tryhackme.com</em> to learn pentesting techniques.</td> -->
<!-- </tr> -->
<tr>
<td>Sports Activities</td>
<td>Ongoing</td>
<td class="row-leftalign">
Run weekly, active gym attendee, regularly go
hiking.
</td>
</tr>
<tr>
<td>meetup.com</td>
<td>Ongoing</td>
<td class="row-leftalign">
Attending various tech meetups and social events.
</td>
</tr>
<tr>
<td>Boardgames</td>
<td>Ongoing</td>
<td class="row-leftalign">
Meet up regularly to play the game
<i>Root</i>.
</td>
</tr>
<tr>
<td>Leetcode</td>
<td>Ongoing</td>
<td class="row-leftalign">
Do the leetcode daily challenge and hone in on
different programming languages.
</td>
</tr>
<tr>
<td>Construction and Landscaping</td>
<td>Ongoing</td>
<td class="row-leftalign">
Involved in building a house in Bulgaria.
</td>
</tr>
<tr>
<td>University of Waterloo Film Club</td>
<td>2023-2024</td>
<td class="row-leftalign">
Worked on student films <em>Moon King</em> and
<em>HAM</em>, available online.
</td>
</tr>
<tr>
<td>Socratica</td>
<td>2023-2024</td>
<td class="row-leftalign">
Worked with individuals exploring innovative tech.
</td>
</tr>
<tr>
<td>University of Leeds Hockey Club</td>
<td>2022-2023</td>
<td class="row-leftalign">
Played for the University of Leeds Hockey Club.
</td>
</tr>
<tr>
<td>Royal Air Force Air Cadets</td>
<td>2017-2020</td>
<td class="row-leftalign">
Achieved the role of Sergeant and Best Cadet"
award.
</td>
</tr>
</tbody>
</table>
<!-- <div class="interests"> -->
<!-- <table> -->
<!-- <tr><th>Personal qualities</th></tr> -->
<!-- <tr><td>Intuitive</td></tr> -->
<!-- <tr><td>Communicative</td></tr> -->
<!-- <tr><td>Adaptable</td></tr> -->
<!-- <tr><td>Versatile</td></tr> -->
<!-- <tr><td>Diligent</td></tr> -->
<!-- </table> -->
<!-- <table> -->
<!-- <tr><th>Interests</th></tr> -->
<!-- <tr><td>Neuroscience</td></tr> -->
<!-- <tr><td>Bouldering</td></tr> -->
<!-- <tr><td>Science Fiction</td></tr> -->
<!-- <tr><td>Mathematics</td></tr> -->
<!-- <tr><td>Hiking</td></tr> -->
<!-- </table> -->
<!-- <table> -->
<!-- <tr><th>Languages</th></tr> -->
<!-- <tr><td>Rust</td></tr> -->
<!-- <tr><td>HTML/JS</td></tr> -->
<!-- <tr><td>C/C++</td></tr> -->
<!-- <tr><td>React/Vue</td></tr> -->
<!-- <tr><td>Python</td></tr> -->
<!-- </table> -->
<!-- </div> -->
</div>
</main>
</template>
<style scoped>
/* Fonts */
/*@font-face {
font-family: "AldoTheApache";
src: url("/fonts/AldotheApache.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "RobotFont";
src: url("/fonts/Robot_Font.otf") format("opentype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "m12";
src: url("/fonts/m12.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}*/
@font-face {
font-family: "big_noodle_titling";
src: url("/fonts/big_noodle_titling.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "CreatoDisplay";
src: url("/fonts/CreatoDisplay-Bold.otf") format("opentype");
font-weight: normal;
font-style: normal;
}
/* Variables */
* {
/* Blue - Beige */
/* --primary: #153448;
--secondary: #3C5B6F;
--tertiary: #948979;
--quaternary: #f5bb78;
--background: #DFD0B8; */
/* Blue - Turqouise */
/* --primary: #161D6F;
--secondary: #0B2F9F;
--tertiary: #98DED9;
--quaternary: #C7FFD8;
--background: #C2EFD1; */
/* Red - Blue */
/* --primary: #ff204e; */
/* --secondary: #a0153e; */
/* --tertiary: #5d0341; */
/* --quaternary: #3a0e41; */
/* --background: #00224d; */
/* Blue - Brown */
/* --primary: #35374B; */
/* --secondary: #344955; */
/* --tertiary: #50727b; */
/* --quaternary: #78a083; */
/* --background: #c7b077; */
/* Black - White */
--primary: black;
--secondary: black;
--tertiary: black;
--quaternary: #cccccc;
--background: white;
/* Blue - White */
/* --primary: #201e43; */
/* --secondary: #134b70; */
/* --tertiary: #508c9b; */
/* --quaternary: #cceeee; */
/* --background: #eeeeee; */
--font-heading: big_noodle_titling;
--font-text: CreatoDisplay;
--font-size-text: 90%;
--font-size-heading: 2.5em;
--font-size-subheading: 1.5em;
--font-size-tableheading: 1.2em;
}
/* A5 Page */
.a5page {
/* overflow: scroll; */
display: flex;
flex-direction: column;
font-family: var(--font-text);
height: 148mm;
width: 210mm;
padding: 4mm;
box-sizing: border-box;
background-color: var(--background);
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
border: solid 2px var(--primary);
}
/* A4 Page */
.a4page {
line-height: 1.6;
font-family: var(--font-text);
width: 210mm;
/* Standard A4 width */
height: 297mm;
/* Standard A4 height */
padding: 8mm;
box-sizing: border-box;
background-color: var(--background);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
border: 1px solid var(--primary);
overflow: auto;
/* Enables scrolling when content exceeds height */
margin: auto auto;
/* Centers the page horizontally */
}
/* Component Styling */
main {
padding: 0px;
display: flex;
flex-direction: column;
height: fit-content;
}
span {
height: 2em;
}
h1,
h2,
h3,
h4 {
border: none;
color: var(--primary);
font-family: var(--font-heading);
text-transform: capitalize;
text-align: center;
}
h1 {
font-size: var(--font-size-heading);
}
h2 {
margin: 0px;
margin-bottom: 3px;
border-bottom: 1px solid var(--primary);
font-size: var(--font-size-subheading);
}
p {
color: var(--secondary);
font-size: var(--font-size-text);
margin-top: 0.3em;
margin-bottom: 0.5em;
}
table {
color: var(--secondary);
border-collapse: collapse;
border: 1px solid black;
}
td {
/* border: 2px solid var(--tertiary); */
color: var(--secondary);
border-top: 1px solid var(--tertiary);
padding: 1px 10px 1px 10px;
font-size: var(--font-size-text);
text-align: left;
}
th {
color: var(--secondary);
border: 2px solid var(--tertiary);
padding: 1px 0px 1px 7px;
font-family: var(--font-heading);
font-size: var(--font-size-tableheading);
background-color: var(--quaternary);
text-align: left;
}
a {
text-decoration: none;
color: inherit;
}
a:hover,
a:visited {
color: inherit;
}
/* Classes */
/* Cover Navigation (for ease of use) */
.cover-nav {
position: fixed;
top: 0.5vh;
/* Position the element at the top of the screen */
left: 80vw;
/* Position the element at the left of the screen */
border: 2px solid var(--tertiary);
width: 19.5vw;
/* Make the element span the width of the screen (optional) */
background-color: var(--background);
/* Set a background color to avoid overlap issues */
z-index: 1000;
/* Ensures the element is above other content */
}
.cover-nav td,
tr {
font-family: var(--font-text);
border: 0;
}
.cover-nav th {
text-align: center;
align-items: center;
border-bottom: 2px solid var(--tertiary);
}
@media print {
.no-print {
display: none !important;
}
}
/* Cover letter styling */
textarea {
width: 100%;
height: 100%;
padding: 10px;
box-sizing: border-box;
border: 0px solid var(--primary);
resize: none;
font-family: var(--font-text);
}
/* Contact At Top of Page */
.contact {
all: unset;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--primary);
}
.contact-details {
all: unset;
display: flex;
flex-direction: column;
text-align: right;
}
.contact-details p {
margin: 1px 0;
}
/* Interests and Skills at bottom of page */
.interests {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-top: solid 2px var(--primary);
}
.interests td,
tr,
th {
border: 0;
}
.row-leftalign {
/* background-image: url("https://www.fridakahlo.org/assets/img/paintings/without-hope.jpg"); */
text-align: left;
}
</style>

View File

@@ -0,0 +1,336 @@
<script setup>
import Project from "./Project.vue";
</script>
<template>
<main>
<div class="a4page">
<div class="flex flex-row justify-between">
<h1 class="name">Adam French</h1>
<!-- <a href="covers.html"><img width=25 height=50 src="img/rune.png"></a> -->
<div class="contact-details text-right">
<p>+447563266931</p>
<p>adam.a.french@outlook.com</p>
<a href="https://www.adam-french.co.uk">
www.adam-french.co.uk
</a>
</div>
</div>
<h2>Profile</h2>
<p>
Recent Computer Science with Mathematics (International)
graduate from the University of Leeds, awarded First Class
Honours (81.1%). Strong foundation in full-stack software
development, CI/CD workflows, and modern programming languages.
Experienced in creating scalable, maintainable systems and
motivated by solving complex technical problems. Enthusiastic
about working within organisations that promote innovation,
collaboration, and positive social impact.
</p>
<h2>Projects</h2>
<Project class="border-b border-dotted">
<template v-slot:left>
<a
href="https://www.adam-french.co.uk/gitea/adamf/web_server.git"
>
web_server.git
</a>
</template>
<template v-slot:top>
<small>
Nginx, Vue, Postgres, Docker, Go, Python, Rust -> Wasm,
Git Actions, JWT Auth
</small>
<small>2025</small>
</template>
<p>
Developed and self-hosted a personal website with a fully
automated maintenance CI/CD pipeline. Experimented with
diverse tech stacks including Svelte, React/Redux, SQLite,
Rust Actix, and Deno.
</p>
</Project>
<Project class="border-b border-dotted">
<template v-slot:left>
<a
href="https://www.adam-french.co.uk/gitea/adamf/tour.git"
>
tour.git
</a>
</template>
<template v-slot:top>
<small>Rust</small>
<small>2026</small>
</template>
<p>
Created a command-line tool for building and viewing
interactive code tutorials. Designed functionality analogous
to Git for intuitive version traversal and educational use.
</p>
</Project>
<Project class="border-b border-dotted">
<template v-slot:left>
<a
href="https://www.adam-french.co.uk/gitea/adamf/rust-raytracer.git"
>
rust-raytracer.git
</a>
</template>
<template v-slot:top>
<small>Rust, Linear Algebra, Multithreading</small>
<small>2023</small>
</template>
<p>
Built a parallelised, recursive ray tracer for realistic 3D
rendering as part of a university module. Focused on
algorithmic efficiency and low-level memory management in
Rust.
</p>
</Project>
<Project>
<template #left>
<p>
<a
class="text-center w-full"
href="https://community.wolfram.com/groups/-/m/t/3210947"
>
Wolfram Summer School
</a>
</p>
</template>
<template #top>
<small>Wolfram Mathematica</small>
<small>2024</small>
</template>
<p>
Designed and implemented a research project on Mobile
Automata, including data visualisation and presentation of
findings. Completed the project within a short deadline and
collaborated with academic mentors to refine outcomes.
</p>
</Project>
<h2>University & Modules</h2>
<div class="w-full h-fit flex-row flex gap-5">
<div class="flex-1 border-r border-dotted pr-3">
<h3>University of Leeds</h3>
<div
class="flex-row flex place-content-between m-auto place-items-center"
>
<small> 81.1% (First Class Honours)</small>
<small> 2021-2025 </small>
</div>
<small>BSc Computer Science with Mathematics </small>
<ul>
<li>Procedural & Object Oriented Programming,</li>
<li></li>
<li>Algorithms and Data Structures I & II</li>
<li>Databases</li>
<li>Computer Processors</li>
<li>Compiler Design and Construction</li>
<li>Formal Languages and Finite Automata</li>
<li>Probability and Statistics I</li>
<li>Machine Learning</li>
<li>Graph Algorithms & Complexity Theory</li>
</ul>
</div>
<div class="flex-1 pl-3">
<h3>The University of Waterloo</h3>
<div
class="flex-row flex place-content-between m-auto place-items-center"
>
<small>---</small>
<small> 2023-2024 </small>
</div>
<div class="flex-row flex place-content-between"></div>
<ul>
<li>Applied Cryptography</li>
<li>Introduction to Computer Graphics</li>
<li>
Introduction to Rings and Fields with Applications
</li>
</ul>
</div>
</div>
</div>
<!-- <div class="a4page"> -->
<!-- <h2>Experience</h2> -->
<!-- <Project> -->
<!-- <template #left> -->
<!-- <p>Hospitality</p> -->
<!-- </template> -->
<!-- <template #top> -->
<!-- <small>Cashier, Bartender, Waiter</small> -->
<!-- <small>2018-2023</small> -->
<!-- </template> -->
<!-- <p> -->
<!-- Worked at venues including: -->
<!-- <em>Belgrave Music Hall</em>, -->
<!-- <em>The Crown and Anchor Eastbourne</em>, -->
<!-- <em>To The Rise Bakery</em>, -->
<!-- <em>BFI Riverfront Kitchen</em>. -->
<!-- </p> -->
<!-- </Project> -->
<!-- <h2>Commitments</h2> -->
<!-- <ul> -->
<!-- <li>Gym</li> -->
<!-- <li>Climbing</li> -->
<!-- <li>Meetup.com</li> -->
<!-- <li>Boardgames</li> -->
<!-- <li>Leetcode</li> -->
<!-- <li>Learning Mandarin</li> -->
<!-- </ul> -->
<!-- </div> -->
</main>
</template>
<style scoped>
/* Fonts */
@font-face {
font-family: "big_noodle_titling";
src: url("/fonts/big_noodle_titling.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "CreatoDisplay";
src: url("/fonts/CreatoDisplay-Bold.otf") format("opentype");
font-weight: normal;
font-style: normal;
}
/* Variables */
* {
/* Black - White */
--primary: black;
--secondary: #0000ff;
--tertiary: #ff0000;
--quaternary: #cccccc;
--background: white;
--font-heading: big_noodle_titling;
--font-text: CreatoDisplay;
--font-size-name: 2.5em;
--font-size-text: 100%;
--font-size-small: 0.9em;
--font-size-heading: 2.1em;
--font-size-subheading: 1.7em;
--font-size-subsubheading: 1.4em;
}
/* A4 Page */
.a4page {
line-height: 1.6;
font-family: var(--font-text);
width: 210mm;
/* Standard A4 width */
height: 297mm;
/* Standard A4 height */
padding: 5mm;
box-sizing: border-box;
background-color: var(--background);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
border: 1px solid var(--primary);
overflow: hidden;
/* Enables scrolling when content exceeds height */
margin: auto auto;
/* Centers the page horizontally */
}
/* Component Styling */
main {
padding: 0px;
display: flex;
flex-direction: column;
height: fit-content;
}
span {
height: 2em;
}
h1,
h2,
h3,
h4 {
border: none;
color: var(--primary);
font-family: var(--font-heading);
text-transform: capitalize;
}
h1 {
font-size: var(--font-size-heading);
}
h2 {
border-bottom: 1px solid var(--primary);
font-size: var(--font-size-subheading);
}
h3 {
font-size: var(--font-size-subsubheading);
}
a:hover {
color: var(--tertiary);
}
a {
background-color: transparent;
color: var(--secondary);
}
p {
margin-bottom: 0.2em;
color: var(--primary);
font-size: var(--font-size-text);
}
table {
color: var(--secondary);
border-collapse: collapse;
border: 1px solid black;
}
td {
/* border: 2px solid var(--tertiary); */
color: var(--secondary);
border-top: 1px solid var(--tertiary);
padding: 1px 10px 1px 10px;
font-size: var(--font-size-text);
text-align: left;
}
th {
color: var(--secondary);
border: 2px solid var(--tertiary);
padding: 1px 0px 1px 7px;
font-family: var(--font-heading);
font-size: var(--font-size-subsubheading);
background-color: var(--quaternary);
text-align: left;
}
@media print {
.no-print {
display: none !important;
}
}
small {
font-size: var(--font-size-small);
color: var(--primary);
}
ul {
font-size: var(--font-size-small);
}
li {
font-size: var(--font-size-small);
color: var(--primary);
}
</style>

View File

@@ -0,0 +1,17 @@
<script setup></script>
<template>
<div class="flex-row flex">
<div class="w-2/7 p-5 m-auto">
<slot name="left" />
</div>
<div class="w-full p-2">
<div
class="flex-row flex place-content-between m-auto place-items-center"
>
<slot name="top" />
</div>
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { useTemplateRef, ref, onMounted, onUnmounted } from 'vue';
const display = useTemplateRef('display')
const displayText = ref("");
const charHeight: number = 14;
const charWidth: number = charHeight * 0.6;
let n: number;
let m: number;
function setup() {
display.value.style.fontSize = `${charHeight}px`;
display.value.style.lineHeight = `${charHeight}px`;
fillDisplay()
}
function fillDisplay() {
// M rows N columns
m = Math.floor(display.value.offsetHeight / charHeight);
n = Math.floor(display.value.offsetWidth / charWidth);
const row = ' '.repeat(n);
displayText.value = (row + '\n').repeat(m - 1) + row
}
function close() {
displayText.value = ""
}
onMounted(() => {
setup()
})
onUnmounted(() => {
close()
})
</script>
<template>
<pre class="overflow-scroll w-full h-full bg-black text-white m-0 p-0" id="container" ref="display">{{ displayText
}}</pre>
</template>

View File

@@ -5,10 +5,10 @@ import Header from "@/components/text/Header.vue";
const images = [ const images = [
{ url: "/img/memes/pidgeon.gif", comment: "鸟" }, { url: "/img/memes/pidgeon.gif", comment: "鸟" },
//{ url: "/img/memes/no_slip.png" }, // { url: "/img/memes/no_slip.png" },
//{ url: "/img/memes/epic.jpeg" }, //{ url: "/img/memes/epic.jpeg" },
{ url: "/img/bedroom/img2.png", comment: "办公桌" }, // { url: "/img/bedroom/img2.png", comment: "办公桌" },
{ url: "/img/bedroom/img1.png", comment: "床" }, // { url: "/img/bedroom/img1.png", comment: "床" },
]; ];
const currentIndex = ref(0); const currentIndex = ref(0);
@@ -18,6 +18,12 @@ const currentUrl = computed(() => images[currentIndex.value].url);
let nextId; let nextId;
function nextImage() { function nextImage() {
clearTimeout(nextId);
currentIndex.value = (currentIndex.value + 1) % images.length;
nextId = setTimeout(nextImage, 10000);
}
function nextRandomImage() {
clearTimeout(nextId); clearTimeout(nextId);
let newIndex; let newIndex;
do { do {

View File

@@ -1,11 +1,15 @@
<script setup> <script setup>
import Timer from "@/components/util/Timer.vue"; import Timer from "@/components/util/Timer.vue";
import Elle from "@/components/elle/Elle.vue";
import Time from "@/components/util/Time.vue"; import Time from "@/components/util/Time.vue";
import Radio from "@/components/util/Radio.vue";
import Elle from "@/components/elle/Elle.vue";
import Chat from "@/components/util/Chat.vue"; import Chat from "@/components/util/Chat.vue";
import MusicPlayer from "@/components/util/MusicPlayer.vue"; import MusicPlayer from "@/components/util/MusicPlayer.vue";
import CommitHistory from "@/components/util/CommitHistory.vue";
import Intro from "./Intro.vue"; import Intro from "./Intro.vue";
import Intro2 from "./Intro2.vue";
import BadApple from "./BadApple.vue";
import Stamps from "./Stamps.vue"; import Stamps from "./Stamps.vue";
import Listening from "./Listening.vue"; import Listening from "./Listening.vue";
import Links from "./Links.vue"; import Links from "./Links.vue";
@@ -14,15 +18,15 @@ import Collage from "./Collage.vue";
import Favorites from "./Favorites.vue"; import Favorites from "./Favorites.vue";
import Gym from "./Gym.vue"; import Gym from "./Gym.vue";
import Consumption from "./Consumption.vue"; import Consumption from "./Consumption.vue";
import UtenaFrame from "@/components/borders/UtenaFrame.vue";
</script> </script>
<template> <template>
<main class="halftone justify-center flex flex-row w-full h-full"> <main class="halftone justify-center flex flex-row w-full h-full">
<div class="h-fit flex flex-row"> <div class="h-fit flex flex-row">
<div class="a4page-portrait homeGrid relative bdr-1"> <div class="a4page-portrait homeGrid relative bdr-1">
<Intro class="intro" /> <!-- <Intro class="intro" /> -->
<Intro2 class="intro" />
<!-- <BadApple class="intro" /> -->
<Listening class="listening" /> <Listening class="listening" />
<Stamps class="stamps" /> <Stamps class="stamps" />
<Feed class="feed" /> <Feed class="feed" />
@@ -33,11 +37,20 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
<Gym class="gym" /> <Gym class="gym" />
</div> </div>
<div <div
class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60 border-2" class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60"
> >
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1 gap-2">
<Time class="bg-bg_primary border-primary border-b" /> <Time
<Timer class="border-primary border-b bg-bg_primary" /> class="bg-bg_primary border-primary border text-center"
/>
<Timer class="border-primary border bg-bg_primary" />
<Radio
class="border-primary border bg-bg_primary text-center"
/>
<CommitHistory
class="border-primary border bg-bg_primary text-center"
/>
<!-- <Elle class="flex-1" /> --> <!-- <Elle class="flex-1" /> -->
<!-- <Chat class="bdr-2 bg-bg_primary" /> --> <!-- <Chat class="bdr-2 bg-bg_primary" /> -->
<!-- <MusicPlayer /> --> <!-- <MusicPlayer /> -->
@@ -117,6 +130,7 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
grid-column: span 4; grid-column: span 4;
grid-row: span 2; grid-row: span 2;
} }
.gym { .gym {
grid-column: span 3; grid-column: span 3;
grid-row: span 2; grid-row: span 2;

View File

@@ -4,34 +4,27 @@ import Paragraph from "@/components/text/Paragraph.vue";
</script> </script>
<template> <template>
<div <div class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start">
class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start" <Header>Yo</Header>
> <!-- <Header>Intro</Header> -->
<Header>Intro</Header> <!-- <Paragraph> -->
<Paragraph> <!-- Hi, I'm Adam, thank you for visiting my website. -->
Hi, I'm Adam, thank you for visiting my website. I'm currently a 20 <!-- </Paragraph> -->
something graduate looking for work. I like to game, listen to lots <!-- <Header>Getting around</Header> -->
of music and occasionally watch anime. <!-- <Paragraph> -->
</Paragraph> <!-- Pages available can be traversed through links below. I am hoping to -->
<Header>Getting around</Header> <!-- add some shrines, code-walkthoughs, live chat and page transitions -->
<Paragraph> <!-- at a later date. -->
Pages available can be traversed through links below. I am hoping to <!-- </Paragraph> -->
add some shrines, code-walkthoughs, live chat and page transitions <!-- <Header>Contact</Header> -->
at a later date. <!-- <Paragraph> -->
</Paragraph> <!-- Please email me <a href="mailto:adam.a.french@outlook.com">here</a>, -->
<Header>Contact</Header> <!-- or contact me though any of the social medias linked. -->
<Paragraph> <!-- </Paragraph> -->
Please email me <a href="mailto:adam.a.french@outlook.com">here</a>, <!-- <Header>A Quote</Header> -->
or contact me though any of the social medias linked. <!-- <Paragraph> -->
</Paragraph> <!-- One crossed wire, one wayward pinch of potassium chlorate, one -->
<Header>A Quote</Header> <!-- errant twitch, and KA-BLOOIE! -->
<!-- <p> <!-- </Paragraph> -->
What makes me a good demoman? If I were a bad demoman, I wouldn't be
sittin' here discussin' it with you, now would I?!
</p> -->
<Paragraph>
One crossed wire, one wayward pinch of potassium chlorate, one
errant twitch, and KA-BLOOIE!
</Paragraph>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { rand } from "@vueuse/core";
import { ref, onMounted, onUnmounted, nextTick } from "vue";
interface Item {
x: number;
y: number;
dx: number;
dy: number;
content: string;
}
const container = ref<HTMLDivElement | null>(null);
const itemEls = ref<HTMLDivElement[]>([]);
const phrases = [
"Welcome to my website",
"I'm looking to do a big revamp",
"A4 sheets of paper are what I'm used to",
"I'd love to know your recommendations",
"for very short books",
"Message me by any means necessary",
"I like anime, all kinds of music and sci fic",
];
const items = ref<Item[]>(
phrases.map((text, i) => ({
x: i * 20,
y: i * 20,
dx: rand(0, 30) / 100,
dy: 0.5,
content: text,
})),
);
let rafId = 0;
function animate() {
const c = container.value;
if (!c) return;
const cw = c.clientWidth;
const ch = c.clientHeight;
items.value.forEach((item, i) => {
const el = itemEls.value[i];
if (!el) return;
const ew = el.offsetWidth;
const eh = el.offsetHeight;
item.x += item.dx;
item.y += item.dy;
if (item.x < 0 || item.x > cw - ew) item.dx *= -1;
if (item.y < 0 || item.y > ch - eh) item.dy *= -1;
});
rafId = requestAnimationFrame(animate);
}
onMounted(async () => {
await nextTick();
rafId = requestAnimationFrame(animate);
});
onUnmounted(() => {
cancelAnimationFrame(rafId);
});
</script>
<template>
<div
ref="container"
class="w-full h-full min-h-125 relative overflow-hidden"
>
<div
v-for="(item, i) in items"
:key="i"
ref="itemEls"
class="absolute w-fit h-fit"
:style="{
transform: `translate(${item.x}px, ${item.y}px)`,
}"
>
<h1>
{{ item.content }}
</h1>
</div>
</div>
</template>

View File

@@ -1,7 +1,6 @@
<script setup> <script setup>
import RouterTable from "@/components/util/RouterTable.vue"; import RouterTable from "@/components/util/RouterTable.vue";
import LinkTable from "@/components/util/LinkTable.vue"; import LinkTable from "@/components/util/LinkTable.vue";
import Markdown from "@/components/util/Markdown.vue";
const site_links = [ const site_links = [
{ name: "CV", link: "/cv" }, { name: "CV", link: "/cv" },
@@ -12,6 +11,7 @@ const site_links = [
]; ];
const social_links = [ const social_links = [
{ name: "Gitea", link: "/gitea/explore/repos" },
{ name: "Steam", link: "https://steamcommunity.com/id/SteveThePug" }, { name: "Steam", link: "https://steamcommunity.com/id/SteveThePug" },
{ name: "Github", link: "https://github.com/SteveThePug" }, { name: "Github", link: "https://github.com/SteveThePug" },
{ name: "Spotify", link: "https://open.spotify.com/user/stevethepug" }, { name: "Spotify", link: "https://open.spotify.com/user/stevethepug" },

View File

@@ -42,7 +42,8 @@ img {
width: 89px; width: 89px;
height: 59px; height: 59px;
} }
.tst { .tst {
width: calc(89px * 4); min-width: calc(89px * 4);
} }
</style> </style>

974
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@vueuse/core": "^14.2.0" "@vueuse/core": "^14.2.0",
"vite": "^7.3.1"
} }
} }

View File

@@ -1,22 +1,49 @@
# TODO # Introduction
- ML for 4chan
![screenshot](nginx/vue/public/img/screenshot.png)
Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting.
This website is currently self hosted on my Rasberry PI. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :).
# Future ideas
- Rust to wasm
- ML for chatboards
- Cache requests - Cache requests
- Login to add / remove posts (auth) - Design more webpages
- Design webpage
- Calendar to show radio times - Calendar to show radio times
- Nice smooth function background - Nice smooth function background and transitions
- Shrines - Design shrines
- Redis (not really) - Redis (not really but practical experience)
# .env # .env
These environment variables are found in the `.env` file. The use of environment variables can be found by reading the code so the security of the variable names are not significant.
```
POSTGRES_USER= POSTGRES_USER=
POSTGRES_PASSWORD= POSTGRES_PASSWORD=
POSTGRES_DB= POSTGRES_DB=
POSTGRES_PORT= POSTGRES_PORT=
POSTGRES_HOST= POSTGRES_HOST=
GITEA_HOST=
GITEA_PORT=
POSTGRES_GITEA_DB=
GITEA_RUNNER_HOST=
GITEA_RUNNER_NAME=
GITEA_RUNNER_REGISTRATION_TOKEN=
BACKEND_PORT= BACKEND_PORT=
BACKEND_HOST= BACKEND_HOST=
BACKEND_SECRET=
BACKEND_ENDPOINT=
OBSIDIAN_DIR=
SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET= SPOTIFY_CLIENT_SECRET=
SPOTIFY_REDIRECT_URI= SPOTIFY_REDIRECT_URI=
@@ -32,3 +59,5 @@ ICECAST_MOUNT=
DOMAIN= DOMAIN=
EMAIL= EMAIL=
```