style: format entire codebase with prettier
This commit is contained in:
@@ -35,11 +35,13 @@ docker run -d \
|
||||
## Environment Variables
|
||||
|
||||
Required:
|
||||
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `NODE_ENV` - Set to `production`
|
||||
- `PORT` - Default `3000`
|
||||
|
||||
Optional (Docker Compose):
|
||||
|
||||
- `POSTGRES_USER` - Database user (default: `wishlistuser`)
|
||||
- `POSTGRES_PASSWORD` - Database password (default: `wishlistpassword`)
|
||||
- `POSTGRES_DB` - Database name (default: `wishlist`)
|
||||
@@ -61,6 +63,7 @@ docker exec -it wishlist-app bun run db:push
|
||||
## Migrations
|
||||
|
||||
Production migrations:
|
||||
|
||||
```bash
|
||||
docker exec -it wishlist-app bun run db:migrate
|
||||
```
|
||||
|
||||
5
SETUP.md
5
SETUP.md
@@ -16,6 +16,7 @@ Visit `http://localhost:3000`
|
||||
Choose one:
|
||||
|
||||
**Local PostgreSQL:**
|
||||
|
||||
```bash
|
||||
sudo apt install postgresql
|
||||
sudo -u postgres createdb wishlist
|
||||
@@ -26,6 +27,7 @@ GRANT ALL PRIVILEGES ON DATABASE wishlist TO wishlistuser;
|
||||
```
|
||||
|
||||
**Docker PostgreSQL:**
|
||||
|
||||
```bash
|
||||
docker run --name wishlist-postgres \
|
||||
-e POSTGRES_DB=wishlist \
|
||||
@@ -57,15 +59,18 @@ Visit `http://localhost:5173`
|
||||
## Troubleshooting
|
||||
|
||||
**Connection errors:**
|
||||
|
||||
- Check PostgreSQL is running: `sudo systemctl status postgresql`
|
||||
- Test connection: `psql "postgresql://user:pass@localhost:5432/wishlist"`
|
||||
|
||||
**Port in use:**
|
||||
|
||||
```bash
|
||||
bun run dev -- --port 3000
|
||||
```
|
||||
|
||||
**Schema changes:**
|
||||
|
||||
```bash
|
||||
bun run db:push
|
||||
```
|
||||
|
||||
209
bun.lock
209
bun.lock
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "wishlist-app",
|
||||
@@ -14,13 +13,13 @@
|
||||
"bits-ui": "^2.14.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"lucide-svelte": "^0.554.0",
|
||||
"postgres": "^3.4.7",
|
||||
"svelte-dnd-action": "^0.9.67",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
@@ -29,13 +28,19 @@
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-svelte": "^3.5.1",
|
||||
"globals": "^16.0.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"svelte": "^5.43.8",
|
||||
"svelte-check": "^4.3.4",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.31.0",
|
||||
"vite": "^7.2.2",
|
||||
},
|
||||
},
|
||||
@@ -105,12 +110,38 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.10.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
@@ -237,26 +268,58 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
||||
|
||||
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bits-ui": ["bits-ui@2.14.4", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
@@ -267,6 +330,8 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
@@ -281,12 +346,18 @@
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
@@ -317,18 +388,52 @@
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
|
||||
|
||||
"eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrap": ["esrap@2.1.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
|
||||
|
||||
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
@@ -341,6 +446,10 @@
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@@ -353,12 +462,22 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
@@ -375,16 +494,30 @@
|
||||
|
||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||
|
||||
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||
@@ -409,9 +542,13 @@
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
||||
|
||||
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"lucide-svelte": ["lucide-svelte@0.554.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-LLcpHi3SuKup0nVD1kKqo8FDZnjXJp48uST26GGh8Jcyrxqk5gmgpnvKmHsHox674UL3cPS1DCul/wFL7ybGqg=="],
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
@@ -421,6 +558,8 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
@@ -431,6 +570,8 @@
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
|
||||
|
||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||
@@ -441,8 +582,18 @@
|
||||
|
||||
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"patch-package": ["patch-package@8.0.1", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^10.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.2.4", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
@@ -453,6 +604,14 @@
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
|
||||
|
||||
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
|
||||
|
||||
"postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
|
||||
|
||||
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||
|
||||
"postinstall-postinstall": ["postinstall-postinstall@2.1.0", "", {}, "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ=="],
|
||||
@@ -461,12 +620,22 @@
|
||||
|
||||
"preact-render-to-string": ["preact-render-to-string@5.2.3", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.5.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg=="],
|
||||
|
||||
"pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
|
||||
@@ -495,6 +664,8 @@
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -507,6 +678,8 @@
|
||||
|
||||
"svelte-dnd-action": ["svelte-dnd-action@0.9.67", "", { "peerDependencies": { "svelte": ">=3.23.0 || ^5.0.0-next.0" } }, "sha512-yEJQZ9SFy3O4mnOdtjwWyotRsWRktNf4W8k67zgiLiMtMNQnwCyJHBjkGMgZMDh8EGZ4gr88l+GebBWoHDwo+g=="],
|
||||
|
||||
"svelte-eslint-parser": ["svelte-eslint-parser@1.6.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0", "semver": "^7.7.2" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw=="],
|
||||
|
||||
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
|
||||
|
||||
"tabbable": ["tabbable@6.3.0", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="],
|
||||
@@ -527,24 +700,38 @@
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"@auth/drizzle-adapter/@auth/core": ["@auth/core@0.41.1", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7.0.7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw=="],
|
||||
@@ -553,6 +740,10 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||
@@ -567,8 +758,16 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"postcss-load-config/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||
|
||||
"@auth/drizzle-adapter/@auth/core/jose": ["jose@6.1.2", "", {}, "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ=="],
|
||||
|
||||
"@auth/drizzle-adapter/@auth/core/oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="],
|
||||
@@ -628,5 +827,9 @@
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
volumes:
|
||||
- /mnt/HC_Volume_102830676/wishlist:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
PORT: 3000
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
AUTH_URL: ${AUTH_URL}
|
||||
AUTH_TRUST_HOST: "true"
|
||||
AUTH_TRUST_HOST: 'true'
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||
AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import { wishlists, items, user, savedWishlists, reservations, session, account } from "./schema";
|
||||
import { relations } from 'drizzle-orm/relations';
|
||||
import { wishlists, items, user, savedWishlists, reservations, session, account } from './schema';
|
||||
|
||||
export const itemsRelations = relations(items, ({ one, many }) => ({
|
||||
wishlist: one(wishlists, {
|
||||
fields: [items.wishlistId],
|
||||
references: [wishlists.id]
|
||||
}),
|
||||
reservations: many(reservations),
|
||||
reservations: many(reservations)
|
||||
}));
|
||||
|
||||
export const wishlistsRelations = relations(wishlists, ({ one, many }) => ({
|
||||
@@ -15,14 +15,14 @@ export const wishlistsRelations = relations(wishlists, ({one, many}) => ({
|
||||
fields: [wishlists.userId],
|
||||
references: [user.id]
|
||||
}),
|
||||
savedWishlists: many(savedWishlists),
|
||||
savedWishlists: many(savedWishlists)
|
||||
}));
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
wishlists: many(wishlists),
|
||||
savedWishlists: many(savedWishlists),
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
accounts: many(account)
|
||||
}));
|
||||
|
||||
export const savedWishlistsRelations = relations(savedWishlists, ({ one }) => ({
|
||||
@@ -33,26 +33,26 @@ export const savedWishlistsRelations = relations(savedWishlists, ({one}) => ({
|
||||
wishlist: one(wishlists, {
|
||||
fields: [savedWishlists.wishlistId],
|
||||
references: [wishlists.id]
|
||||
}),
|
||||
})
|
||||
}));
|
||||
|
||||
export const reservationsRelations = relations(reservations, ({ one }) => ({
|
||||
item: one(items, {
|
||||
fields: [reservations.itemId],
|
||||
references: [items.id]
|
||||
}),
|
||||
})
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id]
|
||||
}),
|
||||
})
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id]
|
||||
}),
|
||||
})
|
||||
}));
|
||||
@@ -1,74 +1,95 @@
|
||||
import { pgTable, foreignKey, text, numeric, boolean, timestamp, unique, primaryKey } from "drizzle-orm/pg-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
import {
|
||||
pgTable,
|
||||
foreignKey,
|
||||
text,
|
||||
numeric,
|
||||
boolean,
|
||||
timestamp,
|
||||
unique,
|
||||
primaryKey
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
|
||||
|
||||
export const items = pgTable("items", {
|
||||
export const items = pgTable(
|
||||
'items',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
wishlistId: text("wishlist_id").notNull(),
|
||||
wishlistId: text('wishlist_id').notNull(),
|
||||
title: text().notNull(),
|
||||
description: text(),
|
||||
link: text(),
|
||||
imageUrl: text("image_url"),
|
||||
imageUrl: text('image_url'),
|
||||
price: numeric({ precision: 10, scale: 2 }),
|
||||
currency: text().default('DKK'),
|
||||
color: text(),
|
||||
order: numeric().default('0').notNull(),
|
||||
isReserved: boolean("is_reserved").default(false).notNull(),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
isReserved: boolean('is_reserved').default(false).notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull()
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.wishlistId],
|
||||
foreignColumns: [wishlists.id],
|
||||
name: "items_wishlist_id_wishlists_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
]);
|
||||
name: 'items_wishlist_id_wishlists_id_fk'
|
||||
}).onDelete('cascade')
|
||||
]
|
||||
);
|
||||
|
||||
export const wishlists = pgTable("wishlists", {
|
||||
export const wishlists = pgTable(
|
||||
'wishlists',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
userId: text("user_id"),
|
||||
userId: text('user_id'),
|
||||
title: text().notNull(),
|
||||
description: text(),
|
||||
ownerToken: text("owner_token").notNull(),
|
||||
publicToken: text("public_token").notNull(),
|
||||
isFavorite: boolean("is_favorite").default(false).notNull(),
|
||||
ownerToken: text('owner_token').notNull(),
|
||||
publicToken: text('public_token').notNull(),
|
||||
isFavorite: boolean('is_favorite').default(false).notNull(),
|
||||
color: text(),
|
||||
endDate: timestamp("end_date", { mode: 'string' }),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
theme: text().default('none'),
|
||||
}, (table) => [
|
||||
endDate: timestamp('end_date', { mode: 'string' }),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
theme: text().default('none')
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: "wishlists_user_id_user_id_fk"
|
||||
}).onDelete("set null"),
|
||||
unique("wishlists_owner_token_unique").on(table.ownerToken),
|
||||
unique("wishlists_public_token_unique").on(table.publicToken),
|
||||
]);
|
||||
name: 'wishlists_user_id_user_id_fk'
|
||||
}).onDelete('set null'),
|
||||
unique('wishlists_owner_token_unique').on(table.ownerToken),
|
||||
unique('wishlists_public_token_unique').on(table.publicToken)
|
||||
]
|
||||
);
|
||||
|
||||
export const savedWishlists = pgTable("saved_wishlists", {
|
||||
export const savedWishlists = pgTable(
|
||||
'saved_wishlists',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
userId: text("user_id").notNull(),
|
||||
wishlistId: text("wishlist_id").notNull(),
|
||||
isFavorite: boolean("is_favorite").default(false).notNull(),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
ownerToken: text("owner_token"),
|
||||
}, (table) => [
|
||||
userId: text('user_id').notNull(),
|
||||
wishlistId: text('wishlist_id').notNull(),
|
||||
isFavorite: boolean('is_favorite').default(false).notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
ownerToken: text('owner_token')
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: "saved_wishlists_user_id_user_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
name: 'saved_wishlists_user_id_user_id_fk'
|
||||
}).onDelete('cascade'),
|
||||
foreignKey({
|
||||
columns: [table.wishlistId],
|
||||
foreignColumns: [wishlists.id],
|
||||
name: "saved_wishlists_wishlist_id_wishlists_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
]);
|
||||
name: 'saved_wishlists_wishlist_id_wishlists_id_fk'
|
||||
}).onDelete('cascade')
|
||||
]
|
||||
);
|
||||
|
||||
export const user = pgTable("user", {
|
||||
export const user = pgTable(
|
||||
'user',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
name: text(),
|
||||
email: text(),
|
||||
@@ -76,66 +97,90 @@ export const user = pgTable("user", {
|
||||
image: text(),
|
||||
password: text(),
|
||||
username: text(),
|
||||
dashboardTheme: text("dashboard_theme").default('none'),
|
||||
dashboardColor: text("dashboard_color"),
|
||||
lastLogin: timestamp("last_login", { mode: 'string' }),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
unique("user_email_unique").on(table.email),
|
||||
unique("user_username_unique").on(table.username),
|
||||
]);
|
||||
dashboardTheme: text('dashboard_theme').default('none'),
|
||||
dashboardColor: text('dashboard_color'),
|
||||
lastLogin: timestamp('last_login', { mode: 'string' }),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull()
|
||||
},
|
||||
(table) => [
|
||||
unique('user_email_unique').on(table.email),
|
||||
unique('user_username_unique').on(table.username)
|
||||
]
|
||||
);
|
||||
|
||||
export const reservations = pgTable("reservations", {
|
||||
export const reservations = pgTable(
|
||||
'reservations',
|
||||
{
|
||||
id: text().primaryKey().notNull(),
|
||||
itemId: text("item_id").notNull(),
|
||||
reserverName: text("reserver_name"),
|
||||
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||
}, (table) => [
|
||||
itemId: text('item_id').notNull(),
|
||||
reserverName: text('reserver_name'),
|
||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull()
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.itemId],
|
||||
foreignColumns: [items.id],
|
||||
name: "reservations_item_id_items_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
]);
|
||||
name: 'reservations_item_id_items_id_fk'
|
||||
}).onDelete('cascade')
|
||||
]
|
||||
);
|
||||
|
||||
export const session = pgTable("session", {
|
||||
export const session = pgTable(
|
||||
'session',
|
||||
{
|
||||
sessionToken: text().primaryKey().notNull(),
|
||||
userId: text().notNull(),
|
||||
expires: timestamp({ mode: 'string' }).notNull(),
|
||||
}, (table) => [
|
||||
expires: timestamp({ mode: 'string' }).notNull()
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: "session_userId_user_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
]);
|
||||
name: 'session_userId_user_id_fk'
|
||||
}).onDelete('cascade')
|
||||
]
|
||||
);
|
||||
|
||||
export const verificationToken = pgTable("verificationToken", {
|
||||
export const verificationToken = pgTable(
|
||||
'verificationToken',
|
||||
{
|
||||
identifier: text().notNull(),
|
||||
token: text().notNull(),
|
||||
expires: timestamp({ mode: 'string' }).notNull(),
|
||||
}, (table) => [
|
||||
primaryKey({ columns: [table.identifier, table.token], name: "verificationToken_identifier_token_pk"}),
|
||||
]);
|
||||
expires: timestamp({ mode: 'string' }).notNull()
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
columns: [table.identifier, table.token],
|
||||
name: 'verificationToken_identifier_token_pk'
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
export const account = pgTable("account", {
|
||||
export const account = pgTable(
|
||||
'account',
|
||||
{
|
||||
userId: text().notNull(),
|
||||
type: text().notNull(),
|
||||
provider: text().notNull(),
|
||||
providerAccountId: text().notNull(),
|
||||
refreshToken: text("refresh_token"),
|
||||
accessToken: text("access_token"),
|
||||
expiresAt: numeric("expires_at"),
|
||||
tokenType: text("token_type"),
|
||||
refreshToken: text('refresh_token'),
|
||||
accessToken: text('access_token'),
|
||||
expiresAt: numeric('expires_at'),
|
||||
tokenType: text('token_type'),
|
||||
scope: text(),
|
||||
idToken: text("id_token"),
|
||||
sessionState: text("session_state"),
|
||||
}, (table) => [
|
||||
idToken: text('id_token'),
|
||||
sessionState: text('session_state')
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.userId],
|
||||
foreignColumns: [user.id],
|
||||
name: "account_userId_user_id_fk"
|
||||
}).onDelete("cascade"),
|
||||
primaryKey({ columns: [table.provider, table.providerAccountId], name: "account_provider_providerAccountId_pk"}),
|
||||
]);
|
||||
name: 'account_userId_user_id_fk'
|
||||
}).onDelete('cascade'),
|
||||
primaryKey({
|
||||
columns: [table.provider, table.providerAccountId],
|
||||
name: 'account_provider_providerAccountId_pk'
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"bits-ui": "^2.14.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
|
||||
"postgres": "^3.4.7",
|
||||
"svelte-dnd-action": "^0.9.67",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import "tw-animate-css";
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<script>
|
||||
(function () {
|
||||
const theme = localStorage.getItem('theme') || 'system';
|
||||
const isDark = theme === 'dark' ||
|
||||
const isDark =
|
||||
theme === 'dark' ||
|
||||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
|
||||
@@ -81,10 +81,7 @@ const authConfig: SvelteKitAuthConfig = {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValidPassword = await bcrypt.compare(
|
||||
credentials.password as string,
|
||||
user.password
|
||||
);
|
||||
const isValidPassword = await bcrypt.compare(credentials.password as string, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
@@ -105,9 +102,7 @@ const authConfig: SvelteKitAuthConfig = {
|
||||
callbacks: {
|
||||
async signIn({ user }) {
|
||||
if (user?.id) {
|
||||
await db.update(users)
|
||||
.set({ lastLogin: new Date() })
|
||||
.where(eq(users.id, user.id));
|
||||
await db.update(users).set({ lastLogin: new Date() }).where(eq(users.id, user.id));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import WishlistSection from '$lib/components/dashboard/WishlistSection.svelte';
|
||||
import { getLocalWishlists, forgetLocalWishlist, toggleLocalFavorite, type LocalWishlist } from '$lib/utils/localWishlists';
|
||||
import {
|
||||
getLocalWishlists,
|
||||
forgetLocalWishlist,
|
||||
toggleLocalFavorite,
|
||||
type LocalWishlist
|
||||
} from '$lib/utils/localWishlists';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import { Star } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -101,31 +106,33 @@
|
||||
// Description depends on authentication status
|
||||
const sectionDescription = $derived(() => {
|
||||
if (isAuthenticated) {
|
||||
return t.dashboard.localWishlistsAuthDescription || "Wishlists stored in your browser that haven't been claimed yet.";
|
||||
return (
|
||||
t.dashboard.localWishlistsAuthDescription ||
|
||||
"Wishlists stored in your browser that haven't been claimed yet."
|
||||
);
|
||||
}
|
||||
return t.dashboard.localWishlistsDescription || "Wishlists stored in your browser. Sign in to save them permanently.";
|
||||
return (
|
||||
t.dashboard.localWishlistsDescription ||
|
||||
'Wishlists stored in your browser. Sign in to save them permanently.'
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<WishlistSection
|
||||
title={t.dashboard.localWishlists || "Local Wishlists"}
|
||||
title={t.dashboard.localWishlists || 'Local Wishlists'}
|
||||
description={sectionDescription()}
|
||||
items={transformedWishlists()}
|
||||
emptyMessage={t.dashboard.emptyLocalWishlists || "No local wishlists yet"}
|
||||
emptyActionLabel={t.dashboard.createLocalWishlist || "Create local wishlist"}
|
||||
emptyMessage={t.dashboard.emptyLocalWishlists || 'No local wishlists yet'}
|
||||
emptyActionLabel={t.dashboard.createLocalWishlist || 'Create local wishlist'}
|
||||
emptyActionHref="/"
|
||||
showCreateButton={true}
|
||||
fallbackColor={fallbackColor}
|
||||
fallbackTheme={fallbackTheme}
|
||||
{fallbackColor}
|
||||
{fallbackTheme}
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onclick={() => handleToggleFavorite(wishlist.ownerToken)}
|
||||
>
|
||||
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||
<Button size="sm" variant="outline" onclick={() => handleToggleFavorite(wishlist.ownerToken)}>
|
||||
<Star class={wishlist.isFavorite ? 'fill-yellow-500 text-yellow-500' : ''} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -145,12 +152,8 @@
|
||||
{t.dashboard.copyLink}
|
||||
</Button>
|
||||
{#if unlocked}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onclick={() => handleForget(wishlist.ownerToken)}
|
||||
>
|
||||
{t.dashboard.forget || "Forget"}
|
||||
<Button size="sm" variant="destructive" onclick={() => handleForget(wishlist.ownerToken)}>
|
||||
{t.dashboard.forget || 'Forget'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import EmptyState from '$lib/components/layout/EmptyState.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
const filteredItems = $derived(() => {
|
||||
if (!searchQuery.trim()) return items;
|
||||
|
||||
return items.filter(item => {
|
||||
return items.filter((item) => {
|
||||
const title = item.title || item.wishlist?.title || '';
|
||||
const description = item.description || item.wishlist?.description || '';
|
||||
const query = searchQuery.toLowerCase();
|
||||
@@ -89,7 +89,11 @@
|
||||
function formatEndDate(date: Date | string | null): string | null {
|
||||
if (!date) return null;
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString(languageStore.t.date.format.short, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
return d.toLocaleDateString(languageStore.t.date.format.short, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function getWishlistDescription(item: any): string | null {
|
||||
@@ -156,8 +160,8 @@
|
||||
itemCount={wishlist.items?.length || 0}
|
||||
color={wishlist.color}
|
||||
theme={wishlist.theme}
|
||||
fallbackColor={fallbackColor}
|
||||
fallbackTheme={fallbackTheme}
|
||||
{fallbackColor}
|
||||
{fallbackTheme}
|
||||
>
|
||||
{@render actions(item, unlocked)}
|
||||
</WishlistCard>
|
||||
|
||||
@@ -73,9 +73,13 @@
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-3xl font-bold">{t.nav.dashboard}</h1>
|
||||
{#if isAuthenticated}
|
||||
<p class="text-muted-foreground truncate">{t.dashboard.welcomeBack}, {userName || userEmail}</p>
|
||||
<p class="text-muted-foreground truncate">
|
||||
{t.dashboard.welcomeBack}, {userName || userEmail}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">{t.dashboard.anonymousDashboard || "Your local wishlists"}</p>
|
||||
<p class="text-muted-foreground">
|
||||
{t.dashboard.anonymousDashboard || 'Your local wishlists'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
||||
@@ -84,9 +88,13 @@
|
||||
<LanguageToggle color={localColor} />
|
||||
<ThemeToggle />
|
||||
{#if isAuthenticated}
|
||||
<Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}>{t.auth.signOut}</Button>
|
||||
<Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}
|
||||
>{t.auth.signOut}</Button
|
||||
>
|
||||
{:else}
|
||||
<Button variant="outline" onclick={() => (window.location.href = '/signin')}>{t.auth.signIn}</Button>
|
||||
<Button variant="outline" onclick={() => (window.location.href = '/signin')}
|
||||
>{t.auth.signIn}</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,22 @@
|
||||
|
||||
<nav class="flex items-center gap-1 sm:gap-2 mb-6 w-full">
|
||||
{#if isAuthenticated}
|
||||
<Button variant="outline" size="sm" onclick={() => (window.location.href = '/dashboard')} class="px-2 sm:px-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => (window.location.href = '/dashboard')}
|
||||
class="px-2 sm:px-3"
|
||||
>
|
||||
<LayoutDashboard class="w-4 h-4" />
|
||||
<span class="hidden sm:inline sm:ml-2">{t.nav.dashboard}</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="outline" size="sm" onclick={() => (window.location.href = '/signin')} class="px-2 sm:px-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => (window.location.href = '/signin')}
|
||||
class="px-2 sm:px-3"
|
||||
>
|
||||
{t.auth.signIn}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -40,13 +40,7 @@
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{#if color}
|
||||
<IconButton
|
||||
onclick={clearColor}
|
||||
{color}
|
||||
{size}
|
||||
aria-label="Clear color"
|
||||
rounded="md"
|
||||
>
|
||||
<IconButton onclick={clearColor} {color} {size} aria-label="Clear color" rounded="md">
|
||||
<X class={iconSize} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
@@ -54,7 +48,10 @@
|
||||
class="{buttonSize} flex items-center justify-center rounded-md border border-input hover:opacity-90 transition-opacity cursor-pointer relative overflow-hidden"
|
||||
style={color ? `background-color: ${color};` : ''}
|
||||
>
|
||||
<Pencil class="{iconSize} relative z-10 pointer-events-none" style={color ? 'color: white; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));' : ''} />
|
||||
<Pencil
|
||||
class="{iconSize} relative z-10 pointer-events-none"
|
||||
style={color ? 'color: white; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));' : ''}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={color || '#ffffff'}
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
lg: 'rounded-lg'
|
||||
};
|
||||
|
||||
const baseClasses = 'flex items-center justify-center border border-input transition-colors backdrop-blur';
|
||||
const baseClasses =
|
||||
'flex items-center justify-center border border-input transition-colors backdrop-blur';
|
||||
const sizeClass = sizeClasses[size];
|
||||
const roundedClass = roundedClasses[rounded];
|
||||
</script>
|
||||
|
||||
@@ -11,8 +11,4 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
type="search"
|
||||
{placeholder}
|
||||
bind:value
|
||||
/>
|
||||
<Input type="search" {placeholder} bind:value />
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
onclick={handleClick}
|
||||
variant={unlocked ? "default" : "outline"}
|
||||
>
|
||||
<Button onclick={handleClick} variant={unlocked ? 'default' : 'outline'}>
|
||||
{#if unlocked}
|
||||
<Lock class="mr-2 h-4 w-4" />
|
||||
{t.wishlist.lockDeletion}
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
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 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*="size-"])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
|
||||
outline:
|
||||
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
let { class: className, children, ...restProps }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<div class={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</script>
|
||||
|
||||
<input
|
||||
type={type}
|
||||
{type}
|
||||
class={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
let {
|
||||
color = $bindable(null),
|
||||
size = 'sm',
|
||||
size = 'sm'
|
||||
}: {
|
||||
color: string | null;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
|
||||
@@ -114,7 +114,11 @@
|
||||
bind:value={imageUrl}
|
||||
/>
|
||||
|
||||
<ImageSelector images={scrapedImages} bind:selectedImage={imageUrl} isLoading={isLoadingImages} />
|
||||
<ImageSelector
|
||||
images={scrapedImages}
|
||||
bind:selectedImage={imageUrl}
|
||||
isLoading={isLoadingImages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
@@ -138,7 +142,7 @@
|
||||
<div class="md:col-span-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="color">{t.form.cardColor}</Label>
|
||||
<ColorPicker bind:color={color} />
|
||||
<ColorPicker bind:color />
|
||||
</div>
|
||||
<input type="hidden" name="color" value={color || ''} />
|
||||
</div>
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
{#if isAuthenticated}
|
||||
<div class="mb-6">
|
||||
{#if isOwner}
|
||||
<Button
|
||||
disabled
|
||||
variant="outline"
|
||||
class="w-full md:w-auto opacity-60 cursor-not-allowed"
|
||||
>
|
||||
<Button disabled variant="outline" class="w-full md:w-auto opacity-60 cursor-not-allowed">
|
||||
{t.wishlist.youOwnThis}
|
||||
</Button>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
@@ -38,19 +34,15 @@
|
||||
{:else}
|
||||
<form
|
||||
method="POST"
|
||||
action={hasClaimed ? "?/unclaimWishlist" : "?/claimWishlist"}
|
||||
action={hasClaimed ? '?/unclaimWishlist' : '?/claimWishlist'}
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={hasClaimed ? "outline" : "default"}
|
||||
class="w-full md:w-auto"
|
||||
>
|
||||
{hasClaimed ? "Unclaim Wishlist" : "Claim Wishlist"}
|
||||
<Button type="submit" variant={hasClaimed ? 'outline' : 'default'} class="w-full md:w-auto">
|
||||
{hasClaimed ? 'Unclaim Wishlist' : 'Claim Wishlist'}
|
||||
</Button>
|
||||
</form>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
@@ -60,7 +52,9 @@
|
||||
Claim this wishlist to add it to your dashboard for easy access.
|
||||
{#if isLocal}
|
||||
<br />
|
||||
<span class="text-xs">It will remain in your local wishlists and also appear in your claimed wishlists.</span>
|
||||
<span class="text-xs"
|
||||
>It will remain in your local wishlists and also appear in your claimed wishlists.</span
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
@@ -27,17 +27,13 @@
|
||||
return;
|
||||
}
|
||||
return async ({ result }) => {
|
||||
if (result.type === "success") {
|
||||
window.location.href = "/dashboard";
|
||||
if (result.type === 'success') {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
class="w-full md:w-auto"
|
||||
>
|
||||
<Button type="submit" variant="destructive" class="w-full md:w-auto">
|
||||
{t.wishlist.deleteWishlist}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -24,7 +24,17 @@
|
||||
wishlistTheme?: string | null;
|
||||
}
|
||||
|
||||
let { item, onSuccess, onCancel, onColorChange, currentPosition = 1, totalItems = 1, onPositionChange, wishlistColor = null, wishlistTheme = null }: Props = $props();
|
||||
let {
|
||||
item,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
onColorChange,
|
||||
currentPosition = 1,
|
||||
totalItems = 1,
|
||||
onPositionChange,
|
||||
wishlistColor = null,
|
||||
wishlistTheme = null
|
||||
}: Props = $props();
|
||||
|
||||
const cardStyle = $derived(getCardStyle(wishlistColor, null));
|
||||
|
||||
@@ -88,7 +98,13 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<Label for="title">{t.form.wishName} ({t.form.required})</Label>
|
||||
<Input id="title" name="title" required value={item.title} placeholder="e.g., Blue Headphones" />
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
required
|
||||
value={item.title}
|
||||
placeholder="e.g., Blue Headphones"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
@@ -124,12 +140,23 @@
|
||||
bind:value={imageUrl}
|
||||
/>
|
||||
|
||||
<ImageSelector images={scrapedImages} bind:selectedImage={imageUrl} isLoading={isLoadingImages} />
|
||||
<ImageSelector
|
||||
images={scrapedImages}
|
||||
bind:selectedImage={imageUrl}
|
||||
isLoading={isLoadingImages}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="price">{t.form.price}</Label>
|
||||
<Input id="price" name="price" type="number" step="0.01" value={item.price || ''} placeholder="0.00" />
|
||||
<Input
|
||||
id="price"
|
||||
name="price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={item.price || ''}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
@@ -148,7 +175,7 @@
|
||||
<div class="md:col-span-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="color">{t.form.cardColor}</Label>
|
||||
<ColorPicker bind:color={color} onchange={() => onColorChange?.(item.id, color || '')} />
|
||||
<ColorPicker bind:color onchange={() => onColorChange?.(item.id, color || '')} />
|
||||
</div>
|
||||
<input type="hidden" name="color" value={color || ''} />
|
||||
</div>
|
||||
@@ -178,7 +205,9 @@
|
||||
<div class="flex gap-2">
|
||||
<Button type="submit" class="flex-1 md:flex-none">{t.form.saveChanges}</Button>
|
||||
{#if onCancel}
|
||||
<Button type="button" variant="outline" class="flex-1 md:flex-none" onclick={onCancel}>{t.form.cancel}</Button>
|
||||
<Button type="button" variant="outline" class="flex-1 md:flex-none" onclick={onCancel}
|
||||
>{t.form.cancel}</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import WishlistItem from "$lib/components/wishlist/WishlistItem.svelte";
|
||||
import EmptyState from "$lib/components/layout/EmptyState.svelte";
|
||||
import type { Item } from "$lib/server/schema";
|
||||
import { enhance } from "$app/forms";
|
||||
import { flip } from "svelte/animate";
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent } from '$lib/components/ui/card';
|
||||
import WishlistItem from '$lib/components/wishlist/WishlistItem.svelte';
|
||||
import EmptyState from '$lib/components/layout/EmptyState.svelte';
|
||||
import type { Item } from '$lib/server/schema';
|
||||
import { enhance } from '$app/forms';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
|
||||
import { getCardStyle } from "$lib/utils/colors";
|
||||
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
|
||||
let {
|
||||
items = $bindable([]),
|
||||
@@ -46,21 +46,9 @@
|
||||
{t.wishlist.edit}
|
||||
</Button>
|
||||
{#if rearranging}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteItem"
|
||||
use:enhance
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="itemId"
|
||||
value={item.id}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
>
|
||||
<form method="POST" action="?/deleteItem" use:enhance>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<Button type="submit" variant="destructive" size="sm">
|
||||
{t.form.delete}
|
||||
</Button>
|
||||
</form>
|
||||
@@ -74,9 +62,7 @@
|
||||
<Card style={cardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={theme} color={wishlistColor} showPattern={false} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"}
|
||||
/>
|
||||
<EmptyState message={t.wishlist.noWishes + '. ' + t.wishlist.addFirstWish + '!'} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
@@ -39,20 +39,20 @@
|
||||
{#if canCancel()}
|
||||
{#if showCancelConfirmation}
|
||||
<div class="flex flex-col gap-2 items-start">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Cancel this reservation?
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">Cancel this reservation?</p>
|
||||
<div class="flex gap-2">
|
||||
<form method="POST" action="?/unreserve" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/unreserve"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showCancelConfirmation = false;
|
||||
await update();
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="itemId" value={itemId} />
|
||||
<Button type="submit" variant="destructive" size="sm">
|
||||
Yes, Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="destructive" size="sm">Yes, Cancel</Button>
|
||||
</form>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -76,9 +76,7 @@
|
||||
{:else}
|
||||
<form method="POST" action="?/unreserve" use:enhance>
|
||||
<input type="hidden" name="itemId" value={itemId} />
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
Cancel Reservation
|
||||
</Button>
|
||||
<Button type="submit" variant="outline" size="sm">Cancel Reservation</Button>
|
||||
</form>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
const publicLink = $derived(
|
||||
typeof window !== 'undefined' ? `${window.location.origin}${publicUrl}` : ''
|
||||
);
|
||||
const ownerLink = $derived(ownerUrl && typeof window !== 'undefined' ? `${window.location.origin}${ownerUrl}` : '');
|
||||
const ownerLink = $derived(
|
||||
ownerUrl && typeof window !== 'undefined' ? `${window.location.origin}${ownerUrl}` : ''
|
||||
);
|
||||
|
||||
async function copyToClipboard(text: string, type: 'public' | 'owner') {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
|
||||
let {
|
||||
@@ -16,10 +16,7 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<Button
|
||||
onclick={onToggleAddForm}
|
||||
class="w-full md:w-auto"
|
||||
>
|
||||
<Button onclick={onToggleAddForm} class="w-full md:w-auto">
|
||||
{showAddForm ? t.form.cancel : t.wishlist.addWish}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { Pencil, Check, X } from "@lucide/svelte";
|
||||
import ColorPicker from "$lib/components/ui/ColorPicker.svelte";
|
||||
import ThemePicker from "$lib/components/ui/theme-picker.svelte";
|
||||
import IconButton from "$lib/components/ui/IconButton.svelte";
|
||||
import type { Wishlist } from "$lib/db/schema";
|
||||
import { Card, CardContent } from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Pencil, Check, X } from '@lucide/svelte';
|
||||
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
||||
import ThemePicker from '$lib/components/ui/theme-picker.svelte';
|
||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||
import type { Wishlist } from '$lib/db/schema';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
|
||||
@@ -32,13 +32,11 @@
|
||||
let editingTitle = $state(false);
|
||||
let editingDescription = $state(false);
|
||||
let wishlistTitle = $state(wishlist.title);
|
||||
let wishlistDescription = $state(wishlist.description || "");
|
||||
let wishlistDescription = $state(wishlist.description || '');
|
||||
let wishlistColor = $state<string | null>(wishlist.color);
|
||||
let wishlistTheme = $state<string>(wishlist.theme || 'none');
|
||||
let wishlistEndDate = $state<string | null>(
|
||||
wishlist.endDate
|
||||
? new Date(wishlist.endDate).toISOString().split("T")[0]
|
||||
: null,
|
||||
wishlist.endDate ? new Date(wishlist.endDate).toISOString().split('T')[0] : null
|
||||
);
|
||||
|
||||
const cardStyle = $derived(getCardStyle(null, wishlistColor));
|
||||
@@ -64,7 +62,7 @@
|
||||
if (success) {
|
||||
editingDescription = false;
|
||||
} else {
|
||||
wishlistDescription = wishlist.description || "";
|
||||
wishlistDescription = wishlist.description || '';
|
||||
editingDescription = false;
|
||||
}
|
||||
}
|
||||
@@ -88,9 +86,9 @@
|
||||
bind:value={wishlistTitle}
|
||||
class="text-3xl font-bold h-auto py-0 leading-[2.25rem]"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter') {
|
||||
saveTitle();
|
||||
} else if (e.key === "Escape") {
|
||||
} else if (e.key === 'Escape') {
|
||||
wishlistTitle = wishlist.title;
|
||||
editingTitle = false;
|
||||
}
|
||||
@@ -111,7 +109,7 @@
|
||||
color={wishlistColor}
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
aria-label={editingTitle ? "Save title" : "Edit title"}
|
||||
aria-label={editingTitle ? 'Save title' : 'Edit title'}
|
||||
>
|
||||
{#if editingTitle}
|
||||
<Check class="w-4 h-4" />
|
||||
@@ -155,7 +153,7 @@
|
||||
color={wishlistColor}
|
||||
size="sm"
|
||||
class="flex-shrink-0"
|
||||
aria-label={editingDescription ? "Save description" : "Edit description"}
|
||||
aria-label={editingDescription ? 'Save description' : 'Edit description'}
|
||||
>
|
||||
{#if editingDescription}
|
||||
<Check class="w-4 h-4" />
|
||||
@@ -171,15 +169,17 @@
|
||||
class="w-full"
|
||||
rows={3}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
wishlistDescription = wishlist.description || "";
|
||||
if (e.key === 'Escape') {
|
||||
wishlistDescription = wishlist.description || '';
|
||||
editingDescription = false;
|
||||
}
|
||||
}}
|
||||
autofocus
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-full py-2 px-3 rounded-md border border-input bg-transparent text-sm min-h-[80px]">
|
||||
<div
|
||||
class="w-full py-2 px-3 rounded-md border border-input bg-transparent text-sm min-h-[80px]"
|
||||
>
|
||||
{wishlistDescription || t.form.noDescription}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -202,7 +202,7 @@
|
||||
<Input
|
||||
id="wishlist-end-date"
|
||||
type="date"
|
||||
value={wishlistEndDate || ""}
|
||||
value={wishlistEndDate || ''}
|
||||
onchange={handleEndDateChange}
|
||||
class="w-full sm:w-auto"
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardContent } from "$lib/components/ui/card";
|
||||
import type { Item } from "$lib/db/schema";
|
||||
import { GripVertical, ExternalLink } from "@lucide/svelte";
|
||||
import { Card, CardContent } from '$lib/components/ui/card';
|
||||
import type { Item } from '$lib/db/schema';
|
||||
import { GripVertical, ExternalLink } from '@lucide/svelte';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||
|
||||
@@ -28,24 +28,21 @@
|
||||
const t = $derived(languageStore.t);
|
||||
|
||||
const currencySymbols: Record<string, string> = {
|
||||
DKK: "kr",
|
||||
EUR: "€",
|
||||
USD: "$",
|
||||
SEK: "kr",
|
||||
NOK: "kr",
|
||||
GBP: "£",
|
||||
DKK: 'kr',
|
||||
EUR: '€',
|
||||
USD: '$',
|
||||
SEK: 'kr',
|
||||
NOK: 'kr',
|
||||
GBP: '£'
|
||||
};
|
||||
|
||||
function formatPrice(
|
||||
price: string | null,
|
||||
currency: string | null,
|
||||
): string {
|
||||
if (!price) return "";
|
||||
const symbol = currency ? currencySymbols[currency] || currency : "kr";
|
||||
function formatPrice(price: string | null, currency: string | null): string {
|
||||
if (!price) return '';
|
||||
const symbol = currency ? currencySymbols[currency] || currency : 'kr';
|
||||
const amount = parseFloat(price).toFixed(2);
|
||||
|
||||
// For Danish, Swedish, Norwegian kroner, put symbol after the amount
|
||||
if (currency && ["DKK", "SEK", "NOK"].includes(currency)) {
|
||||
if (currency && ['DKK', 'SEK', 'NOK'].includes(currency)) {
|
||||
return `${amount} ${symbol}`;
|
||||
}
|
||||
|
||||
@@ -78,7 +75,7 @@
|
||||
src="/api/image-proxy?url={encodeURIComponent(item.imageUrl)}"
|
||||
alt={item.title}
|
||||
class="w-full md:w-32 h-32 object-cover rounded-lg"
|
||||
onerror={(e) => e.currentTarget.src = item.imageUrl}
|
||||
onerror={(e) => (e.currentTarget.src = item.imageUrl)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -88,14 +85,17 @@
|
||||
</div>
|
||||
|
||||
{#if item.description}
|
||||
<p class="text-muted-foreground break-words whitespace-pre-wrap" style="overflow-wrap: anywhere;">{item.description}</p>
|
||||
<p
|
||||
class="text-muted-foreground break-words whitespace-pre-wrap"
|
||||
style="overflow-wrap: anywhere;"
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap gap-2 items-center text-sm mt-2">
|
||||
{#if item.price}
|
||||
<span class="font-medium"
|
||||
>{formatPrice(item.price, item.currency)}</span
|
||||
>
|
||||
<span class="font-medium">{formatPrice(item.price, item.currency)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ export const verificationTokens = pgTable(
|
||||
);
|
||||
|
||||
export const wishlists = pgTable('wishlists', {
|
||||
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
title: text('title').notNull(),
|
||||
description: text('description'),
|
||||
@@ -91,7 +93,9 @@ export const wishlistsRelations = relations(wishlists, ({ one, many }) => ({
|
||||
}));
|
||||
|
||||
export const items = pgTable('items', {
|
||||
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
wishlistId: text('wishlist_id')
|
||||
.notNull()
|
||||
.references(() => wishlists.id, { onDelete: 'cascade' }),
|
||||
@@ -117,7 +121,9 @@ export const itemsRelations = relations(items, ({ one, many }) => ({
|
||||
}));
|
||||
|
||||
export const reservations = pgTable('reservations', {
|
||||
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
itemId: text('item_id')
|
||||
.notNull()
|
||||
.references(() => items.id, { onDelete: 'cascade' }),
|
||||
@@ -138,7 +144,9 @@ export const reservationsRelations = relations(reservations, ({ one }) => ({
|
||||
}));
|
||||
|
||||
export const savedWishlists = pgTable('saved_wishlists', {
|
||||
id: text('id').primaryKey().$defaultFn(() => createId()),
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId()),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
|
||||
@@ -25,9 +25,11 @@ export const da: Translation = {
|
||||
emptyWishlists: 'Du har ikke oprettet nogen ønskelister endnu.',
|
||||
emptyWishlistsAction: 'Opret Din Første Ønskeliste',
|
||||
emptyClaimedWishlists: 'Du har ikke taget ejerskab af nogen ønskelister endnu.',
|
||||
emptyClaimedWishlistsDescription: 'Når nogen deler et redigeringslink med dig, kan du tage ejerskab af det for at administrere det fra dit dashboard.',
|
||||
emptyClaimedWishlistsDescription:
|
||||
'Når nogen deler et redigeringslink med dig, kan du tage ejerskab af det for at administrere det fra dit dashboard.',
|
||||
emptySavedWishlists: 'Du har ikke gemt nogen ønskelister endnu.',
|
||||
emptySavedWishlistsDescription: 'Når du ser en andens ønskeliste, kan du gemme den for nemt at finde den senere.',
|
||||
emptySavedWishlistsDescription:
|
||||
'Når du ser en andens ønskeliste, kan du gemme den for nemt at finde den senere.',
|
||||
by: 'af',
|
||||
ends: 'Slutter',
|
||||
welcomeBack: 'Velkommen tilbage',
|
||||
@@ -63,9 +65,11 @@ export const da: Translation = {
|
||||
alreadyInDashboard: 'Denne ønskeliste er allerede it dit dashboard.',
|
||||
alreadyClaimed: 'Denne ønskeliste er allerede i dit dashboard som en ejet ønskeliste.',
|
||||
claimDescription: 'Tag ejerskab af denne ønskeliste for at tilføje den til dit dashboard',
|
||||
claimedDescription: 'Du har taget ejerskab af denne ønskeliste og kan tilgå den fra dit dashboard',
|
||||
claimedDescription:
|
||||
'Du har taget ejerskab af denne ønskeliste og kan tilgå den fra dit dashboard',
|
||||
deleteWishlist: 'Slet Ønskeliste',
|
||||
deleteConfirm: 'Er du sikker på, at du vil slette denne ønskeliste? Denne handling kan ikke fortrydes.',
|
||||
deleteConfirm:
|
||||
'Er du sikker på, at du vil slette denne ønskeliste? Denne handling kan ikke fortrydes.',
|
||||
lockDeletion: 'Lås Sletning',
|
||||
unlockDeletion: 'Lås Op for Sletning',
|
||||
shareViewOnly: 'Del med venner (afslører reservationer)',
|
||||
|
||||
@@ -22,9 +22,11 @@ export const en = {
|
||||
emptyWishlists: "You haven't created any wishlists yet.",
|
||||
emptyWishlistsAction: 'Create Your First Wishlist',
|
||||
emptyClaimedWishlists: "You haven't claimed any wishlists yet.",
|
||||
emptyClaimedWishlistsDescription: "When someone shares an edit link with you, you can claim it to manage it from your dashboard.",
|
||||
emptyClaimedWishlistsDescription:
|
||||
'When someone shares an edit link with you, you can claim it to manage it from your dashboard.',
|
||||
emptySavedWishlists: "You haven't saved any wishlists yet.",
|
||||
emptySavedWishlistsDescription: "When viewing someone's wishlist, you can save it to easily find it later.",
|
||||
emptySavedWishlistsDescription:
|
||||
"When viewing someone's wishlist, you can save it to easily find it later.",
|
||||
by: 'by',
|
||||
ends: 'Ends',
|
||||
welcomeBack: 'Welcome back',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export function sanitizeString(input: string | null | undefined, maxLength: number = 1000): string | null {
|
||||
export function sanitizeString(
|
||||
input: string | null | undefined,
|
||||
maxLength: number = 1000
|
||||
): string | null {
|
||||
if (input === null || input === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -16,7 +19,10 @@ export function sanitizeUrl(url: string | null | undefined): string | null {
|
||||
return url.trim();
|
||||
}
|
||||
|
||||
export function sanitizeText(input: string | null | undefined, maxLength: number = 10000): string | null {
|
||||
export function sanitizeText(
|
||||
input: string | null | undefined,
|
||||
maxLength: number = 10000
|
||||
): string | null {
|
||||
if (input === null || input === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ class ThemeStore {
|
||||
private applyTheme() {
|
||||
if (!browser) return;
|
||||
|
||||
const isDark = this.current === 'dark' ||
|
||||
const isDark =
|
||||
this.current === 'dark' ||
|
||||
(this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
this.resolved = isDark ? 'dark' : 'light';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
||||
@@ -36,7 +36,7 @@ export function addLocalWishlist(wishlist: LocalWishlist): void {
|
||||
try {
|
||||
const wishlists = getLocalWishlists();
|
||||
|
||||
const exists = wishlists.some(w => w.ownerToken === wishlist.ownerToken);
|
||||
const exists = wishlists.some((w) => w.ownerToken === wishlist.ownerToken);
|
||||
if (exists) return;
|
||||
|
||||
wishlists.push(wishlist);
|
||||
@@ -54,7 +54,7 @@ export function forgetLocalWishlist(ownerToken: string): void {
|
||||
|
||||
try {
|
||||
const wishlists = getLocalWishlists();
|
||||
const filtered = wishlists.filter(w => w.ownerToken !== ownerToken);
|
||||
const filtered = wishlists.filter((w) => w.ownerToken !== ownerToken);
|
||||
localStorage.setItem(LOCAL_WISHLISTS_KEY, JSON.stringify(filtered));
|
||||
} catch (error) {
|
||||
console.error('Failed to forget local wishlist:', error);
|
||||
@@ -79,7 +79,7 @@ export function clearLocalWishlists(): void {
|
||||
*/
|
||||
export function isLocalWishlist(ownerToken: string): boolean {
|
||||
const wishlists = getLocalWishlists();
|
||||
return wishlists.some(w => w.ownerToken === ownerToken);
|
||||
return wishlists.some((w) => w.ownerToken === ownerToken);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,10 +90,8 @@ export function toggleLocalFavorite(ownerToken: string): void {
|
||||
|
||||
try {
|
||||
const wishlists = getLocalWishlists();
|
||||
const updated = wishlists.map(w =>
|
||||
w.ownerToken === ownerToken
|
||||
? { ...w, isFavorite: !w.isFavorite }
|
||||
: w
|
||||
const updated = wishlists.map((w) =>
|
||||
w.ownerToken === ownerToken ? { ...w, isFavorite: !w.isFavorite } : w
|
||||
);
|
||||
localStorage.setItem(LOCAL_WISHLISTS_KEY, JSON.stringify(updated));
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,12 +7,12 @@ type UpdateField = {
|
||||
};
|
||||
|
||||
async function updateWishlist(field: UpdateField): Promise<boolean> {
|
||||
const response = await fetch("?/updateWishlist", {
|
||||
method: "POST",
|
||||
const response = await fetch('?/updateWishlist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams(field),
|
||||
body: new URLSearchParams(field)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -27,34 +27,34 @@ export async function updateTitle(title: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function updateDescription(description: string | null): Promise<boolean> {
|
||||
return updateWishlist({ description: description || "" });
|
||||
return updateWishlist({ description: description || '' });
|
||||
}
|
||||
|
||||
export async function updateColor(color: string | null): Promise<boolean> {
|
||||
return updateWishlist({ color: color || "" });
|
||||
return updateWishlist({ color: color || '' });
|
||||
}
|
||||
|
||||
export async function updateEndDate(endDate: string | null): Promise<boolean> {
|
||||
return updateWishlist({ endDate: endDate || "" });
|
||||
return updateWishlist({ endDate: endDate || '' });
|
||||
}
|
||||
|
||||
export async function updateTheme(theme: string | null): Promise<boolean> {
|
||||
return updateWishlist({ theme: theme || "none" });
|
||||
return updateWishlist({ theme: theme || 'none' });
|
||||
}
|
||||
|
||||
export async function reorderItems(items: Array<{ id: string; order: number }>): Promise<boolean> {
|
||||
const response = await fetch("?/reorderItems", {
|
||||
method: "POST",
|
||||
const response = await fetch('?/reorderItems', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
items: JSON.stringify(items),
|
||||
}),
|
||||
items: JSON.stringify(items)
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to update item order");
|
||||
console.error('Failed to update item order');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
||||
import { LanguageToggle } from '$lib/components/ui/language-toggle';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -77,7 +83,13 @@
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onsubmit={(e) => { e.preventDefault(); createWishlist(); }} class="space-y-4">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createWishlist();
|
||||
}}
|
||||
class="space-y-4"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<Label for="title">{t.form.wishlistTitle}</Label>
|
||||
<Input
|
||||
@@ -99,7 +111,7 @@
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="color">{t.form.wishlistColor}</Label>
|
||||
<ColorPicker bind:color={color} size="sm" />
|
||||
<ColorPicker bind:color size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" class="w-full" disabled={isCreating || !title.trim()}>
|
||||
|
||||
@@ -33,9 +33,9 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': new URL(imageUrl).origin,
|
||||
Referer: new URL(imageUrl).origin,
|
||||
'Sec-Fetch-Dest': 'image',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site'
|
||||
|
||||
@@ -33,11 +33,12 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
Pragma: 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,12 +68,30 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
function isLikelyProductImage(url: string): boolean {
|
||||
const lower = url.toLowerCase();
|
||||
const badPatterns = [
|
||||
'logo', 'icon', 'sprite', 'favicon', 'banner', 'footer',
|
||||
'header', 'background', 'pattern', 'placeholder', 'thumbnail-small',
|
||||
'btn', 'button', 'menu', 'nav', 'navigation', 'social',
|
||||
'instagram', 'facebook', 'twitter', 'linkedin', 'pinterest'
|
||||
'logo',
|
||||
'icon',
|
||||
'sprite',
|
||||
'favicon',
|
||||
'banner',
|
||||
'footer',
|
||||
'header',
|
||||
'background',
|
||||
'pattern',
|
||||
'placeholder',
|
||||
'thumbnail-small',
|
||||
'btn',
|
||||
'button',
|
||||
'menu',
|
||||
'nav',
|
||||
'navigation',
|
||||
'social',
|
||||
'instagram',
|
||||
'facebook',
|
||||
'twitter',
|
||||
'linkedin',
|
||||
'pinterest'
|
||||
];
|
||||
if (badPatterns.some(pattern => lower.includes(pattern))) {
|
||||
if (badPatterns.some((pattern) => lower.includes(pattern))) {
|
||||
return false;
|
||||
}
|
||||
if (url.endsWith('.svg')) {
|
||||
@@ -91,7 +110,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
|
||||
// Priority 1: OpenGraph and Twitter meta tags (main product image)
|
||||
const ogImageRegex = /<meta[^>]+property=["']og:image["'][^>]+content=["']([^"'>]+)["']/gi;
|
||||
const twitterImageRegex = /<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"'>]+)["']/gi;
|
||||
const twitterImageRegex =
|
||||
/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"'>]+)["']/gi;
|
||||
|
||||
while ((match = ogImageRegex.exec(html)) !== null) {
|
||||
const url = toAbsoluteUrl(match[1]);
|
||||
@@ -108,7 +128,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
}
|
||||
|
||||
// Priority 2: Look for JSON-LD structured data (very common in modern e-commerce)
|
||||
const jsonLdRegex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
||||
const jsonLdRegex =
|
||||
/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
||||
while ((match = jsonLdRegex.exec(html)) !== null) {
|
||||
try {
|
||||
const jsonStr = match[1];
|
||||
@@ -147,7 +168,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
|
||||
const jsonImages = new Set<string>();
|
||||
extractImages(jsonData, jsonImages);
|
||||
jsonImages.forEach(img => {
|
||||
jsonImages.forEach((img) => {
|
||||
if (!imageUrls.includes(img)) {
|
||||
imageUrls.push(img);
|
||||
}
|
||||
@@ -210,7 +231,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
}
|
||||
|
||||
// Final filtering: remove very long URLs and duplicates
|
||||
const finalImages = [...new Set(imageUrls)].filter(url => {
|
||||
const finalImages = [...new Set(imageUrls)].filter((url) => {
|
||||
return url.length < 2000 && isLikelyProductImage(url);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
|
||||
// Find wishlist by either ownerToken or publicToken
|
||||
const wishlist = await db.query.wishlists.findFirst({
|
||||
where: or(
|
||||
eq(wishlists.ownerToken, token),
|
||||
eq(wishlists.publicToken, token)
|
||||
),
|
||||
where: or(eq(wishlists.ownerToken, token), eq(wishlists.publicToken, token)),
|
||||
with: {
|
||||
items: {
|
||||
orderBy: (items, { asc }) => [asc(items.order)]
|
||||
|
||||
@@ -50,15 +50,17 @@ export const load: PageServerLoad = async (event) => {
|
||||
|
||||
// Map saved wishlists to include ownerToken from savedWishlists table (not from wishlist)
|
||||
// This ensures users only see ownerToken if they claimed via edit link
|
||||
const savedWithAccess = saved.map(s => ({
|
||||
const savedWithAccess = saved.map((s) => ({
|
||||
...s,
|
||||
wishlist: s.wishlist ? {
|
||||
wishlist: s.wishlist
|
||||
? {
|
||||
...s.wishlist,
|
||||
// Override ownerToken: use the one stored in savedWishlists (which is null for public saves)
|
||||
ownerToken: s.ownerToken,
|
||||
// Keep publicToken as-is for viewing
|
||||
publicToken: s.wishlist.publicToken
|
||||
} : null
|
||||
}
|
||||
: null
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -84,7 +86,8 @@ export const actions: Actions = {
|
||||
return { success: false, error: 'Wishlist ID is required' };
|
||||
}
|
||||
|
||||
await db.update(wishlists)
|
||||
await db
|
||||
.update(wishlists)
|
||||
.set({ isFavorite: !isFavorite, updatedAt: new Date() })
|
||||
.where(eq(wishlists.id, wishlistId));
|
||||
|
||||
@@ -105,7 +108,8 @@ export const actions: Actions = {
|
||||
return { success: false, error: 'Saved wishlist ID is required' };
|
||||
}
|
||||
|
||||
await db.update(savedWishlists)
|
||||
await db
|
||||
.update(savedWishlists)
|
||||
.set({ isFavorite: !isFavorite })
|
||||
.where(eq(savedWishlists.id, savedWishlistId));
|
||||
|
||||
@@ -125,11 +129,11 @@ export const actions: Actions = {
|
||||
return { success: false, error: 'Saved wishlist ID is required' };
|
||||
}
|
||||
|
||||
await db.delete(savedWishlists)
|
||||
.where(and(
|
||||
eq(savedWishlists.id, savedWishlistId),
|
||||
eq(savedWishlists.userId, session.user.id)
|
||||
));
|
||||
await db
|
||||
.delete(savedWishlists)
|
||||
.where(
|
||||
and(eq(savedWishlists.id, savedWishlistId), eq(savedWishlists.userId, session.user.id))
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
@@ -147,11 +151,9 @@ export const actions: Actions = {
|
||||
return { success: false, error: 'Wishlist ID is required' };
|
||||
}
|
||||
|
||||
await db.delete(wishlists)
|
||||
.where(and(
|
||||
eq(wishlists.id, wishlistId),
|
||||
eq(wishlists.userId, session.user.id)
|
||||
));
|
||||
await db
|
||||
.delete(wishlists)
|
||||
.where(and(eq(wishlists.id, wishlistId), eq(wishlists.userId, session.user.id)));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
@@ -169,7 +171,8 @@ export const actions: Actions = {
|
||||
return { success: false, error: 'Theme is required' };
|
||||
}
|
||||
|
||||
await db.update(users)
|
||||
await db
|
||||
.update(users)
|
||||
.set({ dashboardTheme: theme, updatedAt: new Date() })
|
||||
.where(eq(users.id, session.user.id));
|
||||
|
||||
@@ -185,7 +188,8 @@ export const actions: Actions = {
|
||||
const formData = await request.formData();
|
||||
const color = formData.get('color') as string | null;
|
||||
|
||||
await db.update(users)
|
||||
await db
|
||||
.update(users)
|
||||
.set({ dashboardColor: color, updatedAt: new Date() })
|
||||
.where(eq(users.id, session.user.id));
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
|
||||
const claimedWishlists = $derived(() => {
|
||||
return (data.savedWishlists || [])
|
||||
.filter(saved => saved.wishlist?.ownerToken)
|
||||
.map(saved => ({
|
||||
.filter((saved) => saved.wishlist?.ownerToken)
|
||||
.map((saved) => ({
|
||||
...saved.wishlist,
|
||||
isFavorite: saved.isFavorite,
|
||||
isClaimed: true,
|
||||
@@ -72,7 +72,7 @@
|
||||
});
|
||||
|
||||
const savedWishlists = $derived(() => {
|
||||
return (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken);
|
||||
return (data.savedWishlists || []).filter((saved) => !saved.wishlist?.ownerToken);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -101,15 +101,19 @@
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<form method="POST" action="?/toggleFavorite" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/toggleFavorite"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="wishlistId" value={wishlist.id} />
|
||||
<input type="hidden" name="isFavorite" value={wishlist.isFavorite} />
|
||||
<Button type="submit" size="sm" variant="outline">
|
||||
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||
<Star class={wishlist.isFavorite ? 'fill-yellow-500 text-yellow-500' : ''} />
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
@@ -130,11 +134,15 @@
|
||||
{t.dashboard.copyLink}
|
||||
</Button>
|
||||
{#if unlocked}
|
||||
<form method="POST" action="?/deleteWishlist" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteWishlist"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="wishlistId" value={wishlist.id} />
|
||||
<Button type="submit" size="sm" variant="destructive">
|
||||
{t.dashboard.delete}
|
||||
@@ -163,15 +171,19 @@
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<form method="POST" action="?/toggleSavedFavorite" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/toggleSavedFavorite"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} />
|
||||
<input type="hidden" name="isFavorite" value={wishlist.isFavorite} />
|
||||
<Button type="submit" size="sm" variant="outline">
|
||||
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||
<Star class={wishlist.isFavorite ? 'fill-yellow-500 text-yellow-500' : ''} />
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
@@ -192,11 +204,15 @@
|
||||
{t.dashboard.copyLink}
|
||||
</Button>
|
||||
{#if unlocked}
|
||||
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/unsaveWishlist"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} />
|
||||
<Button type="submit" size="sm" variant="destructive">
|
||||
{t.dashboard.unclaim}
|
||||
@@ -219,15 +235,19 @@
|
||||
>
|
||||
{#snippet actions(saved, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<form method="POST" action="?/toggleSavedFavorite" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/toggleSavedFavorite"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="savedWishlistId" value={saved.id} />
|
||||
<input type="hidden" name="isFavorite" value={saved.isFavorite} />
|
||||
<Button type="submit" size="sm" variant="outline">
|
||||
<Star class={saved.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||
<Star class={saved.isFavorite ? 'fill-yellow-500 text-yellow-500' : ''} />
|
||||
</Button>
|
||||
</form>
|
||||
<Button
|
||||
@@ -237,11 +257,15 @@
|
||||
{t.dashboard.viewWishlist}
|
||||
</Button>
|
||||
{#if unlocked}
|
||||
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/unsaveWishlist"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="savedWishlistId" value={saved.id} />
|
||||
<Button type="submit" size="sm" variant="destructive">
|
||||
{t.dashboard.unsave}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
||||
@@ -49,13 +55,17 @@
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
{#if data.registered}
|
||||
<div class="bg-green-50 border border-green-200 text-green-700 dark:bg-green-950 dark:border-green-800 dark:text-green-300 px-4 py-3 rounded">
|
||||
<div
|
||||
class="bg-green-50 border border-green-200 text-green-700 dark:bg-green-950 dark:border-green-800 dark:text-green-300 px-4 py-3 rounded"
|
||||
>
|
||||
Account created successfully! Please sign in.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.error}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300 px-4 py-3 rounded">
|
||||
<div
|
||||
class="bg-red-50 border border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300 px-4 py-3 rounded"
|
||||
>
|
||||
Invalid username or password
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
||||
@@ -26,7 +32,9 @@
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
{#if form?.error}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300 px-4 py-3 rounded">
|
||||
<div
|
||||
class="bg-red-50 border border-red-200 text-red-700 dark:bg-red-950 dark:border-red-800 dark:text-red-300 px-4 py-3 rounded"
|
||||
>
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -49,7 +57,13 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="confirmPassword">{t.form.confirmPassword}</Label>
|
||||
<Input id="confirmPassword" name="confirmPassword" type="password" required minlength={8} />
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
minlength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" class="w-full">{t.auth.signUp}</Button>
|
||||
|
||||
@@ -75,10 +75,7 @@ export const actions: Actions = {
|
||||
reserverName: reserverName?.trim() || null
|
||||
});
|
||||
|
||||
await tx
|
||||
.update(items)
|
||||
.set({ isReserved: true })
|
||||
.where(eq(items.id, itemId));
|
||||
await tx.update(items).set({ isReserved: true }).where(eq(items.id, itemId));
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
@@ -114,10 +111,7 @@ export const actions: Actions = {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(reservations).where(eq(reservations.itemId, itemId));
|
||||
|
||||
await tx
|
||||
.update(items)
|
||||
.set({ isReserved: false })
|
||||
.where(eq(items.id, itemId));
|
||||
await tx.update(items).set({ isReserved: false }).where(eq(items.id, itemId));
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
@@ -175,10 +169,7 @@ export const actions: Actions = {
|
||||
await db
|
||||
.delete(savedWishlists)
|
||||
.where(
|
||||
and(
|
||||
eq(savedWishlists.id, savedWishlistId),
|
||||
eq(savedWishlists.userId, session.user.id)
|
||||
)
|
||||
and(eq(savedWishlists.id, savedWishlistId), eq(savedWishlists.userId, session.user.id))
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import type { PageData } from "./$types";
|
||||
import WishlistItem from "$lib/components/wishlist/WishlistItem.svelte";
|
||||
import ReservationButton from "$lib/components/wishlist/ReservationButton.svelte";
|
||||
import PageContainer from "$lib/components/layout/PageContainer.svelte";
|
||||
import Navigation from "$lib/components/layout/Navigation.svelte";
|
||||
import EmptyState from "$lib/components/layout/EmptyState.svelte";
|
||||
import { enhance } from "$app/forms";
|
||||
import { getCardStyle } from "$lib/utils/colors";
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import type { PageData } from './$types';
|
||||
import WishlistItem from '$lib/components/wishlist/WishlistItem.svelte';
|
||||
import ReservationButton from '$lib/components/wishlist/ReservationButton.svelte';
|
||||
import PageContainer from '$lib/components/layout/PageContainer.svelte';
|
||||
import Navigation from '$lib/components/layout/Navigation.svelte';
|
||||
import EmptyState from '$lib/components/layout/EmptyState.svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import SearchBar from "$lib/components/ui/SearchBar.svelte";
|
||||
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
|
||||
import SearchBar from '$lib/components/ui/SearchBar.svelte';
|
||||
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
const headerCardStyle = $derived(getCardStyle(data.wishlist.color));
|
||||
|
||||
const filteredItems = $derived(
|
||||
data.wishlist.items?.filter(item =>
|
||||
data.wishlist.items?.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
) || []
|
||||
@@ -47,37 +48,31 @@
|
||||
<div class="flex-1">
|
||||
<CardTitle class="text-3xl">{data.wishlist.title}</CardTitle>
|
||||
{#if data.wishlist.description}
|
||||
<CardDescription class="text-base"
|
||||
>{data.wishlist.description}</CardDescription
|
||||
>
|
||||
<CardDescription class="text-base">{data.wishlist.description}</CardDescription>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.isAuthenticated}
|
||||
{#if data.isClaimed}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
>
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
{t.wishlist.youClaimedThis}
|
||||
</Button>
|
||||
{:else if data.isSaved}
|
||||
<form method="POST" action="?/unsaveWishlist" use:enhance>
|
||||
<input
|
||||
type="hidden"
|
||||
name="savedWishlistId"
|
||||
value={data.savedWishlistId}
|
||||
/>
|
||||
<input type="hidden" name="savedWishlistId" value={data.savedWishlistId} />
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
{t.wishlist.unsaveWishlist}
|
||||
</Button>
|
||||
</form>
|
||||
{:else}
|
||||
<form method="POST" action="?/saveWishlist" use:enhance={() => {
|
||||
<form
|
||||
method="POST"
|
||||
action="?/saveWishlist"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
};
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="wishlistId" value={data.wishlist.id} />
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
{t.wishlist.saveWishlist}
|
||||
@@ -85,11 +80,7 @@
|
||||
</form>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => (window.location.href = "/signin")}
|
||||
>
|
||||
<Button variant="outline" size="sm" onclick={() => (window.location.href = '/signin')}>
|
||||
{t.wishlist.signInToSave}
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -118,18 +109,18 @@
|
||||
<Card style={headerCardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message="No wishes match your search."
|
||||
/>
|
||||
<EmptyState message="No wishes match your search." />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card style={headerCardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} showPattern={false} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message={t.wishlist.emptyWishes}
|
||||
<ThemeCard
|
||||
themeName={data.wishlist.theme}
|
||||
color={data.wishlist.color}
|
||||
showPattern={false}
|
||||
/>
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState message={t.wishlist.emptyWishes} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
@@ -119,7 +119,8 @@ export const actions: Actions = {
|
||||
throw error(404, 'Wishlist not found');
|
||||
}
|
||||
|
||||
await db.update(items)
|
||||
await db
|
||||
.update(items)
|
||||
.set({
|
||||
title: title.trim(),
|
||||
description: description?.trim() || null,
|
||||
@@ -175,7 +176,8 @@ export const actions: Actions = {
|
||||
const updates = JSON.parse(itemsJson) as Array<{ id: string; order: number }>;
|
||||
|
||||
for (const update of updates) {
|
||||
await db.update(items)
|
||||
await db
|
||||
.update(items)
|
||||
.set({ order: String(update.order), updatedAt: new Date() })
|
||||
.where(eq(items.id, update.id));
|
||||
}
|
||||
@@ -242,9 +244,7 @@ export const actions: Actions = {
|
||||
updates.theme = theme?.toString().trim() || 'none';
|
||||
}
|
||||
|
||||
await db.update(wishlists)
|
||||
.set(updates)
|
||||
.where(eq(wishlists.id, wishlist.id));
|
||||
await db.update(wishlists).set(updates).where(eq(wishlists.id, wishlist.id));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
@@ -302,11 +302,10 @@ export const actions: Actions = {
|
||||
throw error(404, 'Wishlist not found');
|
||||
}
|
||||
|
||||
await db.delete(savedWishlists).where(
|
||||
and(
|
||||
eq(savedWishlists.userId, session.user.id),
|
||||
eq(savedWishlists.wishlistId, wishlist.id)
|
||||
)
|
||||
await db
|
||||
.delete(savedWishlists)
|
||||
.where(
|
||||
and(eq(savedWishlists.userId, session.user.id), eq(savedWishlists.wishlistId, wishlist.id))
|
||||
);
|
||||
|
||||
return { success: true, message: 'Wishlist unclaimed' };
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
import AddItemForm from "$lib/components/wishlist/AddItemForm.svelte";
|
||||
import EditItemForm from "$lib/components/wishlist/EditItemForm.svelte";
|
||||
import ShareLinks from "$lib/components/wishlist/ShareLinks.svelte";
|
||||
import PageContainer from "$lib/components/layout/PageContainer.svelte";
|
||||
import Navigation from "$lib/components/layout/Navigation.svelte";
|
||||
import WishlistHeader from "$lib/components/wishlist/WishlistHeader.svelte";
|
||||
import WishlistActionButtons from "$lib/components/wishlist/WishlistActionButtons.svelte";
|
||||
import EditableItemsList from "$lib/components/wishlist/EditableItemsList.svelte";
|
||||
import ClaimWishlistSection from "$lib/components/wishlist/ClaimWishlistSection.svelte";
|
||||
import DangerZone from "$lib/components/wishlist/DangerZone.svelte";
|
||||
import type { Item } from "$lib/server/schema";
|
||||
import SearchBar from "$lib/components/ui/SearchBar.svelte";
|
||||
import * as wishlistUpdates from "$lib/utils/wishlistUpdates";
|
||||
import type { PageData } from './$types';
|
||||
import AddItemForm from '$lib/components/wishlist/AddItemForm.svelte';
|
||||
import EditItemForm from '$lib/components/wishlist/EditItemForm.svelte';
|
||||
import ShareLinks from '$lib/components/wishlist/ShareLinks.svelte';
|
||||
import PageContainer from '$lib/components/layout/PageContainer.svelte';
|
||||
import Navigation from '$lib/components/layout/Navigation.svelte';
|
||||
import WishlistHeader from '$lib/components/wishlist/WishlistHeader.svelte';
|
||||
import WishlistActionButtons from '$lib/components/wishlist/WishlistActionButtons.svelte';
|
||||
import EditableItemsList from '$lib/components/wishlist/EditableItemsList.svelte';
|
||||
import ClaimWishlistSection from '$lib/components/wishlist/ClaimWishlistSection.svelte';
|
||||
import DangerZone from '$lib/components/wishlist/DangerZone.svelte';
|
||||
import type { Item } from '$lib/server/schema';
|
||||
import SearchBar from '$lib/components/ui/SearchBar.svelte';
|
||||
import * as wishlistUpdates from '$lib/utils/wishlistUpdates';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -21,22 +21,21 @@
|
||||
let editingItem = $state<Item | null>(null);
|
||||
let addFormElement = $state<HTMLElement | null>(null);
|
||||
let editFormElement = $state<HTMLElement | null>(null);
|
||||
let searchQuery = $state("");
|
||||
let searchQuery = $state('');
|
||||
let currentTheme = $state(data.wishlist.theme || 'none');
|
||||
let currentColor = $state(data.wishlist.color);
|
||||
|
||||
let items = $state<Item[]>([]);
|
||||
|
||||
$effect.pre(() => {
|
||||
const sorted = [...data.wishlist.items].sort(
|
||||
(a, b) => Number(a.order) - Number(b.order),
|
||||
);
|
||||
const sorted = [...data.wishlist.items].sort((a, b) => Number(a.order) - Number(b.order));
|
||||
items = sorted;
|
||||
});
|
||||
|
||||
let filteredItems = $derived(
|
||||
searchQuery.trim()
|
||||
? items.filter(item =>
|
||||
? items.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
@@ -56,16 +55,14 @@
|
||||
showAddForm = false;
|
||||
setTimeout(() => {
|
||||
editFormElement?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function handleColorChange(itemId: string, newColor: string) {
|
||||
items = items.map((item) =>
|
||||
item.id === itemId ? { ...item, color: newColor } : item,
|
||||
);
|
||||
items = items.map((item) => (item.id === itemId ? { ...item, color: newColor } : item));
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
@@ -75,7 +72,7 @@
|
||||
async function handleReorder(items: Item[]) {
|
||||
const updates = items.map((item, index) => ({
|
||||
id: item.id,
|
||||
order: index,
|
||||
order: index
|
||||
}));
|
||||
await wishlistUpdates.reorderItems(updates);
|
||||
}
|
||||
@@ -85,8 +82,8 @@
|
||||
if (showAddForm) {
|
||||
setTimeout(() => {
|
||||
addFormElement?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
@@ -95,7 +92,7 @@
|
||||
async function handlePositionChange(newPosition: number) {
|
||||
if (!editingItem) return;
|
||||
|
||||
const currentIndex = items.findIndex(item => item.id === editingItem.id);
|
||||
const currentIndex = items.findIndex((item) => item.id === editingItem.id);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const newIndex = newPosition - 1; // Convert to 0-based index
|
||||
@@ -148,15 +145,15 @@
|
||||
ownerToken={data.wishlist.ownerToken}
|
||||
/>
|
||||
|
||||
<WishlistActionButtons
|
||||
bind:rearranging={rearranging}
|
||||
showAddForm={showAddForm}
|
||||
onToggleAddForm={handleToggleAddForm}
|
||||
/>
|
||||
<WishlistActionButtons bind:rearranging {showAddForm} onToggleAddForm={handleToggleAddForm} />
|
||||
|
||||
{#if showAddForm}
|
||||
<div bind:this={addFormElement}>
|
||||
<AddItemForm onSuccess={handleItemAdded} wishlistColor={currentColor} wishlistTheme={currentTheme} />
|
||||
<AddItemForm
|
||||
onSuccess={handleItemAdded}
|
||||
wishlistColor={currentColor}
|
||||
wishlistTheme={currentTheme}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -167,7 +164,7 @@
|
||||
onSuccess={handleItemUpdated}
|
||||
onCancel={cancelEditing}
|
||||
onColorChange={handleColorChange}
|
||||
currentPosition={items.findIndex(item => item.id === editingItem.id) + 1}
|
||||
currentPosition={items.findIndex((item) => item.id === editingItem.id) + 1}
|
||||
totalItems={items.length}
|
||||
onPositionChange={handlePositionChange}
|
||||
wishlistColor={currentColor}
|
||||
|
||||
@@ -12,11 +12,11 @@ const config = {
|
||||
out: 'build'
|
||||
}),
|
||||
alias: {
|
||||
'$db': './src/lib/db',
|
||||
'$components': './src/lib/components',
|
||||
'$utils': './src/lib/utils',
|
||||
'$stores': './src/lib/stores',
|
||||
'$i18n': './src/lib/i18n'
|
||||
$db: './src/lib/db',
|
||||
$components: './src/lib/components',
|
||||
$utils: './src/lib/utils',
|
||||
$stores: './src/lib/stores',
|
||||
$i18n: './src/lib/i18n'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user