Compare commits
10 Commits
c8a8b8df1b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9ef3e8248 | ||
|
|
a0eb060257 | ||
|
|
e5025d89a6 | ||
|
|
6507ac7f0c | ||
|
|
259bb23ec9 | ||
|
|
2f53620de7 | ||
|
|
b13c69b46e | ||
|
|
2caf688dfa | ||
|
|
915131db58 | ||
|
|
31404b874a |
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json .
|
||||||
|
COPY package-lock.json .
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "build"]
|
||||||
243
package-lock.json
generated
243
package-lock.json
generated
@@ -11,7 +11,7 @@
|
|||||||
"pocketbase": "^0.26.3"
|
"pocketbase": "^0.26.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-node": "^5.4.0",
|
||||||
"@sveltejs/kit": "^2.47.1",
|
"@sveltejs/kit": "^2.47.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"svelte": "^5.41.0",
|
"svelte": "^5.41.0",
|
||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.3.3",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tailwindcss-motion": "^1.1.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.10"
|
"vite": "^7.1.10"
|
||||||
}
|
}
|
||||||
@@ -524,6 +525,112 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
|
"version": "28.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.9.tgz",
|
||||||
|
"integrity": "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
|
"commondir": "^1.0.1",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"fdir": "^6.2.0",
|
||||||
|
"is-reference": "1.2.1",
|
||||||
|
"magic-string": "^0.30.3",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0 || 14 >= 14.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.68.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-json": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
|
"version": "16.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
|
||||||
|
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
|
"@types/resolve": "1.20.2",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"is-module": "^1.0.0",
|
||||||
|
"resolve": "^1.22.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.53.2",
|
"version": "4.53.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
|
||||||
@@ -849,14 +956,20 @@
|
|||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/adapter-auto": {
|
"node_modules/@sveltejs/adapter-node": {
|
||||||
"version": "7.0.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.4.0.tgz",
|
||||||
"integrity": "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==",
|
"integrity": "sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.1",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
|
"rollup": "^4.9.5"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/kit": "^2.0.0"
|
"@sveltejs/kit": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
@@ -1223,6 +1336,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/resolve": {
|
||||||
|
"version": "1.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -1282,6 +1402,13 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commondir": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
@@ -1410,6 +1537,13 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -1443,6 +1577,16 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -1450,6 +1594,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-module": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-reference": {
|
"node_modules/is-reference": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||||
@@ -1804,6 +1984,13 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1979,6 +2166,27 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.16.1",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.53.2",
|
"version": "4.53.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
|
||||||
@@ -2066,6 +2274,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.43.8",
|
"version": "5.43.8",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.8.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.8.tgz",
|
||||||
@@ -2123,6 +2344,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss-motion": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss-motion/-/tailwindcss-motion-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-CeeQAc5o31BuEPMyWdq/786X7QWNeifa+8khfu74Fs8lGkgEwjNYv6dGv+lRFS8FWXV5dp7F3AU9JjBXjiaQfw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"lint": "prettier --check ."
|
"lint": "prettier --check ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^7.0.0",
|
"@sveltejs/adapter-node": "^5.4.0",
|
||||||
"@sveltejs/kit": "^2.47.1",
|
"@sveltejs/kit": "^2.47.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"svelte": "^5.41.0",
|
"svelte": "^5.41.0",
|
||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.3.3",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tailwindcss-motion": "^1.1.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.10"
|
"vite": "^7.1.10"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@plugin 'tailwindcss-motion';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-gray-50 font-sans text-gray-800 antialiased;
|
@apply bg-gray-50 font-sans text-gray-800 antialiased;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
primary:
|
primary:
|
||||||
'px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-xl transition-all duration-200 ease-in-out disabled:opacity-70 disabled:cursor-not-allowed',
|
'px-4 py-2 text-sm font-medium text-white bg-emerald-800 hover:bg-emerald-900 rounded-xl transition-all duration-200 ease-in-out disabled:opacity-70 disabled:cursor-not-allowed',
|
||||||
secondary:
|
secondary:
|
||||||
'px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-100 rounded-xl transition-all duration-200 ease-in-out',
|
'px-4 py-2 text-sm font-medium text-gray-100 bg-gray-600 border border-gray-300 hover:bg-gray-500 rounded-xl transition-all duration-200 ease-in-out',
|
||||||
danger:
|
danger:
|
||||||
'px-4 py-2 text-sm font-medium text-white bg-red-500 hover:bg-red-600 rounded-xl transition-all duration-200 ease-in-out disabled:opacity-70 disabled:cursor-not-allowed'
|
'px-4 py-2 text-sm font-medium text-white bg-red-800 hover:bg-red-900 rounded-xl transition-all duration-200 ease-in-out disabled:opacity-70 disabled:cursor-not-allowed'
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -18,14 +19,17 @@
|
|||||||
href?: string;
|
href?: string;
|
||||||
};
|
};
|
||||||
let { children, onClick, disabled, variant = 'primary', href }: Props = $props();
|
let { children, onClick, disabled, variant = 'primary', href }: Props = $props();
|
||||||
|
|
||||||
|
const isActive = page.params.year === href?.split('/').pop();
|
||||||
|
$inspect(isActive, 'isActive', page.params.year, href);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a {href} class={variants[variant]}>
|
<a {href} class={variants[variant]} class:!bg-gray-900={isActive}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button class={variants[variant]} onclick={onClick} {disabled}>
|
<button style:cursor="pointer" class={variants[variant]} onclick={onClick} {disabled}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="rounded-2xl bg-gray-200 p-4 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md md:p-6"
|
class="rounded-2xl bg-gray-500 p-4 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md md:p-6"
|
||||||
>
|
>
|
||||||
<div class="mb-1 text-sm text-gray-600">{title}</div>
|
<div class="mb-1 text-sm text-gray-200">{title}</div>
|
||||||
<div class="text-3xl font-bold text-gray-800">{value}</div>
|
<div class="text-3xl font-bold text-gray-100">{value}</div>
|
||||||
{#if description}
|
{#if description}
|
||||||
<div class="mt-2 text-xs text-gray-500">{description}</div>
|
<div class="mt-2 text-xs text-gray-200">{description}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { refreshAll } from '$app/navigation';
|
||||||
import { formatCurrency } from '$lib/helpers/formatCurrency';
|
import { formatCurrency } from '$lib/helpers/formatCurrency';
|
||||||
|
import { DB } from '$lib/integrations/db';
|
||||||
import Badge from '../atoms/Badge.svelte';
|
import Badge from '../atoms/Badge.svelte';
|
||||||
import Heading from '../atoms/Heading.svelte';
|
import Heading from '../atoms/Heading.svelte';
|
||||||
import { fly } from 'svelte/transition';
|
import GiftModal from './GiftModal.svelte';
|
||||||
|
|
||||||
let gift: Gift = $props();
|
type Props = {
|
||||||
|
editable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
let gift: Gift & Props = $props();
|
||||||
|
let isEditModal = $state(false);
|
||||||
|
|
||||||
const bgByStatus = {
|
const bgByStatus = {
|
||||||
planned: 'bg-gray-600',
|
planned: 'bg-gray-600',
|
||||||
@@ -17,8 +24,8 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class={`my-6 rounded-2xl p-4 shadow-sm transition-all duration-200 ease-in-out ${bgByStatus[gift.status]}`}
|
class={`my-6 rounded-2xl p-4 shadow-sm transition-all duration-200 ease-in-out ${bgByStatus[gift.status]}`}
|
||||||
in:fly={{ x: 100 }}
|
class:cursor-pointer={!!gift.editable}
|
||||||
out:fly={{ x: -100 }}
|
onclick={() => (isEditModal = true)}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -26,6 +33,19 @@
|
|||||||
<Heading size="small" spacing="none">
|
<Heading size="small" spacing="none">
|
||||||
{gift.title}
|
{gift.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
{#if gift.editable}
|
||||||
|
<p
|
||||||
|
class="cursor-pointer text-xs text-red-400"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
DB.deleteGift(gift.id).finally(() => {
|
||||||
|
refreshAll();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Usuń
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if gift.link}
|
{#if gift.link}
|
||||||
@@ -38,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2 text-white">
|
||||||
<span>Koszt: {formatCurrency(gift.cost)}</span>
|
<span>Koszt: {formatCurrency(gift.cost)}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if gift.description}
|
{#if gift.description}
|
||||||
@@ -46,3 +66,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if isEditModal}
|
||||||
|
<GiftModal
|
||||||
|
title="Edytuj prezent"
|
||||||
|
personId={gift.person}
|
||||||
|
editGift={gift}
|
||||||
|
isOpen={isEditModal}
|
||||||
|
onClose={() => (isEditModal = false)}
|
||||||
|
onSave={async (data) => {
|
||||||
|
await DB.updateGift(gift.id, data);
|
||||||
|
refreshAll();
|
||||||
|
isEditModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
149
src/lib/components/molecules/GiftModal.svelte
Normal file
149
src/lib/components/molecules/GiftModal.svelte
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import type { DB } from '$lib/integrations/db';
|
||||||
|
import Modal from '../atoms/Modal.svelte';
|
||||||
|
import Button from '../atoms/Button.svelte';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { formatStatus } from '$lib/helpers/formatStatus';
|
||||||
|
|
||||||
|
type CreateGift = Parameters<typeof DB.createGift>[0];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSave: (data: CreateGift) => Promise<void>;
|
||||||
|
editGift?: Gift;
|
||||||
|
personId: string;
|
||||||
|
} & Omit<ComponentProps<typeof Modal>, 'children'>;
|
||||||
|
|
||||||
|
let { onSave, isOpen, onClose, editGift, personId }: Props = $props();
|
||||||
|
|
||||||
|
let gift = $state<Gift>(editGift ? { ...editGift } : ({} as Gift));
|
||||||
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
if (!gift?.title) {
|
||||||
|
gift.title = '';
|
||||||
|
gift.description = '';
|
||||||
|
gift.link = '';
|
||||||
|
gift.cost = 0;
|
||||||
|
gift.imageUrl = '';
|
||||||
|
gift.status = 'planned';
|
||||||
|
gift.year = page.data.year.id;
|
||||||
|
gift.person = personId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await onSave(gift);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal {isOpen} {onClose} title={gift ? 'Edytuj prezent' : 'Dodaj nowy prezent'}>
|
||||||
|
<form onsubmit={handleSubmit} class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="title" class="mb-2 block text-sm font-medium text-gray-700"> Tytuł * </label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
bind:value={gift.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description" class="mb-2 block text-sm font-medium text-gray-700"> Opis </label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
rows={3}
|
||||||
|
class="w-full resize-none rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
bind:value={gift.description}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label for="cost" class="mb-2 block text-sm font-medium text-gray-700">
|
||||||
|
Koszt (PLN) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="cost"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
bind:value={gift.cost}
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="status" class="mb-2 block text-sm font-medium text-gray-700"> Status * </label>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
bind:value={gift.status}
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<option value="planned">{formatStatus('planned')} </option>
|
||||||
|
<option value="decided">{formatStatus('decided')}</option>
|
||||||
|
<option value="bought">{formatStatus('bought')}</option>
|
||||||
|
<option value="ready">{formatStatus('ready')}</option>
|
||||||
|
<option value="wrapped">{formatStatus('wrapped')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="link" class="mb-2 block text-sm font-medium text-gray-700"> Link </label>
|
||||||
|
<textarea
|
||||||
|
id="linki"
|
||||||
|
bind:value={gift.link}
|
||||||
|
rows={3}
|
||||||
|
class="w-full resize-none rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="ceneoId" class="mb-2 block text-sm font-medium text-gray-700"> ID Ceneo </label>
|
||||||
|
<input
|
||||||
|
id="ceneoId"
|
||||||
|
type="text"
|
||||||
|
bind:value={gift.ceneo_id}
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<p class="mt-2 text-xs text-gray-500">
|
||||||
|
Jeśli produkt nie został jeszcze kupiony i zostanie podany jego ID na ceneo, bot będzie
|
||||||
|
obserwował ceny i w razie zmian będzie informował o tym na discord.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="imageUrl" class="mb-2 block text-sm font-medium text-gray-700">
|
||||||
|
URL obrazu
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="imageUrl"
|
||||||
|
type="url"
|
||||||
|
bind:value={gift.imageUrl}
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<Button variant="secondary" onClick={onClose} disabled={isLoading}>Anuluj</Button>
|
||||||
|
<Button variant="primary" onClick={handleSubmit} disabled={isLoading || !gift?.title?.trim()}>
|
||||||
|
{isLoading ? 'Zapisywanie...' : 'Zapisz'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
89
src/lib/components/molecules/PersonCard.svelte
Normal file
89
src/lib/components/molecules/PersonCard.svelte
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { refreshAll } from '$app/navigation';
|
||||||
|
import { formatCurrency } from '$lib/helpers/formatCurrency';
|
||||||
|
import { DB } from '$lib/integrations/db';
|
||||||
|
import Button from '../atoms/Button.svelte';
|
||||||
|
import Heading from '../atoms/Heading.svelte';
|
||||||
|
import GiftCard from './GiftCard.svelte';
|
||||||
|
import GiftModal from './GiftModal.svelte';
|
||||||
|
import PersonModal from './PersonModal.svelte';
|
||||||
|
|
||||||
|
let person: Person = $props();
|
||||||
|
const gifts = $derived(person.expand.gifts);
|
||||||
|
const totalCost = $derived(gifts?.reduce((acc, gift) => acc + gift.cost, 0) || 0);
|
||||||
|
|
||||||
|
let editPersonModal = $state(false);
|
||||||
|
let addGiftModal = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-6 rounded-2xl bg-gray-300 p-4 text-gray-700 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md md:p-6"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mb-6 flex flex-col items-center gap-4 border-b border-gray-700 pb-4 sm:justify-between"
|
||||||
|
>
|
||||||
|
<Heading size="large" spacing="none">
|
||||||
|
<span class="text-gray-700">
|
||||||
|
{person.name}
|
||||||
|
</span>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-center gap-2">
|
||||||
|
<Button variant="primary" onClick={() => (editPersonModal = true)}>Edytuj</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => {
|
||||||
|
DB.deletePerson(person.id).finally(() => {
|
||||||
|
refreshAll();
|
||||||
|
});
|
||||||
|
}}>Usuń</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<PersonModal
|
||||||
|
isOpen={editPersonModal}
|
||||||
|
onClose={() => (editPersonModal = false)}
|
||||||
|
editPerson={person}
|
||||||
|
title="Edytuj osobę"
|
||||||
|
onSave={async (data) => {
|
||||||
|
await DB.updatePerson(person.id, data);
|
||||||
|
refreshAll();
|
||||||
|
editPersonModal = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if person.notes}
|
||||||
|
<p class="text-gray-600">{person.notes}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500">
|
||||||
|
<span>Ilość prezentów: {gifts?.length || 0}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Koszt: {formatCurrency(totalCost)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 mb-4 flex items-start justify-between gap-4">
|
||||||
|
<Heading size="medium" spacing="none">
|
||||||
|
<span class="text-gray-700">
|
||||||
|
Prezenty <br />
|
||||||
|
</span>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Button variant="secondary" onClick={() => (addGiftModal = true)}>+</Button>
|
||||||
|
|
||||||
|
<!-- <GiftCardAdd person={person} /> -->
|
||||||
|
<GiftModal
|
||||||
|
title="Dodaj nowy prezent"
|
||||||
|
isOpen={addGiftModal}
|
||||||
|
onClose={() => (addGiftModal = false)}
|
||||||
|
onSave={async (data) => {
|
||||||
|
await DB.createGift(data);
|
||||||
|
refreshAll();
|
||||||
|
addGiftModal = false;
|
||||||
|
}}
|
||||||
|
personId={person.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each gifts as gift (gift.id)}
|
||||||
|
<GiftCard {...gift} editable />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
80
src/lib/components/molecules/PersonModal.svelte
Normal file
80
src/lib/components/molecules/PersonModal.svelte
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ComponentProps } from 'svelte';
|
||||||
|
import type { DB } from '$lib/integrations/db';
|
||||||
|
import Modal from '../atoms/Modal.svelte';
|
||||||
|
import Button from '../atoms/Button.svelte';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
|
||||||
|
type CreatePerson = Parameters<typeof DB.createPerson>[0];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSave: (data: CreatePerson) => Promise<void>;
|
||||||
|
editPerson?: Person;
|
||||||
|
} & Omit<ComponentProps<typeof Modal>, 'children'>;
|
||||||
|
|
||||||
|
let { onSave, isOpen, onClose, editPerson }: Props = $props();
|
||||||
|
|
||||||
|
let person = $state<Person>(editPerson ? { ...editPerson } : ({} as Person));
|
||||||
|
let isLoading = $state(false);
|
||||||
|
|
||||||
|
if (!person?.name) {
|
||||||
|
person.name = '';
|
||||||
|
person.notes = '';
|
||||||
|
person.years = [page.params.year || new Date().getFullYear().toString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!person) return;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
await onSave(person);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal {isOpen} {onClose} title={person ? 'Edytuj osobę' : 'Dodaj nową osobę'}>
|
||||||
|
<form onsubmit={handleSubmit} class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="name" class="mb-2 block text-sm font-medium text-gray-700">
|
||||||
|
Imię i nazwisko *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
required
|
||||||
|
disabled={isLoading}
|
||||||
|
bind:value={person.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="notes" class="mb-2 block text-sm font-medium text-gray-700"> Notatki </label>
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
rows={4}
|
||||||
|
class="w-full resize-none rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={isLoading}
|
||||||
|
bind:value={person.notes}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<Button variant="secondary" onClick={onClose} disabled={isLoading}>Anuluj</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isLoading || !person?.name?.trim()}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Zapisywanie...' : 'Zapisz'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button variant="secondary" href="/">Prezenty tego roku</Button>
|
<Button variant="secondary" href="/">Prezenty tego roku</Button>
|
||||||
{#each years as year}
|
{#each years as year}
|
||||||
<Button href={`/year/${year.year}`} variant="secondary">
|
<Button href={`/rok/${year.year}`} variant="secondary">
|
||||||
{year.year}
|
{year.year}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import GiftCard from '../molecules/GiftCard.svelte';
|
import GiftCard from '../molecules/GiftCard.svelte';
|
||||||
import { cubicInOut } from 'svelte/easing';
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
gifts: Gift[];
|
gifts: Gift[];
|
||||||
@@ -12,29 +12,43 @@
|
|||||||
let sort = $state<keyof Pick<Gift, 'title' | 'cost' | 'person' | 'status'>>('title');
|
let sort = $state<keyof Pick<Gift, 'title' | 'cost' | 'person' | 'status'>>('title');
|
||||||
|
|
||||||
let sortedGifts = $derived.by(() => {
|
let sortedGifts = $derived.by(() => {
|
||||||
let newSortedGifts = [...gifts];
|
let out = [...gifts];
|
||||||
if (sort === 'cost') {
|
|
||||||
newSortedGifts.sort((a, b) => a.cost - b.cost);
|
switch (sort) {
|
||||||
} else if (sort === 'person') {
|
case 'title':
|
||||||
newSortedGifts.sort((a, b) => a.expand.person.name.localeCompare(b.expand.person.name));
|
out.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
} else if (sort === 'status') {
|
break;
|
||||||
newSortedGifts.sort((a, b) => a.status.localeCompare(b.status));
|
case 'cost':
|
||||||
|
out.sort((a, b) => a.cost - b.cost);
|
||||||
|
break;
|
||||||
|
case 'person':
|
||||||
|
out.sort((a, b) => a.expand.person.name.localeCompare(b.expand.person.name));
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
out.sort((a, b) => a.status.localeCompare(b.status));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return newSortedGifts;
|
|
||||||
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
let filteredGifts = $derived.by(() => {
|
let filteredGifts = $derived.by(() => {
|
||||||
if (filter === 'all') return sortedGifts;
|
let out = [...sortedGifts];
|
||||||
return sortedGifts.filter((gift) => gift.status === filter);
|
|
||||||
|
if (filter !== 'all') {
|
||||||
|
out = out.filter((gift) => gift.status === filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
let renderGifts = $derived(filteredGifts);
|
let renderedGifts = $derived(filteredGifts);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="flex flex-row items-center justify-between gap-4">
|
<div class="flex flex-row items-center justify-between gap-4">
|
||||||
<select
|
<select
|
||||||
class="rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
class="ml-auto rounded-xl border border-gray-300 px-4 py-2 transition-all outline-none focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||||
bind:value={filter}
|
bind:value={filter}
|
||||||
>
|
>
|
||||||
<option value="all">Wszystkie</option>
|
<option value="all">Wszystkie</option>
|
||||||
@@ -54,9 +68,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each renderGifts as gift (gift.id)}
|
{#key filter}
|
||||||
<div animate:flip={{ duration: 400, easing: cubicInOut }}>
|
<div in:fly={{ x: 100, delay: 250, duration: 200 }} out:fly={{ x: -100, duration: 200 }}>
|
||||||
|
{#each renderedGifts as gift (gift.id)}
|
||||||
|
<div animate:flip={{ duration: 200 }}>
|
||||||
<GiftCard {...gift} />
|
<GiftCard {...gift} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
22
src/lib/components/organisms/YearControls.svelte
Normal file
22
src/lib/components/organisms/YearControls.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { refreshAll } from '$app/navigation';
|
||||||
|
import { DB } from '$lib/integrations/db';
|
||||||
|
import ActionCard from '../molecules/ActionCard.svelte';
|
||||||
|
import PersonModal from '../molecules/PersonModal.svelte';
|
||||||
|
|
||||||
|
let isOpen = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="my-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<ActionCard title="Dodaj nową osobę" onClick={() => (isOpen = true)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PersonModal
|
||||||
|
{isOpen}
|
||||||
|
onClose={() => (isOpen = false)}
|
||||||
|
title="Dodaj nową osobę"
|
||||||
|
onSave={async (data) => {
|
||||||
|
await DB.createPerson(data);
|
||||||
|
refreshAll();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
14
src/lib/helpers/formatStatus/index.ts
Normal file
14
src/lib/helpers/formatStatus/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const formatStatus = (status: Gift['status']) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'planned':
|
||||||
|
return 'Planowane';
|
||||||
|
case 'decided':
|
||||||
|
return 'Do kupienia';
|
||||||
|
case 'bought':
|
||||||
|
return 'Kupione';
|
||||||
|
case 'ready':
|
||||||
|
return 'Do spakowania';
|
||||||
|
case 'wrapped':
|
||||||
|
return 'Gotowe';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,7 +18,8 @@ export const DB = {
|
|||||||
return await pb.collection('gifts_person').getFirstListItem(`name = "${name}"`);
|
return await pb.collection('gifts_person').getFirstListItem(`name = "${name}"`);
|
||||||
},
|
},
|
||||||
createPerson: async (data: Pick<DB.Person, 'name' | 'notes' | 'years'>): Promise<DB.Person> => {
|
createPerson: async (data: Pick<DB.Person, 'name' | 'notes' | 'years'>): Promise<DB.Person> => {
|
||||||
return await pb.collection('gifts_person').create({ ...data });
|
const year = await pb.collection('gifts_year').getFirstListItem(`year = ${data.years[0]}`);
|
||||||
|
return await pb.collection('gifts_person').create({ ...data, years: [year.id] });
|
||||||
},
|
},
|
||||||
updatePerson: async (id: string, data: Pick<DB.Person, 'name' | 'notes'>): Promise<Person> => {
|
updatePerson: async (id: string, data: Pick<DB.Person, 'name' | 'notes'>): Promise<Person> => {
|
||||||
return await pb.collection('gifts_person').update(id, data);
|
return await pb.collection('gifts_person').update(id, data);
|
||||||
|
|||||||
5
src/lib/loaders/LoaderPerson.ts
Normal file
5
src/lib/loaders/LoaderPerson.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { DB } from '$lib/integrations/db';
|
||||||
|
|
||||||
|
export const LoaderPersons = (yearId: string) => {
|
||||||
|
return () => DB.getPersons(yearId);
|
||||||
|
};
|
||||||
19
src/routes/rok/[year]/+page.server.ts
Normal file
19
src/routes/rok/[year]/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { orchestrateLoaders } from '$lib/loaders';
|
||||||
|
import { LoaderPersons } from '$lib/loaders/LoaderPerson';
|
||||||
|
import { LoaderYear } from '$lib/loaders/LoaderYear';
|
||||||
|
import { LoaderYears } from '$lib/loaders/LoaderYear';
|
||||||
|
|
||||||
|
export const load = async ({ params }: { params: { year: string } }) => {
|
||||||
|
const [yearData, yearsData] = await orchestrateLoaders([
|
||||||
|
LoaderYear(Number(params.year)),
|
||||||
|
LoaderYears()
|
||||||
|
]);
|
||||||
|
|
||||||
|
const personsData = await LoaderPersons(yearData.id)();
|
||||||
|
|
||||||
|
return {
|
||||||
|
year: yearData,
|
||||||
|
years: yearsData,
|
||||||
|
persons: personsData
|
||||||
|
};
|
||||||
|
};
|
||||||
17
src/routes/rok/[year]/+page.svelte
Normal file
17
src/routes/rok/[year]/+page.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import PersonCard from '$lib/components/molecules/PersonCard.svelte';
|
||||||
|
import YearNav from '$lib/components/molecules/YearNav.svelte';
|
||||||
|
import YearControls from '$lib/components/organisms/YearControls.svelte';
|
||||||
|
import YearOverview from '$lib/components/organisms/YearOverview.svelte';
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<YearOverview data={data.year} />
|
||||||
|
<YearNav years={data.years} />
|
||||||
|
<YearControls />
|
||||||
|
<section class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each data.persons as person}
|
||||||
|
<PersonCard {...person} />
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-node';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
@@ -18,6 +18,7 @@ const config = {
|
|||||||
warningFilter: (warning) => {
|
warningFilter: (warning) => {
|
||||||
const ignoreCodes = [
|
const ignoreCodes = [
|
||||||
'a11y_no_static_element_interactions',
|
'a11y_no_static_element_interactions',
|
||||||
|
'a11y_no_noninteractive_element_interactions',
|
||||||
'a11y_click_events_have_key_events'
|
'a11y_click_events_have_key_events'
|
||||||
];
|
];
|
||||||
return !ignoreCodes.includes(warning.code);
|
return !ignoreCodes.includes(warning.code);
|
||||||
|
|||||||
Reference in New Issue
Block a user