Lots of dev
This commit is contained in:
parent
2c2b08aa1a
commit
4ea6549ac7
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
uploads
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@ -24,7 +24,8 @@ export default defineConfig(
|
|||||||
rules: {
|
rules: {
|
||||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||||
'no-undef': 'off'
|
'no-undef': 'off',
|
||||||
|
'svelte/no-navigation-without-resolve': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
640
package-lock.json
generated
640
package-lock.json
generated
@ -13,7 +13,10 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"nodemailer": "^7.0.13",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"svelte-preprocess": "^6.0.3"
|
"svelte-preprocess": "^6.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -26,7 +29,10 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
|
"@types/nodemailer": "^7.0.9",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^3.2.4",
|
||||||
"bits-ui": "^2.15.4",
|
"bits-ui": "^2.15.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -86,6 +92,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
|
||||||
@ -786,6 +802,471 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/colour": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
|
"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.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
|
"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.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"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-ppc64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-riscv64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"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-riscv64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
|
"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.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"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-ia32": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||||
|
"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.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
|
"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/@internationalized/date": {
|
"node_modules/@internationalized/date": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
|
||||||
@ -1768,6 +2249,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
||||||
@ -1805,6 +2296,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.18.10",
|
"version": "22.18.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz",
|
||||||
@ -1815,6 +2324,16 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "7.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz",
|
||||||
|
"integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@ -2768,7 +3287,6 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -3660,7 +4178,6 @@
|
|||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||||
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
@ -4370,6 +4887,69 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mode-watcher": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"runed": "^0.25.0",
|
||||||
|
"svelte-toolbelt": "^0.7.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mode-watcher/node_modules/runed": {
|
||||||
|
"version": "0.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz",
|
||||||
|
"integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mode-watcher/node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.23.2",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@ -4451,6 +5031,15 @@
|
|||||||
"node-gyp-build-test": "build-test.js"
|
"node-gyp-build-test": "build-test.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "7.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||||
|
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@ -5326,6 +5915,50 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@img/colour": "^1.0.0",
|
||||||
|
"detect-libc": "^2.1.2",
|
||||||
|
"semver": "^7.7.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.5",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.5",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||||
|
"@img/sharp-linux-arm": "0.34.5",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linux-ppc64": "0.34.5",
|
||||||
|
"@img/sharp-linux-riscv64": "0.34.5",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.5",
|
||||||
|
"@img/sharp-linux-x64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||||
|
"@img/sharp-wasm32": "0.34.5",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.5",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.5",
|
||||||
|
"@img/sharp-win32-x64": "0.34.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",
|
||||||
@ -5513,7 +6146,6 @@
|
|||||||
"version": "1.0.14",
|
"version": "1.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||||
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inline-style-parser": "0.2.7"
|
"inline-style-parser": "0.2.7"
|
||||||
@ -5903,7 +6535,7 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tw-animate-css": {
|
"node_modules/tw-animate-css": {
|
||||||
|
|||||||
@ -25,7 +25,10 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
|
"@types/nodemailer": "^7.0.9",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^3.2.4",
|
||||||
"bits-ui": "^2.15.4",
|
"bits-ui": "^2.15.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -55,7 +58,10 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mode-watcher": "^1.1.0",
|
||||||
|
"nodemailer": "^7.0.13",
|
||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"svelte-preprocess": "^6.0.3"
|
"svelte-preprocess": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/app.d.ts
vendored
4
src/app.d.ts
vendored
@ -3,7 +3,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
|
user: UserPayload | null;
|
||||||
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
|||||||
25
src/hooks.server.ts
Normal file
25
src/hooks.server.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({ path: '.env' });
|
||||||
|
|
||||||
|
export const handle = async ({ event, resolve }) => {
|
||||||
|
const JWT = event.cookies.get('jwt');
|
||||||
|
|
||||||
|
if (process.env.JWT_SECRET === undefined) {
|
||||||
|
throw new Error('JWT_SECRET not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!JWT) {
|
||||||
|
event.locals.user = null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
event.locals.user = jwt.verify(JWT, process.env.JWT_SECRET);
|
||||||
|
} catch {
|
||||||
|
event.cookies.delete('jwt', { path: '/' });
|
||||||
|
event.locals.user = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await resolve(event);
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import type { User } from '$lib/types';
|
import type { User } from '$lib/types/user';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import sql from '$lib/db/db.server';
|
import sql from '$lib/db/db.server';
|
||||||
import type { Cookies } from '@sveltejs/kit';
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
@ -49,22 +49,22 @@ export function setJWT(cookies: Cookies, user: User) {
|
|||||||
|
|
||||||
export async function login(email: string, password: string): Promise<User> {
|
export async function login(email: string, password: string): Promise<User> {
|
||||||
try {
|
try {
|
||||||
const [user] = await sql`
|
const [user]: User[] = await sql`
|
||||||
SELECT id, email, password_hash, perms, name
|
SELECT * FROM users
|
||||||
WHERE email = ${email};
|
WHERE email = ${email};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (await bcrypt.compare(password, user.password_hash)) {
|
if (await bcrypt.compare(password, user.password_hash!)) {
|
||||||
delete user.password_hash;
|
delete user.password_hash;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
sql`
|
sql`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET last_signin = NOW()
|
SET last_sign_in = NOW()
|
||||||
WHERE id = ${user.id};
|
WHERE id = ${user.id};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return <User>user;
|
return user;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
throw Error('Error signing in ');
|
throw Error('Error signing in ');
|
||||||
|
|||||||
166
src/lib/components/custom/image-upload/image-upload.svelte
Normal file
166
src/lib/components/custom/image-upload/image-upload.svelte
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ImagePlus from '@lucide/svelte/icons/image-plus';
|
||||||
|
import X from '@lucide/svelte/icons/x';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
export let name = 'image';
|
||||||
|
export let required = false;
|
||||||
|
export let disabled = false;
|
||||||
|
export let onSelect: ((file: File) => void) | null = null;
|
||||||
|
|
||||||
|
|
||||||
|
let inputEl: HTMLInputElement | null = null;
|
||||||
|
let previewUrl: string | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
|
||||||
|
function openFileDialog() {
|
||||||
|
if (!disabled) {
|
||||||
|
inputEl?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFiles(files: FileList | null) {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const selected = files[0];
|
||||||
|
if (!selected.type.startsWith('image/')) return;
|
||||||
|
|
||||||
|
cleanupPreview();
|
||||||
|
previewUrl = URL.createObjectURL(selected);
|
||||||
|
|
||||||
|
// Trigger callback
|
||||||
|
if (onSelect) onSelect(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInputChange(e: Event) {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
handleFiles(target.files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
dragging = false;
|
||||||
|
if (disabled) return;
|
||||||
|
handleFiles(e.dataTransfer?.files ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPreview() {
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
previewUrl = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(cleanupPreview);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={inputEl}
|
||||||
|
type="file"
|
||||||
|
name={name}
|
||||||
|
accept="image/*"
|
||||||
|
required={required}
|
||||||
|
disabled={disabled}
|
||||||
|
capture="environment"
|
||||||
|
on:change={onInputChange}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="dropzone"
|
||||||
|
class:has-image={!!previewUrl}
|
||||||
|
class:dragging={dragging}
|
||||||
|
on:click={openFileDialog}
|
||||||
|
on:dragover|preventDefault={() => !disabled && (dragging = true)}
|
||||||
|
on:dragleave={() => (dragging = false)}
|
||||||
|
on:drop={onDrop}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{#if previewUrl}
|
||||||
|
<img src={previewUrl} alt="Selected preview" />
|
||||||
|
|
||||||
|
<div class="overlay">
|
||||||
|
<ImagePlus size={24} />
|
||||||
|
<span>Replace image</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder py-4">
|
||||||
|
<ImagePlus size={32} />
|
||||||
|
<p>Click or drag an image here <span class="text-error">{required ? '*' : ''}</span></p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if previewUrl}
|
||||||
|
<button class="hover:text-destructive p-2" on:click={cleanupPreview} type="button">
|
||||||
|
<X size={24} class="inline" />
|
||||||
|
<span class="inline align-middle">Remove image</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropzone {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
min-height: 80px;
|
||||||
|
/*height: 200px;*/
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
/*background: var(--bg, #fafafa);*/
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone.dragging {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-image:hover .overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-image:hover img {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
src/lib/components/custom/item-listing.svelte
Normal file
45
src/lib/components/custom/item-listing.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import type { Item } from '$lib/types/item';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||||
|
|
||||||
|
export let item: Item = <Item>{
|
||||||
|
id: 2,
|
||||||
|
// title: 'Water Bottle',
|
||||||
|
foundDate: new Date(),
|
||||||
|
approvedDate: new Date(),
|
||||||
|
description: 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side.',
|
||||||
|
transferred: true,
|
||||||
|
foundLocation: 'By the tennis courts.'
|
||||||
|
};
|
||||||
|
export let admin = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href="items/{item.id}"
|
||||||
|
class="bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs">
|
||||||
|
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" alt="" class="object-cover max-h-48">
|
||||||
|
<div class="px-2 pb-2">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<!-- <div class="font-bold inline-block">{item.title}</div>-->
|
||||||
|
<!-- <div class="inline-block">-->
|
||||||
|
{#if item.transferred}
|
||||||
|
<Badge variant="secondary" class="float-right">In Lost & Found</Badge>
|
||||||
|
{:else}
|
||||||
|
<Badge variant="outline" class="float-right">With Finder</Badge>
|
||||||
|
{/if}
|
||||||
|
<div>{item.description}</div>
|
||||||
|
{#if item.foundLocation}
|
||||||
|
<div class="pt-2">
|
||||||
|
<LocationIcon class="float-left mr-1" size={24} />
|
||||||
|
<div>{item.foundLocation}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
|
indeterminate = $bindable(false),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
data-slot="checkbox"
|
||||||
|
class={cn(
|
||||||
|
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked, indeterminate })}
|
||||||
|
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||||
|
{#if checked}
|
||||||
|
<CheckIcon class="size-3.5" />
|
||||||
|
{:else if indeterminate}
|
||||||
|
<MinusIcon class="size-3.5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
6
src/lib/components/ui/checkbox/index.ts
Normal file
6
src/lib/components/ui/checkbox/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Root from "./checkbox.svelte";
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Checkbox,
|
||||||
|
};
|
||||||
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||||
45
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
45
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||||
|
import DialogPortal from './dialog-portal.svelte';
|
||||||
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import * as Dialog from './index.js';
|
||||||
|
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||||
|
children: Snippet;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPortal {...portalProps}>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-content"
|
||||||
|
class={cn(
|
||||||
|
"max-h-[calc(100%-2rem)] overflow-y-auto bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
{#if showCloseButton}
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
{/if}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="dialog-header"
|
||||||
|
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
class={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Portal {...restProps} />
|
||||||
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
data-slot="dialog-title"
|
||||||
|
class={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||||
7
src/lib/components/ui/dialog/dialog.svelte
Normal file
7
src/lib/components/ui/dialog/dialog.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Root bind:open {...restProps} />
|
||||||
34
src/lib/components/ui/dialog/index.ts
Normal file
34
src/lib/components/ui/dialog/index.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Root from "./dialog.svelte";
|
||||||
|
import Portal from "./dialog-portal.svelte";
|
||||||
|
import Title from "./dialog-title.svelte";
|
||||||
|
import Footer from "./dialog-footer.svelte";
|
||||||
|
import Header from "./dialog-header.svelte";
|
||||||
|
import Overlay from "./dialog-overlay.svelte";
|
||||||
|
import Content from "./dialog-content.svelte";
|
||||||
|
import Description from "./dialog-description.svelte";
|
||||||
|
import Trigger from "./dialog-trigger.svelte";
|
||||||
|
import Close from "./dialog-close.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Dialog,
|
||||||
|
Title as DialogTitle,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Footer as DialogFooter,
|
||||||
|
Header as DialogHeader,
|
||||||
|
Trigger as DialogTrigger,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Description as DialogDescription,
|
||||||
|
Close as DialogClose,
|
||||||
|
};
|
||||||
10
src/lib/components/ui/radio-group/index.ts
Normal file
10
src/lib/components/ui/radio-group/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Root from "./radio-group.svelte";
|
||||||
|
import Item from "./radio-group-item.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
//
|
||||||
|
Root as RadioGroup,
|
||||||
|
Item as RadioGroupItem,
|
||||||
|
};
|
||||||
31
src/lib/components/ui/radio-group/radio-group-item.svelte
Normal file
31
src/lib/components/ui/radio-group/radio-group-item.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
class={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
|
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
|
||||||
|
{#if checked}
|
||||||
|
<CircleIcon
|
||||||
|
class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
19
src/lib/components/ui/radio-group/radio-group.svelte
Normal file
19
src/lib/components/ui/radio-group/radio-group.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value = $bindable(""),
|
||||||
|
...restProps
|
||||||
|
}: RadioGroupPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
bind:ref
|
||||||
|
bind:value
|
||||||
|
data-slot="radio-group"
|
||||||
|
class={cn("grid gap-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
37
src/lib/components/ui/select/index.ts
Normal file
37
src/lib/components/ui/select/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Root from "./select.svelte";
|
||||||
|
import Group from "./select-group.svelte";
|
||||||
|
import Label from "./select-label.svelte";
|
||||||
|
import Item from "./select-item.svelte";
|
||||||
|
import Content from "./select-content.svelte";
|
||||||
|
import Trigger from "./select-trigger.svelte";
|
||||||
|
import Separator from "./select-separator.svelte";
|
||||||
|
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import GroupHeading from "./select-group-heading.svelte";
|
||||||
|
import Portal from "./select-portal.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Label,
|
||||||
|
Item,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Separator,
|
||||||
|
ScrollDownButton,
|
||||||
|
ScrollUpButton,
|
||||||
|
GroupHeading,
|
||||||
|
Portal,
|
||||||
|
//
|
||||||
|
Root as Select,
|
||||||
|
Group as SelectGroup,
|
||||||
|
Label as SelectLabel,
|
||||||
|
Item as SelectItem,
|
||||||
|
Content as SelectContent,
|
||||||
|
Trigger as SelectTrigger,
|
||||||
|
Separator as SelectSeparator,
|
||||||
|
ScrollDownButton as SelectScrollDownButton,
|
||||||
|
ScrollUpButton as SelectScrollUpButton,
|
||||||
|
GroupHeading as SelectGroupHeading,
|
||||||
|
Portal as SelectPortal,
|
||||||
|
};
|
||||||
45
src/lib/components/ui/select/select-content.svelte
Normal file
45
src/lib/components/ui/select/select-content.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import SelectPortal from "./select-portal.svelte";
|
||||||
|
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
preventScroll = true,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPortal {...portalProps}>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
{sideOffset}
|
||||||
|
{preventScroll}
|
||||||
|
data-slot="select-content"
|
||||||
|
class={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
class={cn(
|
||||||
|
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPortal>
|
||||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-group-heading"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.GroupHeading>
|
||||||
7
src/lib/components/ui/select/select-group.svelte
Normal file
7
src/lib/components/ui/select/select-group.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />
|
||||||
38
src/lib/components/ui/select/select-item.svelte
Normal file
38
src/lib/components/ui/select/select-item.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
{value}
|
||||||
|
data-slot="select-item"
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ selected, highlighted })}
|
||||||
|
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if selected}
|
||||||
|
<CheckIcon class="size-4" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if childrenProp}
|
||||||
|
{@render childrenProp({ selected, highlighted })}
|
||||||
|
{:else}
|
||||||
|
{label || value}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</SelectPrimitive.Item>
|
||||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="select-label"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
7
src/lib/components/ui/select/select-portal.svelte
Normal file
7
src/lib/components/ui/select/select-portal.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Portal {...restProps} />
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
18
src/lib/components/ui/select/select-separator.svelte
Normal file
18
src/lib/components/ui/select/select-separator.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SeparatorPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-separator"
|
||||||
|
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
size = "default",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
class={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon class="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
11
src/lib/components/ui/select/select.svelte
Normal file
11
src/lib/components/ui/select/select.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(false),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: SelectPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||||
7
src/lib/components/ui/textarea/index.ts
Normal file
7
src/lib/components/ui/textarea/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./textarea.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Textarea,
|
||||||
|
};
|
||||||
23
src/lib/components/ui/textarea/textarea.svelte
Normal file
23
src/lib/components/ui/textarea/textarea.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||||
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
"data-slot": dataSlot = "textarea",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
></textarea>
|
||||||
@ -7,3 +7,22 @@ export const PERMISSIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EXPIRE_REMINDER_DAYS = 30;
|
export const EXPIRE_REMINDER_DAYS = 30;
|
||||||
|
|
||||||
|
// const EMAIL_REGEX = new RegExp(
|
||||||
|
// // eslint-disable-next-line no-control-regex
|
||||||
|
// "([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\"\(\[\]!#-[^-~ \t]|(\\[\t -~]))+\")@([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\[[\t -Z^-~]*])"
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const EMAIL_REGEX = new RegExp(
|
||||||
|
// /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const EMAIL_REGEX =
|
||||||
|
// // eslint-disable-next-line no-control-regex
|
||||||
|
// /(?:[a-z0-9!#$%&'*+\/=?^`\{-\}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`\{-\}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
|
||||||
|
|
||||||
|
const EMAIL_REGEX =
|
||||||
|
/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_'+-]@([a-z0-9][a-z0-9-]*\.)+[a-z]{2,}$/i;
|
||||||
|
|
||||||
|
// Replace single quote with HTML entity or remove it from the character class
|
||||||
|
export const EMAIL_REGEX_STRING = EMAIL_REGEX.source.replace(/'/g, ''');
|
||||||
|
|||||||
@ -8,7 +8,8 @@ const sql = postgres({
|
|||||||
port: parseInt(process.env.POSTGRES_PORT!),
|
port: parseInt(process.env.POSTGRES_PORT!),
|
||||||
database: process.env.POSTGRES_DB,
|
database: process.env.POSTGRES_DB,
|
||||||
username: process.env.POSTGRES_USER,
|
username: process.env.POSTGRES_USER,
|
||||||
password: process.env.POSTGRES_PASSWORD
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
|
transform: postgres.camel
|
||||||
});
|
});
|
||||||
|
|
||||||
export default sql;
|
export default sql;
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
import type { User } from '$lib/types';
|
|
||||||
import sql from '$lib/db/db.server';
|
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
// should require MANAGE_USERS permission
|
|
||||||
|
|
||||||
export async function getUsers(searchQuery: string | null = null): Promise<User[]> {
|
|
||||||
return sql`
|
|
||||||
SELECT id,
|
|
||||||
email,
|
|
||||||
perms,
|
|
||||||
name,
|
|
||||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
|
||||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
|
||||||
FROM users
|
|
||||||
WHERE (${!searchQuery ? sql`TRUE` : sql`email ILIKE ${'%' + searchQuery + '%'} OR name ILIKE ${'%' + searchQuery + '%'}`});
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUser(id: number): Promise<User> {
|
|
||||||
return <User>(
|
|
||||||
await sql`
|
|
||||||
SELECT id,
|
|
||||||
email,
|
|
||||||
perms,
|
|
||||||
name,
|
|
||||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
|
||||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
|
||||||
FROM users
|
|
||||||
WHERE (id = ${id}) LIMIT 1;
|
|
||||||
`
|
|
||||||
)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createUser(user: User) {
|
|
||||||
const passwordHash = await bcrypt.hash(user.password!, 12);
|
|
||||||
await sql`
|
|
||||||
INSERT INTO users (email, password_hash, perms, name, created_at, last_signin)
|
|
||||||
VALUES (${user.email}, ${passwordHash}, ${user.perms}, ${user.name}, NOW(), NOW())
|
|
||||||
RETURNING id;
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editUser(user: User) {
|
|
||||||
let passwordHash: string | undefined;
|
|
||||||
if (user.password) {
|
|
||||||
passwordHash = await bcrypt.hash(user.password, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sql`
|
|
||||||
UPDATE users
|
|
||||||
SET
|
|
||||||
email = ${user.email},
|
|
||||||
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
|
|
||||||
perms = ${user.perms},
|
|
||||||
name = ${user.name}
|
|
||||||
WHERE id = ${user.id!}
|
|
||||||
RETURNING id;
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUser(id: number) {
|
|
||||||
await sql`
|
|
||||||
DELETE FROM users
|
|
||||||
WHERE id = ${id}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
23
src/lib/email/sender.server.ts
Normal file
23
src/lib/email/sender.server.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
|
// Create a transporter object using SMTP transport
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_HOST,
|
||||||
|
port: Number(process.env.EMAIL_PORT),
|
||||||
|
secure: true, // true for 465, false for other ports
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function sendEmployerNotificationEmail() {
|
||||||
|
// Send mail with defined transport object
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `CareerConnect Notifications <${process.env.EMAIL_USER}>`,
|
||||||
|
// to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
|
||||||
|
to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||||
|
subject: 'New Application Received!',
|
||||||
|
text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import type { User } from '$lib/types';
|
import type { User } from '$lib/types/user';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const getCookieValue = (name: string): string =>
|
export const getCookieValue = (name: string): string =>
|
||||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||||
@ -21,15 +22,21 @@ export function getFormString(data: FormData, key: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
throw Error(`Incorrect input in field ${key}.`);
|
throw fail(400, {
|
||||||
|
error: `Incorrect input in field ${key}.`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRequiredFormString(data: FormData, key: string) {
|
export function getRequiredFormString(data: FormData, key: string) {
|
||||||
const value = data.get(key);
|
const value = data.get(key);
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string' || value === '') {
|
||||||
throw Error(`Missing required field ${key}.`);
|
throw fail(400, {
|
||||||
|
error: `Missing required field ${key}.`,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
id: number;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
password?: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: Date;
|
|
||||||
lastSignIn: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Item {
|
|
||||||
id: number;
|
|
||||||
ownerEmail: string;
|
|
||||||
ownerPhone: string;
|
|
||||||
foundDate: Date;
|
|
||||||
approvedDate?: Date;
|
|
||||||
claimedDate?: Date;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
transferred: boolean; // to L&F location
|
|
||||||
keywords?: string[];
|
|
||||||
}
|
|
||||||
13
src/lib/types/inquiries.server.ts
Normal file
13
src/lib/types/inquiries.server.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export enum Sender {
|
||||||
|
ADMIN = 'admin',
|
||||||
|
FINDER = 'finder',
|
||||||
|
INQUIRER = 'inquirer'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface message {
|
||||||
|
id: number;
|
||||||
|
threadId: number;
|
||||||
|
sender: Sender;
|
||||||
|
body: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
13
src/lib/types/item.d.ts
vendored
Normal file
13
src/lib/types/item.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export interface Item {
|
||||||
|
id: number;
|
||||||
|
emails?: string[];
|
||||||
|
// ownerPhone: string;
|
||||||
|
foundDate: Date;
|
||||||
|
approvedDate?: Date;
|
||||||
|
claimedDate?: Date;
|
||||||
|
// title: string;
|
||||||
|
description: string;
|
||||||
|
transferred: boolean; // to L&F location
|
||||||
|
keywords?: string[];
|
||||||
|
foundLocation: string;
|
||||||
|
}
|
||||||
30
src/lib/types/user.ts
Normal file
30
src/lib/types/user.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
password?: string;
|
||||||
|
password_hash?: string;
|
||||||
|
settings?: UserSettings;
|
||||||
|
createdAt: Date;
|
||||||
|
lastSignIn: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserPayload {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserSettings {
|
||||||
|
staleItemDays: number;
|
||||||
|
notifyAllApprovedInquiries?: boolean;
|
||||||
|
notifyAllTurnedInInquiries?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DefaultUserSettings: UserSettings = {
|
||||||
|
staleItemDays: 30,
|
||||||
|
notifyAllApprovedInquiries: false,
|
||||||
|
notifyAllTurnedInInquiries: false
|
||||||
|
};
|
||||||
34
src/routes/+error.svelte
Normal file
34
src/routes/+error.svelte
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/state';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="text-center" style="padding-top: 32px">
|
||||||
|
<h1 class="text-9xl font-bold">
|
||||||
|
{page.status}
|
||||||
|
</h1>
|
||||||
|
<h1 class="text-5xl mb-6">That's an error</h1>
|
||||||
|
{#if page.status === 404}
|
||||||
|
<p>We cant seem to find the page you are looking for.</p>
|
||||||
|
<p>The address may be mistyped, or the page may have moved or been deleted.</p>
|
||||||
|
{/if}
|
||||||
|
{#if page.status === 403}
|
||||||
|
<p>You dont have access to this page!</p>
|
||||||
|
<p>Please contact your admin if you think this is a mistake</p>
|
||||||
|
{/if}
|
||||||
|
{#if page.status === 401}
|
||||||
|
<p>You must be signed-in to view this page!</p>
|
||||||
|
{/if}
|
||||||
|
{#if page.status === 500}
|
||||||
|
<p>This one is on our end...</p>
|
||||||
|
<p>We are working to resolve this as fast as possible.</p>
|
||||||
|
{/if}
|
||||||
|
{#if page.status === 400}
|
||||||
|
<p>Invalid request!</p>
|
||||||
|
<p>Make sure you have correctly inputted all information.</p>
|
||||||
|
{/if}
|
||||||
|
{#if page.status !== 404 && page.status !== 403 && page.status !== 401 && page.status !== 500 && page.status !== 400}
|
||||||
|
<p>An unexpected error has occurred.</p>
|
||||||
|
<p>Please try again later</p>
|
||||||
|
{/if}
|
||||||
|
<p class="mt-8">Error: {page.error?.message}</p>
|
||||||
|
</div>
|
||||||
@ -1,10 +1,5 @@
|
|||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
import { getUserFromJWT } from '$lib/shared';
|
|
||||||
|
|
||||||
export const load: LayoutServerLoad = ({ cookies }) => {
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
const jwt = cookies.get('jwt');
|
return { user: locals.user };
|
||||||
if (jwt) {
|
|
||||||
return { selfUser: getUserFromJWT(jwt) };
|
|
||||||
}
|
|
||||||
return { selfUser: null };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,29 +1,59 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import MdevTriangle from '$lib/components/custom/mdev-triangle.svelte';
|
||||||
|
import { ModeWatcher, toggleMode } from 'mode-watcher';
|
||||||
|
import SunIcon from '@lucide/svelte/icons/sun';
|
||||||
|
import MoonIcon from '@lucide/svelte/icons/moon';
|
||||||
|
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
|
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link
|
<ModeWatcher />
|
||||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..40,400,0,0&display=block&icon_names=account_circle,arrow_drop_down,arrow_drop_up,calendar_today,call,check,close,cloud_upload,dark_mode,delete,description,edit,group,home,info,light_mode,login,mail,menu,open_in_new,person,search,sell,store,upload,visibility,visibility_off,work"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!--<header class="bottom-border flex justify-between">-->
|
<div class="flex flex-col min-h-dvh">
|
||||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/">home</a>-->
|
|
||||||
<!-- {#if data.selfUser}-->
|
|
||||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/account"-->
|
|
||||||
<!-- >account_circle</a-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- {:else}-->
|
|
||||||
<!-- <a class="material-symbols-outlined p-2 !text-3xl leading-none" href="/signin">login</a>-->
|
|
||||||
<!-- {/if}-->
|
|
||||||
<!--</header>-->
|
|
||||||
|
|
||||||
<div style="max-width: 800px;" class="center">
|
<div class="flex justify-center">
|
||||||
|
|
||||||
|
<header class="flex justify-between items-center max-w-7xl w-screen p-4">
|
||||||
|
<a href="/" class="flex items-center gap-2 text-2xl font-bold">
|
||||||
|
<MdevTriangle size={48} class="text-primary" />
|
||||||
|
<span class="hidden sm:block">MarinoDev Lost & Found</span>
|
||||||
|
</a>
|
||||||
|
<div class="items-center flex gap-4">
|
||||||
|
<a href="/login" class="{buttonVariants({ variant: 'outline' })}">Admin Log in</a>
|
||||||
|
<div class="inline-block">
|
||||||
|
<Button onclick={toggleMode} variant="outline" size="icon">
|
||||||
|
<SunIcon
|
||||||
|
class="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 !transition-all dark:scale-0 dark:-rotate-90"
|
||||||
|
/>
|
||||||
|
<MoonIcon
|
||||||
|
class="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 !transition-all dark:scale-100 dark:rotate-0"
|
||||||
|
/>
|
||||||
|
<span class="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<main class="w-full">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="gap-2 py-2 text-center text-sm text-muted-foreground">
|
||||||
|
<p>
|
||||||
|
{new Date().getFullYear()} MarinoDev. <a href="https://git.marinodev.com/drake/fbla26"
|
||||||
|
class="hover:underline">Git Repository</a>
|
||||||
|
</p>
|
||||||
|
<!-- <p>-->
|
||||||
|
<!-- <a href="/privacy" class="underline hover:text-gray-700">Privacy Policy</a> |-->
|
||||||
|
<!-- <a href="/terms" class="underline hover:text-gray-700">Terms of Service</a>-->
|
||||||
|
<!-- </p>-->
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
|
|||||||
89
src/routes/+page.server.ts
Normal file
89
src/routes/+page.server.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
import { getFormString, getRequiredFormString } from '$lib/shared';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import path from 'path';
|
||||||
|
import sql from '$lib/db/db.server';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { writeFileSync } from 'node:fs';
|
||||||
|
import type { Item } from '$lib/types/item';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async () => {
|
||||||
|
const items: Item[] = await sql`SELECT * FROM items;`;
|
||||||
|
return {
|
||||||
|
items
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
create: async ({ request }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
|
||||||
|
const description = getRequiredFormString(data, 'description');
|
||||||
|
const foundLocation = getFormString(data, 'foundLocation') || null;
|
||||||
|
const email = getFormString(data, 'email') || null;
|
||||||
|
const location = getFormString(data, 'location') || null;
|
||||||
|
const file = data.get('image')!;
|
||||||
|
|
||||||
|
if (!email && location !== 'turnedIn') {
|
||||||
|
fail(400, {
|
||||||
|
error: "Email is required if it is still in finder's possession",
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(file instanceof File)) {
|
||||||
|
return fail(400, { error: 'No file uploaded or file is invalid', success: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert File → Buffer
|
||||||
|
const inputBuffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
|
// Detect format (Sharp does this internally)
|
||||||
|
const image = sharp(inputBuffer);
|
||||||
|
const metadata = await image.metadata();
|
||||||
|
|
||||||
|
let outputBuffer: Buffer;
|
||||||
|
|
||||||
|
if (metadata.format === 'jpeg') {
|
||||||
|
// Already JPG → keep as-is
|
||||||
|
outputBuffer = inputBuffer;
|
||||||
|
} else {
|
||||||
|
// Convert to JPG
|
||||||
|
outputBuffer = await image
|
||||||
|
.jpeg({ quality: 90 }) // adjust if needed
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
let emailList = null;
|
||||||
|
if (email) {
|
||||||
|
emailList = [email];
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await sql`
|
||||||
|
INSERT INTO items (
|
||||||
|
email,
|
||||||
|
description,
|
||||||
|
transferred,
|
||||||
|
found_location
|
||||||
|
) VALUES (
|
||||||
|
${emailList},
|
||||||
|
${description},
|
||||||
|
${location === 'turnedIn'},
|
||||||
|
${foundLocation}
|
||||||
|
)
|
||||||
|
RETURNING id;
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// It's a good idea to validate the filename to prevent path traversal attacks
|
||||||
|
const savePath = path.join('uploads', `${response[0]['id']}.jpg`);
|
||||||
|
|
||||||
|
writeFileSync(savePath, outputBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('File upload failed:', err);
|
||||||
|
return fail(500, { error: 'Internal server error during file upload', success: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
@ -1,6 +1,111 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||||
|
import * as Field from '$lib/components/ui/field/index.js';
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||||
|
import { genDescription } from './gen-desc.remote';
|
||||||
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import ItemListing from '$lib/components/custom/item-listing.svelte';
|
||||||
|
|
||||||
|
let itemLocation: string | undefined = $state('');
|
||||||
|
let foundLocation: string | undefined = $state();
|
||||||
|
let description: string | undefined = $state();
|
||||||
|
let isGenerating = $state(false);
|
||||||
|
|
||||||
|
async function onSelect() {
|
||||||
|
isGenerating = true;
|
||||||
|
description = await genDescription();
|
||||||
|
isGenerating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto px-4">
|
||||||
|
<div class="justify-between flex">
|
||||||
|
<h1 class="font-semibold text-4xl mb-4 mt-2">Found Items</h1>
|
||||||
|
<div class="inline-block">
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger class={buttonVariants({ variant: 'default' })} type="button"
|
||||||
|
>Post an item
|
||||||
|
</Dialog.Trigger
|
||||||
|
>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Submit Found Item</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Your item will need to be approved before becoming public.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form method="post" action="?/create" enctype="multipart/form-data">
|
||||||
|
<Field.Group>
|
||||||
|
<ImageUpload onSelect={onSelect} required={true} />
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="description">
|
||||||
|
Description<span class="text-error">*</span>
|
||||||
|
</Field.Label>
|
||||||
|
<Input id="description" name="description" bind:value={description}
|
||||||
|
placeholder="A red leather book bag..."
|
||||||
|
required />
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="foundLocation">
|
||||||
|
Where did you find it?
|
||||||
|
</Field.Label>
|
||||||
|
<Input id="foundLocation" name="foundLocation" bind:value={foundLocation}
|
||||||
|
placeholder="By the tennis courts." required />
|
||||||
|
</Field.Field>
|
||||||
|
<RadioGroup.Root name="location" bind:value={itemLocation}>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="finderPossession" id="finderPossession" />
|
||||||
|
<Label for="finderPossession">I still have the item.</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="turnedIn" id="turnedIn" />
|
||||||
|
<Label for="turnedIn">I turned the item in to the school lost and found.</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
<Field.Field class={itemLocation !== 'finderPossession' ? 'disabled hidden' : ''}>
|
||||||
|
<Field.Label for="email">
|
||||||
|
Your Email
|
||||||
|
</Field.Label>
|
||||||
|
<Input id="email" name="email" placeholder="name@domain.com"
|
||||||
|
class={itemLocation !== 'finderPossession' ? 'disabled' : ''} pattern={EMAIL_REGEX_STRING}
|
||||||
|
required={itemLocation === 'finderPossesion'} />
|
||||||
|
<!-- <Field.Error>Enter a valid email address.</Field.Error>-->
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
|
||||||
|
<Dialog.Footer class="mt-4">
|
||||||
|
<Dialog.Close class={buttonVariants({ variant: "outline" })} type="button"
|
||||||
|
>Cancel
|
||||||
|
</Dialog.Close
|
||||||
|
>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="justify-start grid gap-4 grid-cols-[repeat(auto-fill,minmax(16rem,max-content))]">
|
||||||
|
{#each data.items as item (item.id)}
|
||||||
|
<ItemListing item={item} />
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{#if isGenerating}
|
||||||
|
<div class="fixed inset-0 bg-black/75 z-999999 w-screen h-screen justify-center items-center flex">
|
||||||
|
<p class="text-6xl text-primary">Loading...</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|||||||
0
src/routes/create/+page.server.ts
Normal file
0
src/routes/create/+page.server.ts
Normal file
143
src/routes/create/+page.svelte
Normal file
143
src/routes/create/+page.svelte
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||||
|
import * as Field from '$lib/components/ui/field/index.js';
|
||||||
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
|
import * as Select from '$lib/components/ui/select/index.js';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||||
|
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
|
||||||
|
|
||||||
|
let month = $state<string>();
|
||||||
|
let year = $state<string>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full max-w-lg mt-8">
|
||||||
|
<form>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Set>
|
||||||
|
<Field.Legend>Submit Found Item</Field.Legend>
|
||||||
|
<Field.Description
|
||||||
|
>Your item will need to be approved before becoming public
|
||||||
|
</Field.Description>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="image-upload">
|
||||||
|
Image
|
||||||
|
</Field.Label>
|
||||||
|
<ImageUpload></ImageUpload>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="checkout-7j9-card-name-43j"
|
||||||
|
>Name on Card
|
||||||
|
</Field.Label
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="checkout-7j9-card-name-43j"
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<Field.Field class="col-span-2">
|
||||||
|
<Field.Label for="checkout-7j9-card-number-uw1">
|
||||||
|
Card Number
|
||||||
|
</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="checkout-7j9-card-number-uw1"
|
||||||
|
placeholder="1234 5678 9012 3456"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Field.Description>Enter your 16-digit number.</Field.Description>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field class="col-span-1">
|
||||||
|
<Field.Label for="checkout-7j9-cvv">CVV</Field.Label>
|
||||||
|
<Input id="checkout-7j9-cvv" placeholder="123" required />
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="checkout-7j9-exp-month-ts6">Month</Field.Label>
|
||||||
|
<Select.Root type="single" bind:value={month}>
|
||||||
|
<Select.Trigger id="checkout-7j9-exp-month-ts6">
|
||||||
|
<span>
|
||||||
|
{month || "MM"}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="01">01</Select.Item>
|
||||||
|
<Select.Item value="02">02</Select.Item>
|
||||||
|
<Select.Item value="03">03</Select.Item>
|
||||||
|
<Select.Item value="04">04</Select.Item>
|
||||||
|
<Select.Item value="05">05</Select.Item>
|
||||||
|
<Select.Item value="06">06</Select.Item>
|
||||||
|
<Select.Item value="07">07</Select.Item>
|
||||||
|
<Select.Item value="08">08</Select.Item>
|
||||||
|
<Select.Item value="09">09</Select.Item>
|
||||||
|
<Select.Item value="10">10</Select.Item>
|
||||||
|
<Select.Item value="11">11</Select.Item>
|
||||||
|
<Select.Item value="12">12</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="checkout-7j9-exp-year-f59">Year</Field.Label>
|
||||||
|
<Select.Root type="single" bind:value={year}>
|
||||||
|
<Select.Trigger id="checkout-7j9-exp-year-f59">
|
||||||
|
<span>
|
||||||
|
{year || "YYYY"}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="2024">2024</Select.Item>
|
||||||
|
<Select.Item value="2025">2025</Select.Item>
|
||||||
|
<Select.Item value="2026">2026</Select.Item>
|
||||||
|
<Select.Item value="2027">2027</Select.Item>
|
||||||
|
<Select.Item value="2028">2028</Select.Item>
|
||||||
|
<Select.Item value="2029">2029</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
</Field.Group>
|
||||||
|
</Field.Set>
|
||||||
|
<Field.Separator />
|
||||||
|
<Field.Set>
|
||||||
|
<Field.Legend>Billing Address</Field.Legend>
|
||||||
|
<Field.Description>
|
||||||
|
The billing address associated with your payment method
|
||||||
|
</Field.Description>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Field orientation="horizontal">
|
||||||
|
<Checkbox id="checkout-7j9-same-as-shipping-wgm" checked={true} />
|
||||||
|
<Field.Label
|
||||||
|
for="checkout-7j9-same-as-shipping-wgm"
|
||||||
|
class="font-normal"
|
||||||
|
>
|
||||||
|
Same as shipping address
|
||||||
|
</Field.Label>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</Field.Set>
|
||||||
|
<Field.Separator />
|
||||||
|
<Field.Set>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="checkout-7j9-optional-comments"
|
||||||
|
>Comments
|
||||||
|
</Field.Label
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="checkout-7j9-optional-comments"
|
||||||
|
placeholder="Add any additional comments"
|
||||||
|
class="resize-none"
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</Field.Set>
|
||||||
|
<Field.Field orientation="horizontal">
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
<Button variant="outline" type="button">Cancel</Button>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
7
src/routes/gen-desc.remote.ts
Normal file
7
src/routes/gen-desc.remote.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { query } from '$app/server';
|
||||||
|
|
||||||
|
export const genDescription = query(async () => {
|
||||||
|
await new Promise((f) => setTimeout(f, 1000));
|
||||||
|
|
||||||
|
return 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side, resting on a tree trunk in a forest.';
|
||||||
|
});
|
||||||
0
src/routes/items/[itemid]/+page.svelte
Normal file
0
src/routes/items/[itemid]/+page.svelte
Normal file
@ -1,27 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LoginForm from '$lib/components/login-form.svelte';
|
import LoginForm from './login-form.svelte';
|
||||||
import MdevTriangle from '$lib/components/mdev-triangle.svelte';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid min-h-svh lg:grid-cols-2 w-screen">
|
<div class="grid min-h-svh lg:grid-cols-2">
|
||||||
<div class="flex flex-col gap-4 p-6 md:p-10">
|
<div class="flex flex-col gap-4 md:p-6">
|
||||||
<div class="flex justify-center gap-2 md:justify-start">
|
<div class="flex-1"></div>
|
||||||
<a href="/" class="flex items-center gap-2 font-medium">
|
<div class="justify-center flex">
|
||||||
<div
|
|
||||||
class="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<MdevTriangle />
|
|
||||||
</div>
|
|
||||||
MarinoDev
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 items-center justify-center">
|
|
||||||
<div class="w-full max-w-xs">
|
<div class="w-full max-w-xs">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-muted relative hidden lg:block">
|
<div class="bg-muted relative hidden lg:block">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@ -2,11 +2,10 @@
|
|||||||
import {
|
import {
|
||||||
FieldGroup,
|
FieldGroup,
|
||||||
Field,
|
Field,
|
||||||
FieldLabel,
|
FieldLabel
|
||||||
FieldDescription
|
} from '$lib/components/ui/field';
|
||||||
} from '$lib/components/ui/field/index.js';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
|
||||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||||
import type { HTMLFormAttributes } from 'svelte/elements';
|
import type { HTMLFormAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
@ -21,7 +20,7 @@
|
|||||||
<div class="flex flex-col items-center gap-1 text-center">
|
<div class="flex flex-col items-center gap-1 text-center">
|
||||||
<h1 class="text-2xl font-bold">Login to your account</h1>
|
<h1 class="text-2xl font-bold">Login to your account</h1>
|
||||||
<p class="text-muted-foreground text-sm text-balance">
|
<p class="text-muted-foreground text-sm text-balance">
|
||||||
Enter your email below to login to your account
|
Only admins need to log in.<br>Lost something? Go <a href="/" class="underline text-primary">home</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Field>
|
<Field>
|
||||||
@ -40,11 +39,11 @@
|
|||||||
<Field>
|
<Field>
|
||||||
<Button type="submit">Login</Button>
|
<Button type="submit">Login</Button>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<!-- <Field>-->
|
||||||
<FieldDescription class="text-center">
|
<!-- <FieldDescription class="text-center">-->
|
||||||
Don't have an account?
|
<!-- Don't have an account?-->
|
||||||
<a href="/signup" class="underline underline-offset-4">Sign up</a>
|
<!-- <a href="/signup" class="underline underline-offset-4">Sign up</a>-->
|
||||||
</FieldDescription>
|
<!-- </FieldDescription>-->
|
||||||
</Field>
|
<!-- </Field>-->
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SignupForm from '$lib/components/signup-form.svelte';
|
import SignupForm from './signup-form.svelte';
|
||||||
import MdevTriangle from '$lib/components/mdev-triangle.svelte';
|
import MdevTriangle from '$lib/components/custom/mdev-triangle.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid min-h-svh lg:grid-cols-2 w-screen">
|
<div class="grid min-h-svh lg:grid-cols-2 w-screen">
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from '$lib/utils.js';
|
import { cn } from '$lib/utils.js';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Field from '$lib/components/ui/field/index.js';
|
import * as Field from '$lib/components/ui/field';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import type { HTMLAttributes } from 'svelte/elements';
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
let { class: className, ...restProps }: HTMLAttributes<HTMLFormElement> = $props();
|
let { class: className, ...restProps }: HTMLAttributes<HTMLFormElement> = $props();
|
||||||
BIN
static/no-image-available.png
Normal file
BIN
static/no-image-available.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@ -6,7 +6,15 @@ const config = {
|
|||||||
preprocess: sveltePreprocess(),
|
preprocess: sveltePreprocess(),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
|
experimental: {
|
||||||
|
remoteFunctions: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compilerOptions: {
|
||||||
|
experimental: {
|
||||||
|
async: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user