Compare commits

...

9 Commits

11 changed files with 601 additions and 86 deletions

469
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.12", "@floating-ui/dom": "^1.6.12",
"pocketbase": "^0.22.1", "pocketbase": "^0.22.1",
"sharp": "^0.33.5",
"uuid": "^11.0.3" "uuid": "^11.0.3"
}, },
"devDependencies": { "devDependencies": {
@ -115,6 +116,16 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.24.0", "version": "0.24.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz",
@ -566,6 +577,367 @@
"@typescript-eslint/typescript-estree": "^8.14.0" "@typescript-eslint/typescript-estree": "^8.14.0"
} }
}, },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1516,11 +1888,23 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@ -1533,9 +1917,18 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -1662,6 +2055,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz",
@ -2008,6 +2410,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -3422,9 +3830,7 @@
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC", "license": "ISC",
"optional": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@ -3439,6 +3845,45 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3475,6 +3920,15 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
@ -3885,6 +4339,13 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.2", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",

View File

@ -33,6 +33,7 @@
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.12", "@floating-ui/dom": "^1.6.12",
"pocketbase": "^0.22.1", "pocketbase": "^0.22.1",
"sharp": "^0.33.5",
"uuid": "^11.0.3" "uuid": "^11.0.3"
} }
} }

3
src/app.d.ts vendored
View File

@ -1,3 +1,4 @@
import type { User } from "$lib/schema";
import type { PocketBase, RecordModel } from "pocketbase"; import type { PocketBase, RecordModel } from "pocketbase";
// See https://svelte.dev/docs/kit/types#app.d.ts // See https://svelte.dev/docs/kit/types#app.d.ts
@ -6,7 +7,7 @@ declare global {
namespace App { namespace App {
interface Locals { interface Locals {
pb: PocketBase; pb: PocketBase;
user: RecordModel | undefined; user: User | undefined;
admin: boolean; admin: boolean;
} }

View File

@ -1,3 +1,4 @@
import type { Graphic, User } from "$lib/schema";
import type { Handle } from "@sveltejs/kit"; import type { Handle } from "@sveltejs/kit";
import PocketBase from "pocketbase"; import PocketBase from "pocketbase";
@ -14,14 +15,25 @@ export const handle: Handle = async ({ event, resolve }) => {
if (event.locals.pb.authStore.isValid) { if (event.locals.pb.authStore.isValid) {
// If the authentication data is valid, we make a "user" object easily available. // If the authentication data is valid, we make a "user" object easily available.
event.locals.user = structuredClone(event.locals.pb.authStore.model); event.locals.user = structuredClone(event.locals.pb.authStore.model) as User;
if (event.locals.user) { if (event.locals.user) {
if (event.locals.pb.authStore.model.avatar) {
// Fill in the avatar URL // Fill in the avatar URL
event.locals.user.avatar_url = event.locals.pb.files.getURL( event.locals.user.avatar_url = event.locals.pb.files.getURL(
event.locals.pb.authStore.model, event.locals.pb.authStore.model,
event.locals.pb.authStore.model.avatar, event.locals.pb.authStore.model.avatar,
); );
} else {
// Fill in the driver_template URL if no avatar chosen
const driver_template: Graphic = await event.locals.pb
.collection("graphics")
.getFirstListItem('name="driver_template"');
event.locals.user.avatar_url = event.locals.pb.files.getURL(
driver_template,
driver_template.file,
);
}
// Set admin status for easier access // Set admin status for easier access
event.locals.admin = event.locals.user.admin; event.locals.admin = event.locals.user.admin;

View File

@ -69,6 +69,7 @@
<input <input
use:popup={popup_settings} use:popup={popup_settings}
type="text" type="text"
readonly
value={get_label(input_variable) ?? placeholder} value={get_label(input_variable) ?? placeholder}
{...restProps} {...restProps}
/> />

View File

@ -1,7 +1,11 @@
/** /**
* Retrieve an arbitrary object with a matching ID from an Array. * Select an element from an [objects] array where [key] matches [value].
* Supposed to be used on collections returned by the PocketBase API. * Supposed to be used on collections returned by the [PocketBase] client.
*/ */
export const get_by_id = <T extends object>(objects: T[], id: string): T | undefined => { export const get_by_value = <T extends object>(
return objects.find((o: T) => ("id" in o ? o.id === id : false)); objects: T[],
key: keyof T,
value: string,
): T | undefined => {
return objects.find((o: T) => (key in o ? o[key] === value : false));
}; };

View File

@ -1,3 +1,17 @@
export interface Graphic {
name: string;
file: string;
file_url?: string;
}
export interface User {
id: string;
username: string;
avatar: string;
avatar_url?: string;
admin: boolean;
}
export interface Team { export interface Team {
id: string; id: string;
name: string; name: string;

View File

@ -118,6 +118,8 @@
<div class="flex flex-col gap-2 p-2"> <div class="flex flex-col gap-2 p-2">
<h4 class="h4 select-none">Enter Username and Password</h4> <h4 class="h4 select-none">Enter Username and Password</h4>
<form method="POST" class="contents"> <form method="POST" class="contents">
<!-- Supply the pathname so the form can redirect to the current page. -->
<input type="hidden" name="redirect_url" value={$page.url.pathname} />
<Input name="username" placeholder="Username" required> <Input name="username" placeholder="Username" required>
<UserIcon /> <UserIcon />
</Input> </Input>
@ -144,6 +146,8 @@
<div class="flex flex-col gap-2 p-2"> <div class="flex flex-col gap-2 p-2">
<h4 class="h4 select-none">Edit Profile</h4> <h4 class="h4 select-none">Edit Profile</h4>
<form method="POST" enctype="multipart/form-data" class="contents"> <form method="POST" enctype="multipart/form-data" class="contents">
<!-- Supply the pathname so the form can redirect to the current page. -->
<input type="hidden" name="redirect_url" value={$page.url.pathname} />
<input type="hidden" name="id" value={data.user.id} /> <input type="hidden" name="id" value={data.user.id} />
<Input name="username" value={data.user.username} placeholder="Username"><UserIcon /></Input <Input name="username" value={data.user.username} placeholder="Username"><UserIcon /></Input
> >
@ -179,6 +183,7 @@
padding="p-2" padding="p-2"
> >
<svelte:fragment slot="lead"> <svelte:fragment slot="lead">
<div class="flex gap-2">
<!-- Navigation drawer --> <!-- Navigation drawer -->
<div class="lg:hidden"> <div class="lg:hidden">
<Button color="primary" onclick={menu_drawer}> <Button color="primary" onclick={menu_drawer}>
@ -187,7 +192,6 @@
</div> </div>
<!-- Site logo --> <!-- Site logo -->
<div class="ml-2 lg:ml-0">
<Button href="/" color="primary"><span class="text-xl font-bold">Formula 11</span></Button> <Button href="/" color="primary"><span class="text-xl font-bold">Formula 11</span></Button>
</div> </div>
</svelte:fragment> </svelte:fragment>
@ -202,6 +206,7 @@
</div> </div>
<svelte:fragment slot="trail"> <svelte:fragment slot="trail">
<div class="flex gap-2">
<!-- Data drawer --> <!-- Data drawer -->
<Button <Button
color="primary" color="primary"
@ -209,19 +214,22 @@
activate={$page.url.pathname.startsWith("/data")}>Data</Button activate={$page.url.pathname.startsWith("/data")}>Data</Button
> >
<!-- Login/Profile drawer -->
{#if !data.user} {#if !data.user}
<!-- Login drawer -->
<Button color="primary" onclick={login_drawer}>Login</Button> <Button color="primary" onclick={login_drawer}>Login</Button>
{:else} {:else}
<!-- Profile drawer -->
<Avatar <Avatar
id="user_avatar_preview" id="user_avatar_preview"
src={data.user.avatar_url} src={data.user.avatar_url}
rounded="rounded-full" rounded="rounded-full"
width="w-10" width="w-10"
background="bg-primary-50"
onclick={profile_drawer} onclick={profile_drawer}
cursor="cursor-pointer" cursor="cursor-pointer"
/> />
{/if} {/if}
</div>
</svelte:fragment> </svelte:fragment>
</AppBar> </AppBar>
</nav> </nav>

View File

@ -1,6 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form";
import type { Team, Driver, Race, Substitution } from "$lib/schema"; import type { Team, Driver, Race, Substitution, Graphic } from "$lib/schema";
// These "actions" run serverside only, as they're located inside +page.server.ts // These "actions" run serverside only, as they're located inside +page.server.ts
export const actions = { export const actions = {
@ -95,6 +95,18 @@ export const actions = {
// This "load" function runs serverside only, as it's located inside +page.server.ts // This "load" function runs serverside only, as it's located inside +page.server.ts
export const load: PageServerLoad = async ({ fetch, locals }) => { export const load: PageServerLoad = async ({ fetch, locals }) => {
const fetch_graphics = async (): Promise<Graphic[]> => {
const graphics: Graphic[] = await locals.pb
.collection("graphics")
.getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
const fetch_teams = async (): Promise<Team[]> => { const fetch_teams = async (): Promise<Team[]> => {
const teams: Team[] = await locals.pb.collection("teams").getFullList({ const teams: Team[] = await locals.pb.collection("teams").getFullList({
sort: "+name", sort: "+name",
@ -130,6 +142,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
}; };
return { return {
graphics: await fetch_graphics(),
teams: await fetch_teams(), teams: await fetch_teams(),
drivers: await fetch_drivers(), drivers: await fetch_drivers(),
races: await fetch_races(), races: await fetch_races(),

View File

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { Input, Button, Card, Search, Dropdown } from "$lib/components"; import { Input, Button, Card, Search, Dropdown } from "$lib/components";
import { get_image_preview_event_handler } from "$lib/image"; import { get_image_preview_event_handler } from "$lib/image";
import { get_by_id } from "$lib/database";
import type { Driver, Team } from "$lib/schema"; import type { Driver, Team } from "$lib/schema";
import { type PageData, type ActionData } from "./$types"; import { type PageData, type ActionData } from "./$types";
import { FileDropzone, Tab, TabGroup, type AutocompleteOption } from "@skeletonlabs/skeleton"; import { FileDropzone, Tab, TabGroup, type AutocompleteOption } from "@skeletonlabs/skeleton";
// TODO: Why does this work but import { type DropdownOption } from "$lib/components" does not? // TODO: Why does this work but import { type DropdownOption } from "$lib/components" does not?
import type { DropdownOption } from "$lib/components/Dropdown.svelte"; import type { DropdownOption } from "$lib/components/Dropdown.svelte";
import { get_by_value } from "$lib/database";
let { data, form }: { data: PageData; form: ActionData } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
@ -96,11 +96,12 @@
<!-- Add a new team --> <!-- Add a new team -->
{#if data.admin} {#if data.admin}
<Card imgsrc="" imgid="create_team_logo_preview" imghidden> <Card
imgsrc={get_by_value(data.graphics, "name", "team_template")?.file_url}
imgid="create_team_logo_preview"
>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h4 class="h4 select-none">Add a New Team</h4>
<!-- Team name input --> <!-- Team name input -->
<Input id="team_name_create" name="name" required>Name</Input> <Input id="team_name_create" name="name" required>Name</Input>
@ -122,7 +123,7 @@
<!-- By specifying the formaction on the button (instead of action on the form), --> <!-- By specifying the formaction on the button (instead of action on the form), -->
<!-- we can have multiple buttons with different actions in a single form. --> <!-- we can have multiple buttons with different actions in a single form. -->
<Button formaction="?/create_team" color="secondary" submit>Create</Button> <Button formaction="?/create_team" color="tertiary" submit>Create Team</Button>
</div> </div>
</div> </div>
</form> </form>
@ -172,6 +173,7 @@
name="team" name="team"
input_variable={update_driver_team_select_values[driver.id]} input_variable={update_driver_team_select_values[driver.id]}
labelwidth="120px" labelwidth="120px"
disabled={!data.admin}
options={driver_team_select_options}>Team</Dropdown options={driver_team_select_options}>Team</Dropdown
> >
@ -208,31 +210,23 @@
<!-- Add a new driver --> <!-- Add a new driver -->
{#if data.admin} {#if data.admin}
<Card imgsrc="" imgid="create_driver_headshot_preview" imghidden> <Card
imgsrc={get_by_value(data.graphics, "name", "driver_template")?.file_url}
imgid="create_driver_headshot_preview"
>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<h4 class="h4 select-none">Add a New Driver</h4>
<!-- Driver data input --> <!-- Driver data input -->
<Input <Input id="driver_first_name_create" name="firstname" labelwidth="120px" required
id="driver_first_name_create" >First Name</Input
name="firstname"
labelwidth="120px"
disabled={!data.admin}
required>First Name</Input
> >
<Input <Input id="driver_last_name_create" name="lastname" labelwidth="120px" required
id="driver_last_name_create" >Last Name</Input
name="lastname"
labelwidth="120px"
disabled={!data.admin}
required>Last Name</Input
> >
<Input <Input
id="driver_code_create" id="driver_code_create"
name="code" name="code"
labelwidth="120px" labelwidth="120px"
disabled={!data.admin}
maxlength={3} maxlength={3}
minlength={3} minlength={3}
required>Driver Code</Input required>Driver Code</Input
@ -252,7 +246,6 @@
name="headshot" name="headshot"
id="driver_headshot_create" id="driver_headshot_create"
onchange={get_image_preview_event_handler("create_driver_headshot_preview")} onchange={get_image_preview_event_handler("create_driver_headshot_preview")}
disabled={!data.admin}
required required
> >
<svelte:fragment slot="message" <svelte:fragment slot="message"
@ -262,7 +255,9 @@
<!-- Buttons --> <!-- Buttons -->
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button formaction="?/create_driver" color="secondary" submit>Create</Button> <Button formaction="?/create_driver" color="secondary" submit
>Create Driver</Button
>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,13 +1,14 @@
import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form";
import { error, redirect } from "@sveltejs/kit"; import { error, redirect } from "@sveltejs/kit";
import type { Actions } from "./$types"; import type { Actions } from "./$types";
import type { User } from "$lib/schema";
export const actions = { export const actions = {
create_profile: async ({ request, locals }) => { create_profile: async ({ request, locals }): Promise<void> => {
const data = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["username", "password"]); form_data_ensure_keys(data, ["username", "password", "redirect_url"]);
// TODO: Errrr passwordConfirm... How to integrate it into the unified login-/register-UI? // Confirm password lol
const record = await locals.pb.collection("users").create({ const record = await locals.pb.collection("users").create({
username: data.get("username")?.toString(), username: data.get("username")?.toString(),
password: data.get("password")?.toString(), password: data.get("password")?.toString(),
@ -20,46 +21,50 @@ export const actions = {
.collection("users") .collection("users")
.authWithPassword(data.get("username")?.toString(), data.get("password")?.toString()); .authWithPassword(data.get("username")?.toString(), data.get("password")?.toString());
redirect(303, "/"); redirect(303, data.get("redirect_url")?.toString() ?? "/");
}, },
// TODO: PocketBase API rule: Only the active user should be able to modify itself // TODO: PocketBase API rule: Only the active user should be able to modify itself
update_profile: async ({ request, locals }) => { update_profile: async ({ request, locals }): Promise<void> => {
const data = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
const id = form_data_get_and_remove_id(data); form_data_ensure_keys(data, ["redirect_url"]);
const id: string = form_data_get_and_remove_id(data);
const record = await locals.pb.collection("users").update(id, data); const record: User = await locals.pb.collection("users").update(id, data);
redirect(303, "/"); redirect(303, data.get("redirect_url")?.toString() ?? "/");
}, },
login: async ({ request, locals }) => { login: async ({ request, locals }) => {
if (locals.user) { if (locals.user) {
console.log("Already logged in!"); error(400, "Already logged in!");
return;
} }
const data = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["username", "password"]); form_data_ensure_keys(data, ["username", "password", "redirect_url"]);
try { try {
await locals.pb await locals.pb
.collection("users") .collection("users")
.authWithPassword(data.get("username")?.toString(), data.get("password")?.toString()); .authWithPassword(data.get("username")?.toString(), data.get("password")?.toString());
} catch (err) { } catch (err) {
console.log(`Failed to login: ${err}`);
error(400, "Failed to login!"); error(400, "Failed to login!");
} }
// TODO: Would be better to redirect to previous page somehow... redirect(303, data.get("redirect_url")?.toString() ?? "/");
redirect(303, "/");
}, },
logout: async ({ locals }) => { logout: async ({ request, locals }) => {
if (!locals.user) {
error(400, "Not logged in!");
}
const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["redirect_url"]);
locals.pb.authStore.clear(); locals.pb.authStore.clear();
locals.user = undefined; locals.user = undefined;
// TODO: Would be better to redirect to previous page somehow... redirect(303, data.get("redirect_url")?.toString() ?? "/");
redirect(303, "/");
}, },
} satisfies Actions; } satisfies Actions;