Compare commits

...

21 Commits

Author SHA1 Message Date
98b11f7b9b Skeleton: Subscribe to pocketbase realtime events for all collections
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 54s
2025-02-17 23:33:35 +01:00
aaf919af0a Lib: Add pocketbase realtime subscribe/unsubscribe helpers 2025-02-17 23:33:23 +01:00
8c2754cebc Racepicks: Also fetch missing substitutions 2025-02-17 23:32:30 +01:00
ba5dac81fd Racepicks: Fix picked users grid padding inconsistency 2025-02-17 23:32:18 +01:00
5f73f0f952 Lib: Remove SkeletonData schema and use correct PageData types instead 2025-02-17 23:31:57 +01:00
4a1fcc6447 Skeleton: Some login logic fixes 2025-02-17 22:56:03 +01:00
04b69611a1 Skeleton: Define dependencies when fetching data 2025-02-17 21:15:00 +01:00
25a55ec94e Skeleton: Delegate data fetching to specific routes and only load what's needed 2025-02-17 21:12:11 +01:00
2b90f977d5 Lib: Fix invalid field in schema 2025-02-17 20:59:56 +01:00
8c8be5273b Lib: Move data fetching functions to library 2025-02-17 20:59:50 +01:00
37f4234c87 Lib: Implement driver headshot compression 2025-02-17 18:15:32 +01:00
29d992affe Lib: Implement race pictogram compression 2025-02-17 18:15:23 +01:00
24d557958a Lib: Implement team banner compression 2025-02-17 18:15:15 +01:00
524ccc13d2 Skeleton: Implement user avatar compression 2025-02-17 18:15:06 +01:00
8142c72c93 Api/Compress: Add image compression server endpoint 2025-02-17 18:14:50 +01:00
8ca1e3d511 Env: Remove obsolete node deps + re-add sharp 2025-02-17 18:14:34 +01:00
6093553ce5 Lib: Update server-side image compression 2025-02-17 18:14:18 +01:00
bd7e962f83 Lib: Fix deleting from wrong collection in RaceCard 2025-02-17 18:07:09 +01:00
9e39547936 Skeleton: Fetch RaceResults asynchronously in root layout 2025-02-17 15:58:07 +01:00
e58b94022a Env: Remove obsolete flake contents 2025-02-17 15:51:20 +01:00
c211aa21bb Env: Remove obsolete node deps + add image-conversion 2025-02-17 15:51:11 +01:00
25 changed files with 721 additions and 463 deletions

View File

@ -74,35 +74,6 @@
timple timple
]; ];
}; };
f1python = pkgs.python312.withPackages (p:
with p; [
# Basic
rich
# Web
flask
flask-sqlalchemy
flask-caching
sqlalchemy
# Test
pytest
# TODO: For some reason, listing those under fastf1.dependencies doesn't work???
matplotlib
numpy
pandas
python-dateutil
requests
requests-cache
scipy
rapidfuzz
websockets
timple
fastf1
]);
in { in {
devShell = pkgs.devshell.mkShell { devShell = pkgs.devshell.mkShell {
name = "Formula11"; name = "Formula11";
@ -110,16 +81,6 @@
packages = with pkgs; [ packages = with pkgs; [
nodejs_23 nodejs_23
pocketbase pocketbase
# nodePackages.autoprefixer
# nodePackages.postcss
# nodePackages.postcss-cli
# nodePackages.sass
# nodePackages.svelte-check
# nodePackages.tailwindcss
# f1python
# sqlitebrowser
]; ];
# Use $1 for positional args # Use $1 for positional args

317
package-lock.json generated
View File

@ -15,13 +15,11 @@
"@fsouza/prettierd": "^0.25.4", "@fsouza/prettierd": "^0.25.4",
"@skeletonlabs/skeleton": "^2.10.4", "@skeletonlabs/skeleton": "^2.10.4",
"@skeletonlabs/tw-plugin": "^0.4.0", "@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.1", "@sveltejs/kit": "^2.16.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@types/node": "^22.10.10", "@types/node": "^22.10.10",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"pocketbase": "^0.25.1", "pocketbase": "^0.25.1",
@ -33,7 +31,6 @@
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"uuid": "^11.0.5",
"vite": "^6.0.11" "vite": "^6.0.11"
} }
}, },
@ -87,14 +84,14 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.26.7", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@babel/types": "^7.26.7" "@babel/types": "^7.26.9"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -104,9 +101,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.26.7", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -1184,9 +1181,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==", "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1198,9 +1195,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==", "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1212,9 +1209,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==", "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1226,9 +1223,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==", "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1240,9 +1237,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==", "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1254,9 +1251,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==", "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1268,9 +1265,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==", "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1282,9 +1279,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==", "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1296,9 +1293,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==", "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1310,9 +1307,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==", "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1324,9 +1321,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==", "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -1338,9 +1335,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==", "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1352,9 +1349,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==", "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1366,9 +1363,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==", "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1380,9 +1377,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==", "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1394,9 +1391,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==", "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1408,9 +1405,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==", "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1422,9 +1419,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==", "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1436,9 +1433,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==", "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1472,19 +1469,6 @@
"tailwindcss": ">=3.0.0" "tailwindcss": ">=3.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-auto": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz",
"integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-meta-resolve": "^4.1.0"
},
"peerDependencies": {
"@sveltejs/kit": "^2.0.0"
}
},
"node_modules/@sveltejs/adapter-node": { "node_modules/@sveltejs/adapter-node": {
"version": "5.2.12", "version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
@ -1502,9 +1486,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.16.1", "version": "2.17.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.16.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.2.tgz",
"integrity": "sha512-2pF5sgGJx9brYZ/9nNDYnh5KX0JguPF14dnvvtf/MqrvlWrDj/e7Rk3LBJPecFLLK1GRs6ZniD24gFPqZm/NFw==", "integrity": "sha512-Vypk02baf7qd3SOB1uUwUC/3Oka+srPo2J0a8YN3EfJypRshDkNx9HzNKjSmhOnGWwT+SSO06+N0mAb8iVTmTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1607,9 +1591,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.0", "version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1623,17 +1607,10 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
"integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -1646,21 +1623,21 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
"integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.21.0", "@typescript-eslint/types": "8.24.0",
"@typescript-eslint/visitor-keys": "8.21.0", "@typescript-eslint/visitor-keys": "8.24.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"minimatch": "^9.0.4", "minimatch": "^9.0.4",
"semver": "^7.6.0", "semver": "^7.6.0",
"ts-api-utils": "^2.0.0" "ts-api-utils": "^2.0.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1674,14 +1651,14 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.21.0", "version": "8.24.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
"integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.21.0", "@typescript-eslint/types": "8.24.0",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
@ -1927,9 +1904,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001695", "version": "1.0.30001700",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
"integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2156,9 +2133,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.88", "version": "1.5.101",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz",
"integrity": "sha512-K3C2qf1o+bGzbilTDCTBhTQcMS9KW60yTAaTeeXsfvQuTDDwlokLam/AdqlqcSy9u4UainDgsHV23ksXAOgamw==", "integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -2242,9 +2219,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "1.4.3", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz",
"integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==", "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2276,9 +2253,9 @@
} }
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.18.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@ -2886,9 +2863,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.1", "version": "8.5.2",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -3036,9 +3013,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.4.2", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -3173,9 +3150,9 @@
} }
}, },
"node_modules/readdirp": { "node_modules/readdirp": {
"version": "4.1.1", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -3219,9 +3196,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.32.0", "version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -3235,25 +3212,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.32.0", "@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.32.0", "@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.32.0", "@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.32.0", "@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.32.0", "@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.32.0", "@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.32.0", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.32.0", "@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.32.0", "@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.32.0", "@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.32.0", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.32.0", "@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.32.0", "@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.32.0", "@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.32.0", "@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.32.0", "@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.32.0", "@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.32.0", "@rollup/rollup-win32-x64-msvc": "4.34.8",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -3295,9 +3272,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -3579,9 +3556,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.19.6", "version": "5.20.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.6.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.1.tgz",
"integrity": "sha512-6ydekB3qyqUal+UhfMjmVOjRGtxysR8vuiMhi2nwuBtPJWnctVlsGspjVFB05qmR+TXI1emuqtZt81c0XiFleA==", "integrity": "sha512-aCARru2WTdzJl55Ws8SK27+kvQwd8tijl4kY7NoDUXUHtTHhxMa8Lf6QNZKmU7cuPu3jjFloDO1j5HgYJNIIWg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -3807,9 +3784,9 @@
} }
}, },
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
"integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -3893,30 +3870,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.0.11", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.24.2", "esbuild": "^0.24.2",
"postcss": "^8.4.49", "postcss": "^8.5.1",
"rollup": "^4.23.0" "rollup": "^4.30.1"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"

View File

@ -14,13 +14,11 @@
"@fsouza/prettierd": "^0.25.4", "@fsouza/prettierd": "^0.25.4",
"@skeletonlabs/skeleton": "^2.10.4", "@skeletonlabs/skeleton": "^2.10.4",
"@skeletonlabs/tw-plugin": "^0.4.0", "@skeletonlabs/tw-plugin": "^0.4.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.16.1", "@sveltejs/kit": "^2.16.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@types/node": "^22.10.10", "@types/node": "^22.10.10",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"pocketbase": "^0.25.1", "pocketbase": "^0.25.1",
@ -32,7 +30,6 @@
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"uuid": "^11.0.5",
"vite": "^6.0.11" "vite": "^6.0.11"
}, },
"dependencies": { "dependencies": {

View File

@ -9,17 +9,19 @@
type ToastStore, type ToastStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Button, Input, Card, Dropdown } from "$lib/components"; import { Button, Input, Card, Dropdown } from "$lib/components";
import type { Driver, SkeletonData } from "$lib/schema"; import type { Driver } from "$lib/schema";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { team_dropdown_options } from "$lib/dropdown"; import { team_dropdown_options } from "$lib/dropdown";
import { get_driver_headshot_template } from "$lib/database"; import { get_driver_headshot_template } from "$lib/database";
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import { error } from "@sveltejs/kit";
import type { PageData } from "../../../routes/data/season/drivers/$types";
interface DriverCardProps { interface DriverCardProps {
/** Data passed from the page context */ /** Data passed from the page context */
data: SkeletonData; data: PageData;
/** The [Driver] object used to prefill values. */ /** The [Driver] object used to prefill values. */
driver?: Driver; driver?: Driver;
@ -51,7 +53,6 @@
let active_value: boolean = $state(driver?.active ?? true); let active_value: boolean = $state(driver?.active ?? true);
// Database actions // Database actions
// TODO: Headshot compression
const update_driver = (create?: boolean): (() => Promise<void>) => { const update_driver = (create?: boolean): (() => Promise<void>) => {
const handler = async (): Promise<void> => { const handler = async (): Promise<void> => {
if (!firstname_input_value || firstname_input_value === "") { if (!firstname_input_value || firstname_input_value === "") {
@ -71,21 +72,47 @@
return; return;
} }
// Headshot handling
let headshot_avif: Blob | undefined = undefined;
const headshot_file: File | undefined =
headshot_file_value && headshot_file_value.length === 1
? headshot_file_value[0]
: undefined;
if (headshot_file) {
const headshot_formdata: FormData = new FormData();
headshot_formdata.append("image", headshot_file);
headshot_formdata.append("width", DRIVER_HEADSHOT_WIDTH.toString());
headshot_formdata.append("height", DRIVER_HEADSHOT_HEIGHT.toString());
try {
const response = await fetch("/api/compress", {
method: "POST",
body: headshot_formdata,
});
if (!response.ok) {
error(500, "Compression failed.");
}
headshot_avif = await response.blob();
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
}
const driver_data = { const driver_data = {
firstname: firstname_input_value, firstname: firstname_input_value,
lastname: lastname_input_value, lastname: lastname_input_value,
code: code_input_value, code: code_input_value,
team: team_select_value, team: team_select_value,
active: active_value, active: active_value,
headshot: headshot: headshot_avif,
headshot_file_value && headshot_file_value.length === 1
? headshot_file_value[0]
: undefined,
}; };
try { try {
if (create) { if (create) {
if (!headshot_file_value || headshot_file_value.length !== 1) { if (!headshot_avif) {
toastStore.trigger(get_error_toast("Please upload a single driver headshot!")); toastStore.trigger(get_error_toast("Please upload a single driver headshot!"));
return; return;
} }

View File

@ -8,17 +8,19 @@
type ToastStore, type ToastStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Button, Card, Input } from "$lib/components"; import { Button, Card, Input } from "$lib/components";
import type { Race, SkeletonData } from "$lib/schema"; import type { Race } from "$lib/schema";
import { RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_WIDTH } from "$lib/config"; import { RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_WIDTH } from "$lib/config";
import { get_race_pictogram_template } from "$lib/database"; import { get_race_pictogram_template } from "$lib/database";
import { format_date } from "$lib/date"; import { format_date } from "$lib/date";
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import { error } from "@sveltejs/kit";
import type { PageData } from "../../../routes/data/season/races/$types";
interface RaceCardProps { interface RaceCardProps {
/** Data passed from the page context */ /** Data passed from the page context */
data: SkeletonData; data: PageData;
/** The [Race] object used to prefill values. */ /** The [Race] object used to prefill values. */
race?: Race; race?: Race;
@ -68,7 +70,6 @@
} }
// Database actions // Database actions
// TODO: Pictogram compression
const update_race = (create?: boolean): (() => Promise<void>) => { const update_race = (create?: boolean): (() => Promise<void>) => {
const handler = async (): Promise<void> => { const handler = async (): Promise<void> => {
if (!name_value || name_value === "") { if (!name_value || name_value === "") {
@ -92,6 +93,33 @@
return; return;
} }
// Pictogram handling
let pictogram_avif: Blob | undefined = undefined;
const pictogram_file: File | undefined =
pictogram_value && pictogram_value.length === 1 ? pictogram_value[0] : undefined;
if (pictogram_file) {
const pictogram_formdata: FormData = new FormData();
pictogram_formdata.append("image", pictogram_file);
pictogram_formdata.append("width", RACE_PICTOGRAM_WIDTH.toString());
pictogram_formdata.append("height", RACE_PICTOGRAM_HEIGHT.toString());
try {
const response = await fetch("/api/compress", {
method: "POST",
body: pictogram_formdata,
});
if (!response.ok) {
error(500, "Compression failed.");
}
pictogram_avif = await response.blob();
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
}
const race_data = { const race_data = {
name: name_value, name: name_value,
step: step_value, step: step_value,
@ -106,12 +134,12 @@
: undefined, : undefined,
qualidate: new Date(qualidate_value).toISOString(), qualidate: new Date(qualidate_value).toISOString(),
racedate: new Date(racedate_value).toISOString(), racedate: new Date(racedate_value).toISOString(),
pictogram: pictogram_value && pictogram_value.length === 1 ? pictogram_value[0] : undefined, pictogram: pictogram_avif,
}; };
try { try {
if (create) { if (create) {
if (!pictogram_value || pictogram_value.length !== 1) { if (!pictogram_avif) {
toastStore.trigger(get_error_toast("Please upload a single pictogram!")); toastStore.trigger(get_error_toast("Please upload a single pictogram!"));
return; return;
} }
@ -139,7 +167,7 @@
} }
try { try {
await pb.collection("raceresults").delete(race.id); await pb.collection("races").delete(race.id);
invalidateAll(); invalidateAll();
modalStore.close(); modalStore.close();
} catch (error) { } catch (error) {

View File

@ -1,14 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Card, Button, Dropdown } from "$lib/components"; import { Card, Button, Dropdown } from "$lib/components";
import type { import type { Driver, RacePick, Substitution } from "$lib/schema";
CurrentPickedUser,
Driver,
Race,
RacePick,
RaceResult,
SkeletonData,
Substitution,
} from "$lib/schema";
import { get_by_value, get_driver_headshot_template } from "$lib/database"; import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { import {
getModalStore, getModalStore,
@ -21,15 +13,11 @@
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import type { PageData } from "../../../routes/racepicks/$types";
interface RacePickCardProps { interface RacePickCardProps {
/** Data passed from the page context */ /** Data passed from the page context */
data: SkeletonData & { data: PageData;
currentrace: Race;
racepicks: Promise<RacePick[]>;
currentpickedusers: Promise<CurrentPickedUser[]>;
raceresults: Promise<RaceResult[]>;
};
/** The [RacePick] object used to prefill values. */ /** The [RacePick] object used to prefill values. */
racepick?: RacePick; racepick?: RacePick;
@ -59,7 +47,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!racepick); let required: boolean = $derived(!racepick);
let disabled: boolean = $derived(!data.admin); let disabled: boolean = false; // TODO: Datelock
let pxx_select_value: string = $state(racepick?.pxx ?? ""); let pxx_select_value: string = $state(racepick?.pxx ?? "");
let dnf_select_value: string = $state(racepick?.dnf ?? ""); let dnf_select_value: string = $state(racepick?.dnf ?? "");

View File

@ -9,16 +9,17 @@
type ToastStore, type ToastStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Button, Card, Dropdown } from "$lib/components"; import { Button, Card, Dropdown } from "$lib/components";
import type { Driver, Race, RaceResult, SkeletonData } from "$lib/schema"; import type { Driver, Race, RaceResult } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import { race_dropdown_options } from "$lib/dropdown"; import { race_dropdown_options } from "$lib/dropdown";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import type { PageData } from "../../../routes/data/raceresults/$types";
interface RaceResultCardProps { interface RaceResultCardProps {
/** Data passed from the page context */ /** Data passed from the page context */
data: SkeletonData & { results: RaceResult[] }; data: PageData;
/** The [RaceResult] object used to prefill values. */ /** The [RaceResult] object used to prefill values. */
result?: RaceResult; result?: RaceResult;

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Card, Button, Dropdown } from "$lib/components"; import { Card, Button, Dropdown } from "$lib/components";
import type { Driver, SkeletonData, Substitution } from "$lib/schema"; import type { Driver, Substitution } from "$lib/schema";
import { get_by_value, get_driver_headshot_template } from "$lib/database"; import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { import {
getModalStore, getModalStore,
@ -13,10 +13,11 @@
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import type { PageData } from "../../../routes/data/season/substitutions/$types";
interface SubstitutionCardProps { interface SubstitutionCardProps {
/** Data passed from the page context */ /** Data passed from the page context */
data: SkeletonData; data: PageData;
/** The [Substitution] object used to prefill values. */ /** The [Substitution] object used to prefill values. */
substitution?: Substitution; substitution?: Substitution;

View File

@ -8,16 +8,23 @@
type ToastStore, type ToastStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Card, Button, Input, LazyImage } from "$lib/components"; import { Card, Button, Input, LazyImage } from "$lib/components";
import type { SkeletonData, Team } from "$lib/schema"; import type { Team } from "$lib/schema";
import { TEAM_BANNER_HEIGHT, TEAM_BANNER_WIDTH } from "$lib/config"; import {
TEAM_BANNER_HEIGHT,
TEAM_BANNER_WIDTH,
TEAM_LOGO_HEIGHT,
TEAM_LOGO_WIDTH,
} from "$lib/config";
import { get_team_banner_template, get_team_logo_template } from "$lib/database"; import { get_team_banner_template, get_team_logo_template } from "$lib/database";
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { pb } from "$lib/pocketbase"; import { pb } from "$lib/pocketbase";
import { invalidateAll } from "$app/navigation"; import { invalidateAll } from "$app/navigation";
import { error } from "@sveltejs/kit";
import type { PageData } from "../../../routes/data/season/teams/$types";
interface TeamCardProps { interface TeamCardProps {
/** Data from the page context */ /** Data from the page context */
data: SkeletonData; data: PageData;
/** The [Team] object used to prefill values. */ /** The [Team] object used to prefill values. */
team?: Team; team?: Team;
@ -47,7 +54,6 @@
let logo_value: FileList | undefined = $state(); let logo_value: FileList | undefined = $state();
// Database actions // Database actions
// TODO: Banner + logo compression
const update_team = (create?: boolean): (() => Promise<void>) => { const update_team = (create?: boolean): (() => Promise<void>) => {
const handler = async (): Promise<void> => { const handler = async (): Promise<void> => {
if (!name_value || name_value === "") { if (!name_value || name_value === "") {
@ -59,20 +65,83 @@
return; return;
} }
const team_data = { // Banner handling
let banner_avif: Blob | undefined = undefined;
const banner_file: File | undefined =
banner_value && banner_value.length === 1 ? banner_value[0] : undefined;
if (banner_file) {
const banner_formdata: FormData = new FormData();
banner_formdata.append("image", banner_file);
banner_formdata.append("width", TEAM_BANNER_WIDTH.toString());
banner_formdata.append("height", TEAM_BANNER_HEIGHT.toString());
try {
const response = await fetch("/api/compress", {
method: "POST",
body: banner_formdata,
});
if (!response.ok) {
error(500, "Compression failed.");
}
banner_avif = await response.blob();
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
}
// Logo handling
let logo_avif: Blob | undefined = undefined;
const logo_file: File | undefined =
logo_value && logo_value.length === 1 ? logo_value[0] : undefined;
if (logo_file) {
const logo_formdata: FormData = new FormData();
logo_formdata.append("image", logo_file);
logo_formdata.append("width", TEAM_LOGO_WIDTH.toString());
logo_formdata.append("height", TEAM_LOGO_HEIGHT.toString());
try {
const response = await fetch("/api/compress", {
method: "POST",
body: logo_formdata,
});
if (!response.ok) {
error(500, "Compression failed.");
}
logo_avif = await response.blob();
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
}
let team_data = {
name: name_value, name: name_value,
color: color_value, color: color_value,
banner: banner_value && banner_value.length === 1 ? banner_value[0] : undefined, banner: banner_avif,
logo: logo_value && logo_value.length === 1 ? logo_value[0] : undefined, logo: logo_avif,
}; };
// HACK: Having only a single file for the update request
// doesn't work with pocketbase for some reason
if (team_data.banner === undefined) {
delete team_data.banner;
}
if (team_data.logo === undefined) {
delete team_data.logo;
}
try { try {
if (create) { if (create) {
if (!banner_value || banner_value.length !== 1) { if (!banner_avif) {
toastStore.trigger(get_error_toast("Please upload a single team banner!")); toastStore.trigger(get_error_toast("Please upload a single team banner!"));
return; return;
} }
if (!logo_value || logo_value.length !== 1) { if (!logo_avif) {
toastStore.trigger(get_error_toast("Please upload a single team logo!")); toastStore.trigger(get_error_toast("Please upload a single team logo!"));
return; return;
} }

172
src/lib/fetch.ts Normal file
View File

@ -0,0 +1,172 @@
import { pb } from "./pocketbase";
import type {
CurrentPickedUser,
Driver,
Graphic,
Race,
RacePick,
RaceResult,
Substitution,
Team,
User,
} from "./schema";
/**
* Fetch all [Graphics] from the database with file URLs
*/
export const fetch_graphics = async (fetch: (_: any) => Promise<Response>): Promise<Graphic[]> => {
const graphics: Graphic[] = await pb.collection("graphics").getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
/**
* Fetch all [Teams] (sorted ascending by name) from the database with file URLs for banners/logos
*/
export const fetch_teams = async (fetch: (_: any) => Promise<Response>): Promise<Team[]> => {
const teams: Team[] = await pb.collection("teams").getFullList({
sort: "+name",
fetch: fetch,
});
teams.map((team: Team) => {
team.banner_url = pb.files.getURL(team, team.banner);
team.logo_url = pb.files.getURL(team, team.logo);
});
return teams;
};
/**
* Fetch all [Drivers] (sorted ascending by code) from the database with file URLs for headshots
*/
export const fetch_drivers = async (fetch: (_: any) => Promise<Response>): Promise<Driver[]> => {
const drivers: Driver[] = await pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
/**
* Fetch all [Races] (sorted ascending by step) from the database with file URLs for pictograms
*/
export const fetch_races = async (fetch: (_: any) => Promise<Response>): Promise<Race[]> => {
const races: Race[] = await pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = pb.files.getURL(race, race.pictogram);
});
return races;
};
/**
* Fetch all [Substitutions] (sorted ascending by race step) from the database
*/
export const fetch_substitutions = async (
fetch: (_: any) => Promise<Response>,
): Promise<Substitution[]> => {
const substitutions: Substitution[] = await pb.collection("substitutions").getFullList({
expand: "race",
fetch: fetch,
});
// Sort by race step (ascending)
substitutions.sort((a: Substitution, b: Substitution) => a.expand.race.step - b.expand.race.step);
return substitutions;
};
/**
* Fetch all [RaceResults] (sorted descending by race step - newest first) from the database
*/
export const fetch_raceresults = async (
fetch: (_: any) => Promise<Response>,
): Promise<RaceResult[]> => {
const raceresults: RaceResult[] = await pb
.collection("raceresultsdesc")
.getFullList({ fetch: fetch });
return raceresults;
};
/**
* Fetch all [Users] (sorted ascending by username) with file URLs for avatars
*/
export const fetch_users = async (fetch: (_: any) => Promise<Response>): Promise<User[]> => {
const users: User[] = await pb
.collection("users")
.getFullList({ fetch: fetch, sort: "+username" });
users.map((user: User) => {
if (user.avatar) {
user.avatar_url = pb.files.getURL(user, user.avatar);
}
});
return users;
};
/**
* Fetch the first [Race] without result from the database with file URL for the pictogram
*/
export const fetch_currentrace = async (
fetch: (_: any) => Promise<Response>,
): Promise<Race | null> => {
const currentrace: Race[] = await pb.collection("currentrace").getFullList({ fetch: fetch });
// The currentrace collection either has a single or no entries
if (currentrace.length == 0) return null;
currentrace[0].pictogram_url = pb.files.getURL(currentrace[0], currentrace[0].pictogram);
return currentrace[0];
};
/**
* Fetch all [RacePicks] from the database
*/
export const fetch_racepicks = async (
fetch: (_: any) => Promise<Response>,
): Promise<RacePick[]> => {
// Don't expand race/pxx/dnf since we already fetched those
const racepicks: RacePick[] = await pb
.collection("racepicks")
.getFullList({ fetch: fetch, expand: "user" });
return racepicks;
};
/**
* Fetch all [Users] (with the extra field "picked" that is truthy
* if the user already picked for the current race)
* for the current race with file URLs for the avatars
*/
export const fetch_currentpickedusers = async (
fetch: (_: any) => Promise<Response>,
): Promise<CurrentPickedUser[]> => {
const currentpickedusers: CurrentPickedUser[] = await pb
.collection("currentpickedusers")
.getFullList({ fetch: fetch });
currentpickedusers.map((currentpickeduser: CurrentPickedUser) => {
if (currentpickeduser.avatar) {
currentpickeduser.avatar_url = pb.files.getURL(currentpickeduser, currentpickeduser.avatar);
}
});
return currentpickedusers;
};

View File

@ -1,6 +1,7 @@
import Pocketbase, { type AuthRecord } from "pocketbase"; import Pocketbase, { type AuthRecord, type RecordModel, type RecordSubscription } from "pocketbase";
import type { Graphic, User } from "$lib/schema"; import type { Graphic, User } from "$lib/schema";
import { env } from "$env/dynamic/public"; import { env } from "$env/dynamic/public";
import { invalidate } from "$app/navigation";
export let pb = new Pocketbase(env.PUBLIC_PBURL || "http://192.168.86.50:8090"); export let pb = new Pocketbase(env.PUBLIC_PBURL || "http://192.168.86.50:8090");
export let pbUser: User | undefined = undefined; export let pbUser: User | undefined = undefined;
@ -8,6 +9,7 @@ export let pbUser: User | undefined = undefined;
const update_user = async (record: AuthRecord): Promise<void> => { const update_user = async (record: AuthRecord): Promise<void> => {
if (!record) { if (!record) {
pbUser = undefined; pbUser = undefined;
console.log("Returning with pbUser = undefined");
return; return;
} }
@ -32,8 +34,25 @@ const update_user = async (record: AuthRecord): Promise<void> => {
}; };
// Update the pbUser object when authStore changes (e.g. after logging in) // Update the pbUser object when authStore changes (e.g. after logging in)
pb.authStore.onChange(() => { pb.authStore.onChange(async () => {
update_user(pb.authStore.record); await update_user(pb.authStore.record);
// console.log("Updating pbUser...")
// console.dir(pbUser, { depth: null }); // TODO: If the user has not chosen an avatar,
// the page keeps displaying the "Login" button (wtf)
console.log("Updating pbUser...");
console.dir(pbUser, { depth: null });
}, true); }, true);
export const subscribe = (collections: string[]) => {
collections.forEach((collection: string) => {
pb.collection(collection).subscribe("*", (event: RecordSubscription<RecordModel>) => {
invalidate(`data:${collection}`);
});
});
};
export const unsubscribe = (collections: string[]) => {
collections.forEach((collection: string) => {
pb.collection(collection).unsubscribe("*");
});
};

View File

@ -3,20 +3,6 @@
// Application Data // Application Data
/**
* The data returned from the root layout's [load]-function.
*/
export interface SkeletonData {
user: User;
admin: boolean;
graphics: Promise<Graphic[]>;
teams: Promise<Team[]>;
drivers: Promise<Driver[]>;
races: Promise<Race[]>;
substitutions: Promise<Substitution[]>;
}
export interface Graphic { export interface Graphic {
name: string; name: string;
file: string; file: string;
@ -27,7 +13,7 @@ export interface User {
id: string; id: string;
username: string; username: string;
firstname: string; firstname: string;
avatar: string; avatar?: string;
avatar_url?: string; avatar_url?: string;
admin: boolean; admin: boolean;
} }
@ -53,9 +39,6 @@ export interface Driver {
headshot_url?: string; headshot_url?: string;
team: string; team: string;
active: boolean; active: boolean;
expand: {
team: Team;
};
} }
export interface Race { export interface Race {

View File

@ -7,17 +7,21 @@ import sharp from "sharp";
*/ */
export const image_to_avif = async ( export const image_to_avif = async (
data: ArrayBuffer, data: ArrayBuffer,
width: number | undefined = undefined, width?: number,
height: number | undefined = undefined, height?: number,
quality: number = 50, quality: number = 50,
effort: number = 4, effort: number = 4,
): Promise<Blob> => { ): Promise<Blob> => {
console.log(
`Compressing ${data.byteLength} Bytes to ${width ?? -1}x${height ?? -1} avif with quality ${quality} and effort ${effort}...`,
);
const compressed: Buffer = await sharp(data) const compressed: Buffer = await sharp(data)
.resize(width, height) .resize(width, height)
.avif({ quality: quality, effort: effort }) .avif({ quality: quality, effort: effort })
.toBuffer(); .toBuffer();
console.log(`image_to_avif: ${data.byteLength} Bytes -> ${compressed.length} Bytes`); console.log(`Compressed ${data.byteLength} Bytes to ${compressed.length} Bytes`);
return new Blob([compressed]); return new Blob([compressed]);
}; };

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import "../app.css"; import "../app.css";
import type { Snippet } from "svelte"; import { onDestroy, onMount, type Snippet } from "svelte";
import type { LayoutData } from "./$types"; import type { LayoutData } from "./$types";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { import {
@ -19,7 +19,6 @@
RaceResultCard, RaceResultCard,
} from "$lib/components"; } from "$lib/components";
import { get_avatar_preview_event_handler } from "$lib/image"; import { get_avatar_preview_event_handler } from "$lib/image";
import { import {
AppBar, AppBar,
storePopup, storePopup,
@ -39,9 +38,11 @@
getToastStore, getToastStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom"; import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom";
import { invalidateAll } from "$app/navigation"; import { invalidate } from "$app/navigation";
import { get_error_toast } from "$lib/toast"; import { get_error_toast } from "$lib/toast";
import { pb } from "$lib/pocketbase"; import { pb, subscribe, unsubscribe } from "$lib/pocketbase";
import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config";
import { error } from "@sveltejs/kit";
let { data, children }: { data: LayoutData; children: Snippet } = $props(); let { data, children }: { data: LayoutData; children: Snippet } = $props();
@ -133,28 +134,26 @@
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow }); storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
// Reactive state // Reactive state
let username_value: string = $state(""); let username_value: string = $state(data.user?.username ?? "");
let firstname_value: string = $state(""); let firstname_value: string = $state(data.user?.firstname ?? "");
let password_value: string = $state(""); let password_value: string = $state("");
let avatar_value: FileList | undefined = $state(); let avatar_value: FileList | undefined = $state();
// Database actions // Database actions
// TODO: Avatar compression
const login = async (): Promise<void> => { const login = async (): Promise<void> => {
try { try {
await pb.collection("users").authWithPassword(username_value, password_value); await pb.collection("users").authWithPassword(username_value, password_value);
} catch (error) { } catch (error) {
toastStore.trigger(get_error_toast("" + error)); toastStore.trigger(get_error_toast("" + error));
} }
await invalidateAll(); await invalidate("data:user");
drawerStore.close(); drawerStore.close();
password_value = ""; password_value = "";
firstname_value = data.user?.firstname ?? "";
}; };
const logout = async (): Promise<void> => { const logout = async (): Promise<void> => {
pb.authStore.clear(); pb.authStore.clear();
await invalidateAll(); await invalidate("data:user");
drawerStore.close(); drawerStore.close();
username_value = ""; username_value = "";
firstname_value = ""; firstname_value = "";
@ -171,23 +170,50 @@
toastStore.trigger(get_error_toast("Please enter your first name!")); toastStore.trigger(get_error_toast("Please enter your first name!"));
return; return;
} }
if (!password_value || password_value === "") {
toastStore.trigger(get_error_toast("Please enter a password!")); // Avatar handling
return; let avatar_avif: Blob | undefined = undefined;
const avatar_file: File | undefined =
avatar_value && avatar_value.length === 1 ? avatar_value[0] : undefined;
if (avatar_file) {
const avatar_formdata: FormData = new FormData();
avatar_formdata.append("image", avatar_file);
avatar_formdata.append("width", AVATAR_WIDTH.toString());
avatar_formdata.append("height", AVATAR_HEIGHT.toString());
try {
const response = await fetch("/api/compress", {
method: "POST",
body: avatar_formdata,
});
if (!response.ok) {
error(500, "Compression failed.");
}
avatar_avif = await response.blob();
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
} }
try { try {
if (create) { if (create) {
if (!password_value || password_value === "") {
toastStore.trigger(get_error_toast("Please enter a password!"));
return;
}
await pb.collection("users").create({ await pb.collection("users").create({
username: username_value, username: username_value,
firstname: firstname_value, firstname: firstname_value,
password: password_value, password: password_value,
passwordConfirm: password_value, passwordConfirm: password_value, // lol
admin: false, admin: false,
}); });
await login(); await login();
return;
} else { } else {
if (!data.user?.id || data.user.id === "") { if (!data.user?.id || data.user.id === "") {
toastStore.trigger(get_error_toast("Invalid user id!")); toastStore.trigger(get_error_toast("Invalid user id!"));
@ -197,12 +223,12 @@
await pb.collection("users").update(data.user.id, { await pb.collection("users").update(data.user.id, {
username: username_value, username: username_value,
firstname: firstname_value, firstname: firstname_value,
avatar: avatar_value && avatar_value.length === 1 ? avatar_value[0] : undefined, avatar: avatar_avif,
}); });
invalidateAll();
drawerStore.close(); drawerStore.close();
} }
invalidate("data:users");
} catch (error) { } catch (error) {
toastStore.trigger(get_error_toast("" + error)); toastStore.trigger(get_error_toast("" + error));
} }
@ -210,6 +236,39 @@
return handler; return handler;
}; };
// Real-time updates without reloading
onMount(() =>
subscribe([
"users",
"drivers",
"racepicks",
"raceresults",
"races",
"seasonpicks",
"substitutions",
"teams",
"currentpickedusers",
"currentrace",
"raceresultdesc",
]),
);
onDestroy(() =>
unsubscribe([
"users",
"drivers",
"racepicks",
"raceresults",
"races",
"seasonpicks",
"substitutions",
"teams",
"currentpickedusers",
"currentrace",
"raceresultdesc",
]),
);
</script> </script>
<LoadingIndicator /> <LoadingIndicator />

View File

@ -1,91 +1,24 @@
import { pb, pbUser } from "$lib/pocketbase"; import { fetch_graphics } from "$lib/fetch";
import { pbUser } from "$lib/pocketbase";
import type { LayoutLoad } from "./$types";
// This makes the page client-side rendered // This makes the page client-side rendered
export const ssr = false; export const ssr = false;
import type { Driver, Graphic, Race, Substitution, Team } from "$lib/schema";
import type { LayoutLoad } from "./$types";
// On each page load (every route), this function runs serverside. // On each page load (every route), this function runs serverside.
// The "locals.user" object is only available on the server, // The "locals.user" object is only available on the server,
// since it's populated inside hooks.server.ts per request. // since it's populated inside hooks.server.ts per request.
// It will populate the "user" attribute of each page's "data" object, // It will populate the "user" attribute of each page's "data" object,
// so each page has access to the current user (or knows if no one is signed in). // so each page has access to the current user (or knows if no one is signed in).
export const load: LayoutLoad = () => { export const load: LayoutLoad = async ({ fetch, depends }) => {
const fetch_graphics = async (): Promise<Graphic[]> => { depends("data:graphics", "data:user");
const graphics: Graphic[] = await pb.collection("graphics").getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
const fetch_teams = async (): Promise<Team[]> => {
const teams: Team[] = await pb.collection("teams").getFullList({
sort: "+name",
fetch: fetch,
});
teams.map((team: Team) => {
team.banner_url = pb.files.getURL(team, team.banner);
team.logo_url = pb.files.getURL(team, team.logo);
});
return teams;
};
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
const fetch_races = async (): Promise<Race[]> => {
const races: Race[] = await pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = pb.files.getURL(race, race.pictogram);
});
return races;
};
const fetch_substitutions = async (): Promise<Substitution[]> => {
const substitutions: Substitution[] = await pb.collection("substitutions").getFullList({
expand: "race",
fetch: fetch,
});
// Sort by race step (ascending)
substitutions.sort(
(a: Substitution, b: Substitution) => a.expand.race.step - b.expand.race.step,
);
return substitutions;
};
return { return {
// User information // User information (synchronous)
user: pbUser, user: pbUser,
admin: pbUser?.admin ?? false, admin: pbUser?.admin ?? false,
// Return static data asynchronously // Return static data
graphics: fetch_graphics(), graphics: await fetch_graphics(fetch),
teams: fetch_teams(),
drivers: fetch_drivers(),
races: fetch_races(),
substitutions: fetch_substitutions(),
}; };
}; };

View File

@ -0,0 +1,45 @@
import { image_to_avif } from "$lib/server/image";
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
/**
* This route is available at /api/compress.
* It will return the image as a compressed [avif] [Blob].
* We need this because [sharp] is a server-side node library.
*/
export const POST: RequestHandler = async ({ request }) => {
console.log("Hit /api/compress...");
const data: FormData = await request.formData();
const image: File | undefined = data.has("image") ? (data.get("image") as File) : undefined;
const width: number | undefined = data.has("width")
? parseInt(data.get("width")?.toString() ?? "-1")
: undefined;
const height: number | undefined = data.has("height")
? parseInt(data.get("height")?.toString() ?? "-1")
: undefined;
const quality: number | undefined = data.has("quality")
? parseInt(data.get("quality")?.toString() ?? "-1")
: undefined;
const effort: number | undefined = data.has("effort")
? parseInt(data.get("effort")?.toString() ?? "-1")
: undefined;
if (!image) {
error(500, "Can't compress image without image data");
}
const compressedImage: Blob = await image_to_avif(
await image.arrayBuffer(),
width,
height,
quality,
effort,
);
return new Response(compressedImage, {
headers: {
"Content-Type": "image/avif",
},
});
};

View File

@ -11,7 +11,11 @@
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const result_handler = async (event: Event, id?: string) => { const result_handler = async (event: Event, id?: string) => {
const result: RaceResult | undefined = get_by_value(data.results, "id", id ?? "Invalid"); const result: RaceResult | undefined = get_by_value(
await data.raceresults,
"id",
id ?? "Invalid",
);
if (id && !result) return; if (id && !result) return;
@ -90,4 +94,6 @@
<span class="font-bold">Create Race Result</span> <span class="font-bold">Create Race Result</span>
</Button> </Button>
</div> </div>
<Table data={data.results} columns={results_columns} handler={result_handler} /> {#await data.raceresults then results}
<Table data={results} columns={results_columns} handler={result_handler} />
{/await}

View File

@ -1,18 +1,12 @@
import { pb } from "$lib/pocketbase"; import { fetch_drivers, fetch_raceresults, fetch_races } from "$lib/fetch";
import type { RaceResult } from "$lib/schema";
import type { PageLoad } from "../../$types"; import type { PageLoad } from "../../$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch, depends }) => {
// TODO: Duplicated code from racepicks/+page.server.ts depends("data:drivers", "data:races", "data:raceresults");
const fetch_raceresults = async (): Promise<RaceResult[]> => {
const raceresults: RaceResult[] = await pb
.collection("raceresultsdesc")
.getFullList({ fetch: fetch });
return raceresults;
};
return { return {
results: await fetch_raceresults(), drivers: fetch_drivers(fetch),
races: fetch_races(fetch),
raceresults: fetch_raceresults(fetch),
}; };
}; };

View File

@ -0,0 +1,11 @@
import { fetch_drivers, fetch_teams } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:teams", "data:drivers");
return {
teams: fetch_teams(fetch),
drivers: fetch_drivers(fetch),
};
};

View File

@ -0,0 +1,10 @@
import { fetch_races } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:races");
return {
races: fetch_races(fetch),
};
};

View File

@ -0,0 +1,12 @@
import { fetch_drivers, fetch_races, fetch_substitutions } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:races", "data:drivers", "data:substitutions");
return {
races: fetch_races(fetch),
drivers: fetch_drivers(fetch),
substitutions: fetch_substitutions(fetch),
};
};

View File

@ -0,0 +1,10 @@
import { fetch_teams } from "$lib/fetch";
import type { PageLoad } from "../../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:teams");
return {
teams: fetch_teams(fetch),
};
};

View File

@ -1,21 +1,10 @@
import { pb } from "$lib/pocketbase"; import { fetch_users } from "$lib/fetch";
import type { User } from "$lib/schema";
import type { PageLoad } from "../../$types"; import type { PageLoad } from "../../$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch, depends }) => {
const fetch_users = async (): Promise<User[]> => { depends("data:users");
const users: User[] = await pb
.collection("users")
.getFullList({ fetch: fetch, sort: "+username" });
users.map((user: User) => {
user.avatar_url = pb.files.getURL(user, user.avatar);
});
return users;
};
return { return {
users: await fetch_users(), users: await fetch_users(fetch),
}; };
}; };

View File

@ -170,7 +170,7 @@
<h1 class="text-nowrap font-bold"> <h1 class="text-nowrap font-bold">
Picked ({picked.length}/{currentpicked.length}): Picked ({picked.length}/{currentpicked.length}):
</h1> </h1>
<div class="mt-1 grid grid-cols-4 gap-x-2 gap-y-0.5"> <div class="mt-1 grid grid-cols-4 gap-x-0 gap-y-0.5">
{#each picked.slice(0, 16) as user} {#each picked.slice(0, 16) as user}
<LazyImage <LazyImage
src={user.avatar_url ?? get_driver_headshot_template(graphics)} src={user.avatar_url ?? get_driver_headshot_template(graphics)}
@ -303,14 +303,15 @@
{/await} {/await}
</div> </div>
<div class="hide-scrollbar flex w-full overflow-x-scroll pb-2"> <!-- TODO: Horizontal scrollbar missing in desktop chrome (fuck chrome)??? -->
<div class="flex w-full overflow-x-scroll pb-2">
<!-- Not ideal but currentpickedusers contains all users, so we do not need to fetch the users separately --> <!-- Not ideal but currentpickedusers contains all users, so we do not need to fetch the users separately -->
{#await Promise.all( [data.currentpickedusers, data.racepicks, data.races, data.drivers, data.raceresults], ) then [currentpicked, racepicks, races, drivers, raceresults]} {#await Promise.all( [data.currentpickedusers, data.racepicks, data.races, data.drivers, data.raceresults], ) then [currentpicked, racepicks, races, drivers, raceresults]}
{#each currentpicked as user} {#each currentpicked as user}
{@const picks = racepicks.filter((pick: RacePick) => pick.user === user.id)} {@const picks = racepicks.filter((pick: RacePick) => pick.user === user.id)}
<div <div
class="card ml-1 mt-2 w-full min-w-12 overflow-hidden py-2 shadow lg:ml-2 {data.user && class="card ml-1 mt-2 w-full min-w-12 overflow-hidden py-2 shadow lg:ml-2 lg:min-w-40 {data.user &&
data.user.username === user.username data.user.username === user.username
? 'bg-primary-300' ? 'bg-primary-300'
: ''}" : ''}"

View File

@ -1,57 +1,32 @@
import { pb } from "$lib/pocketbase"; import {
import type { CurrentPickedUser, Race, RacePick, RaceResult } from "$lib/schema"; fetch_currentpickedusers,
fetch_currentrace,
fetch_drivers,
fetch_racepicks,
fetch_raceresults,
fetch_races,
fetch_substitutions,
} from "$lib/fetch";
import type { PageLoad } from "../$types"; import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch, depends }) => {
const fetch_currentrace = async (): Promise<Race | null> => { depends(
const currentrace: Race[] = await pb.collection("currentrace").getFullList({ fetch: fetch }); "data:racepicks",
"data:currentpickedusers",
// The currentrace collection either has a single or no entries "data:raceresults",
if (currentrace.length == 0) return null; "data:drivers",
"data:races",
currentrace[0].pictogram_url = pb.files.getURL(currentrace[0], currentrace[0].pictogram); "data:currentrace",
);
return currentrace[0];
};
const fetch_racepicks = async (): Promise<RacePick[]> => {
// Don't expand race/pxx/dnf since we already fetched those
const racepicks: RacePick[] = await pb
.collection("racepicks")
.getFullList({ fetch: fetch, expand: "user" });
return racepicks;
};
const fetch_currentpickedusers = async (): Promise<CurrentPickedUser[]> => {
const currentpickedusers: CurrentPickedUser[] = await pb
.collection("currentpickedusers")
.getFullList({ fetch: fetch });
currentpickedusers.map((currentpickeduser: CurrentPickedUser) => {
if (currentpickeduser.avatar) {
currentpickeduser.avatar_url = pb.files.getURL(currentpickeduser, currentpickeduser.avatar);
}
});
return currentpickedusers;
};
// TODO: Duplicated code from data/raceresults/+page.server.ts
const fetch_raceresults = async (): Promise<RaceResult[]> => {
// Don't expand races/pxxs/dnfs since we already fetched those
const raceresults: RaceResult[] = await pb
.collection("raceresultsdesc")
.getFullList({ fetch: fetch });
return raceresults;
};
return { return {
racepicks: fetch_racepicks(), racepicks: fetch_racepicks(fetch),
currentpickedusers: fetch_currentpickedusers(), currentpickedusers: fetch_currentpickedusers(fetch),
raceresults: fetch_raceresults(), raceresults: fetch_raceresults(fetch),
drivers: fetch_drivers(fetch),
races: fetch_races(fetch),
substitutions: fetch_substitutions(fetch),
currentrace: await fetch_currentrace(), currentrace: await fetch_currentrace(fetch),
}; };
}; };