Update user greeting to display email, add schemas for data validation, and enhance layout with new components
All checks were successful
main-branch-frovide/pipeline/head This commit looks good
All checks were successful
main-branch-frovide/pipeline/head This commit looks good
This commit is contained in:
parent
3a2b4ded8c
commit
2fa72ac137
251
package-lock.json
generated
251
package-lock.json
generated
@ -8,10 +8,16 @@
|
|||||||
"name": "frovide",
|
"name": "frovide",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/core": "^0.0.8",
|
||||||
|
"@dnd-kit-svelte/modifiers": "^0.0.8",
|
||||||
|
"@dnd-kit-svelte/sortable": "^0.0.8",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
|
"@tabler/icons-svelte": "^3.34.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.40.0",
|
"drizzle-orm": "^0.40.0",
|
||||||
"lucide": "^0.513.0",
|
"lucide": "^0.513.0",
|
||||||
@ -27,6 +33,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tanstack/table-core": "^8.21.3",
|
"@tanstack/table-core": "^8.21.3",
|
||||||
|
"@types/d3-scale": "^4.0.9",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"bits-ui": "^2.4.1",
|
"bits-ui": "^2.4.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -113,6 +120,195 @@
|
|||||||
"node": ">17.0.0"
|
"node": ">17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/accessibility": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit-svelte/accessibility/-/accessibility-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-Dm+6jTIuorEI4fkOh9ZcbVeJfUAPQGdseT2HfG9gu30PtxHTd9offH5cGWKxsdxjKZjkr5OH2O3DVeNtFjOPxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/utilities": "latest",
|
||||||
|
"esm-env": "^1.2.2",
|
||||||
|
"runed": "^0.23.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/accessibility/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/core": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit-svelte/core/-/core-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-c77kG70L2DX+af9NW7EbbByKXcHF4y+8sqbZjj81N6QnAtdqrfZdQug/39qSMC3hxLkiS1UowVhSQRJgpdFWGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/accessibility": "latest",
|
||||||
|
"@dnd-kit-svelte/utilities": "latest",
|
||||||
|
"runed": "^0.23.0",
|
||||||
|
"svelte-toolbelt": "^0.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/core/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/core/node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.23.2",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/modifiers": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit-svelte/modifiers/-/modifiers-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-EZyRNNm5yVl/ws86oLt1pJGNiB1QX1/xlHusL6yqtMylk9y486cp5C/BPdpvEtd6arK8XpuhNXV1dJAyYVsNrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/core": "latest",
|
||||||
|
"@dnd-kit-svelte/utilities": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/sortable": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit-svelte/sortable/-/sortable-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-4lHgd7ttFqQqfuxeg2GofqnM2nHrAqtFsxqXf/Vr7/s4rEusPO8cZkQ6n2KTr08lvHGpQM2l8aLBUDyQtWtCVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/core": "latest",
|
||||||
|
"@dnd-kit-svelte/utilities": "latest",
|
||||||
|
"runed": "^0.23.0",
|
||||||
|
"svelte-toolbelt": "^0.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/sortable/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/sortable/node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.23.2",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/utilities": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit-svelte/utilities/-/utilities-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-fiBPmbHS94cuigysqyRA7T8WpNP94uEw2DsYX9En81mOkAN9UH9RU2DlJ63luZwn4BG9k+DjYT0jt2AEaRuYIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"svelte-toolbelt": "^0.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/utilities/node_modules/runed": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte",
|
||||||
|
"https://github.com/sponsors/tglide"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"esm-env": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit-svelte/utilities/node_modules/svelte-toolbelt": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/huntabyte"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"runed": "^0.23.2",
|
||||||
|
"style-to-object": "^1.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18",
|
||||||
|
"pnpm": ">=8.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@drizzle-team/brocli": {
|
"node_modules/@drizzle-team/brocli": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||||
@ -2077,6 +2273,32 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tabler/icons": {
|
||||||
|
"version": "3.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz",
|
||||||
|
"integrity": "sha512-jtVqv0JC1WU2TTEBN32D9+R6mc1iEBuPwLnBsWaR02SIEciu9aq5806AWkCHuObhQ4ERhhXErLEK7Fs+tEZxiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tabler/icons-svelte": {
|
||||||
|
"version": "3.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tabler/icons-svelte/-/icons-svelte-3.34.0.tgz",
|
||||||
|
"integrity": "sha512-GK361EswvIZaXY0Eoa4V3lu7qAUQ7XXiPDEkS7+ZeNR5CULD8JkMUG15UITHSmUtu/q2RhCuhgtkW5rdZeIpFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons": "3.34.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/codecalm"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": ">=3 <6 || >=5.0.0-next.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.8",
|
"version": "4.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz",
|
||||||
@ -2384,6 +2606,23 @@
|
|||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
@ -2687,7 +2926,6 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"internmap": "1 - 2"
|
"internmap": "1 - 2"
|
||||||
@ -2700,7 +2938,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -2774,7 +3011,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -2823,7 +3059,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-color": "1 - 3"
|
"d3-color": "1 - 3"
|
||||||
@ -2843,7 +3078,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -2918,7 +3152,6 @@
|
|||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-array": "2.10.0 - 3",
|
"d3-array": "2.10.0 - 3",
|
||||||
@ -2949,7 +3182,6 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-path": "^3.1.0"
|
"d3-path": "^3.1.0"
|
||||||
@ -2969,7 +3201,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-array": "2 - 3"
|
"d3-array": "2 - 3"
|
||||||
@ -2982,7 +3213,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"d3-time": "1 - 3"
|
"d3-time": "1 - 3"
|
||||||
@ -3626,14 +3856,12 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
||||||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/internmap": {
|
"node_modules/internmap": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@ -4634,7 +4862,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
|
||||||
"integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
|
"integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inline-style-parser": "0.2.4"
|
"inline-style-parser": "0.2.4"
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@tanstack/table-core": "^8.21.3",
|
"@tanstack/table-core": "^8.21.3",
|
||||||
|
"@types/d3-scale": "^4.0.9",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"bits-ui": "^2.4.1",
|
"bits-ui": "^2.4.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -45,10 +46,16 @@
|
|||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit-svelte/core": "^0.0.8",
|
||||||
|
"@dnd-kit-svelte/modifiers": "^0.0.8",
|
||||||
|
"@dnd-kit-svelte/sortable": "^0.0.8",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.12",
|
"@sveltejs/adapter-node": "^5.2.12",
|
||||||
|
"@tabler/icons-svelte": "^3.34.0",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.2.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"drizzle-orm": "^0.40.0",
|
"drizzle-orm": "^0.40.0",
|
||||||
"lucide": "^0.513.0",
|
"lucide": "^0.513.0",
|
||||||
|
|||||||
164
src/lib/components/app-sidebar.svelte
Normal file
164
src/lib/components/app-sidebar.svelte
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CameraIcon from "@tabler/icons-svelte/icons/camera";
|
||||||
|
import ChartBarIcon from "@tabler/icons-svelte/icons/chart-bar";
|
||||||
|
import DashboardIcon from "@tabler/icons-svelte/icons/dashboard";
|
||||||
|
import DatabaseIcon from "@tabler/icons-svelte/icons/database";
|
||||||
|
import FileAiIcon from "@tabler/icons-svelte/icons/file-ai";
|
||||||
|
import FileDescriptionIcon from "@tabler/icons-svelte/icons/file-description";
|
||||||
|
import FileWordIcon from "@tabler/icons-svelte/icons/file-word";
|
||||||
|
import FolderIcon from "@tabler/icons-svelte/icons/folder";
|
||||||
|
import HelpIcon from "@tabler/icons-svelte/icons/help";
|
||||||
|
import InnerShadowTopIcon from "@tabler/icons-svelte/icons/inner-shadow-top";
|
||||||
|
import ListDetailsIcon from "@tabler/icons-svelte/icons/list-details";
|
||||||
|
import ReportIcon from "@tabler/icons-svelte/icons/report";
|
||||||
|
import SearchIcon from "@tabler/icons-svelte/icons/search";
|
||||||
|
import SettingsIcon from "@tabler/icons-svelte/icons/settings";
|
||||||
|
import UsersIcon from "@tabler/icons-svelte/icons/users";
|
||||||
|
import NavDocuments from "./nav-documents.svelte";
|
||||||
|
import NavMain from "./nav-main.svelte";
|
||||||
|
import NavSecondary from "./nav-secondary.svelte";
|
||||||
|
import NavUser from "./nav-user.svelte";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
const data = {
|
||||||
|
user: {
|
||||||
|
name: "shadcn",
|
||||||
|
email: "m@example.com",
|
||||||
|
avatar: "/avatars/shadcn.jpg",
|
||||||
|
},
|
||||||
|
navMain: [
|
||||||
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
url: "#",
|
||||||
|
icon: DashboardIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Lifecycle",
|
||||||
|
url: "#",
|
||||||
|
icon: ListDetailsIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Analytics",
|
||||||
|
url: "#",
|
||||||
|
icon: ChartBarIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Projects",
|
||||||
|
url: "#",
|
||||||
|
icon: FolderIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Team",
|
||||||
|
url: "#",
|
||||||
|
icon: UsersIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navClouds: [
|
||||||
|
{
|
||||||
|
title: "Capture",
|
||||||
|
icon: CameraIcon,
|
||||||
|
isActive: true,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Proposal",
|
||||||
|
icon: FileDescriptionIcon,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Prompts",
|
||||||
|
icon: FileAiIcon,
|
||||||
|
url: "#",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Active Proposals",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Archived",
|
||||||
|
url: "#",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navSecondary: [
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
url: "#",
|
||||||
|
icon: SettingsIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Get Help",
|
||||||
|
url: "#",
|
||||||
|
icon: HelpIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Search",
|
||||||
|
url: "#",
|
||||||
|
icon: SearchIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
name: "Data Library",
|
||||||
|
url: "#",
|
||||||
|
icon: DatabaseIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reports",
|
||||||
|
url: "#",
|
||||||
|
icon: ReportIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Word Assistant",
|
||||||
|
url: "#",
|
||||||
|
icon: FileWordIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let { ...restProps }: ComponentProps<typeof Sidebar.Root> = $props();
|
||||||
|
</script>
|
||||||
|
<Sidebar.Root collapsible="offcanvas" {...restProps}>
|
||||||
|
<Sidebar.Header>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton class="data-[slot=sidebar-menu-button]:!p-1.5">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href="##" {...props}>
|
||||||
|
<InnerShadowTopIcon class="!size-5" />
|
||||||
|
<span class="text-base font-semibold">Acme Inc.</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.Header>
|
||||||
|
<Sidebar.Content>
|
||||||
|
<NavMain items={data.navMain} />
|
||||||
|
<NavDocuments items={data.documents} />
|
||||||
|
<NavSecondary items={data.navSecondary} class="mt-auto" />
|
||||||
|
</Sidebar.Content>
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<NavUser user={data.user} />
|
||||||
|
</Sidebar.Footer>
|
||||||
|
</Sidebar.Root>
|
||||||
253
src/lib/components/chart-area-interactive.svelte
Normal file
253
src/lib/components/chart-area-interactive.svelte
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Chart from "$lib/components/ui/chart/index.js";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
|
||||||
|
import {scaleUtc} from "d3-scale";
|
||||||
|
import {Area, AreaChart} from "layerchart";
|
||||||
|
import {curveNatural} from "d3-shape";
|
||||||
|
|
||||||
|
const chartData = [
|
||||||
|
{date: new Date("2024-04-01"), desktop: 222, mobile: 150},
|
||||||
|
{date: new Date("2024-04-02"), desktop: 97, mobile: 180},
|
||||||
|
{date: new Date("2024-04-03"), desktop: 167, mobile: 120},
|
||||||
|
{date: new Date("2024-04-04"), desktop: 242, mobile: 260},
|
||||||
|
{date: new Date("2024-04-05"), desktop: 373, mobile: 290},
|
||||||
|
{date: new Date("2024-04-06"), desktop: 301, mobile: 340},
|
||||||
|
{date: new Date("2024-04-07"), desktop: 245, mobile: 180},
|
||||||
|
{date: new Date("2024-04-08"), desktop: 409, mobile: 320},
|
||||||
|
{date: new Date("2024-04-09"), desktop: 59, mobile: 110},
|
||||||
|
{date: new Date("2024-04-10"), desktop: 261, mobile: 190},
|
||||||
|
{date: new Date("2024-04-11"), desktop: 327, mobile: 350},
|
||||||
|
{date: new Date("2024-04-12"), desktop: 292, mobile: 210},
|
||||||
|
{date: new Date("2024-04-13"), desktop: 342, mobile: 380},
|
||||||
|
{date: new Date("2024-04-14"), desktop: 137, mobile: 220},
|
||||||
|
{date: new Date("2024-04-15"), desktop: 120, mobile: 170},
|
||||||
|
{date: new Date("2024-04-16"), desktop: 138, mobile: 190},
|
||||||
|
{date: new Date("2024-04-17"), desktop: 446, mobile: 360},
|
||||||
|
{date: new Date("2024-04-18"), desktop: 364, mobile: 410},
|
||||||
|
{date: new Date("2024-04-19"), desktop: 243, mobile: 180},
|
||||||
|
{date: new Date("2024-04-20"), desktop: 89, mobile: 150},
|
||||||
|
{date: new Date("2024-04-21"), desktop: 137, mobile: 200},
|
||||||
|
{date: new Date("2024-04-22"), desktop: 224, mobile: 170},
|
||||||
|
{date: new Date("2024-04-23"), desktop: 138, mobile: 230},
|
||||||
|
{date: new Date("2024-04-24"), desktop: 387, mobile: 290},
|
||||||
|
{date: new Date("2024-04-25"), desktop: 215, mobile: 250},
|
||||||
|
{date: new Date("2024-04-26"), desktop: 75, mobile: 130},
|
||||||
|
{date: new Date("2024-04-27"), desktop: 383, mobile: 420},
|
||||||
|
{date: new Date("2024-04-28"), desktop: 122, mobile: 180},
|
||||||
|
{date: new Date("2024-04-29"), desktop: 315, mobile: 240},
|
||||||
|
{date: new Date("2024-04-30"), desktop: 454, mobile: 380},
|
||||||
|
{date: new Date("2024-05-01"), desktop: 165, mobile: 220},
|
||||||
|
{date: new Date("2024-05-02"), desktop: 293, mobile: 310},
|
||||||
|
{date: new Date("2024-05-03"), desktop: 247, mobile: 190},
|
||||||
|
{date: new Date("2024-05-04"), desktop: 385, mobile: 420},
|
||||||
|
{date: new Date("2024-05-05"), desktop: 481, mobile: 390},
|
||||||
|
{date: new Date("2024-05-06"), desktop: 498, mobile: 520},
|
||||||
|
{date: new Date("2024-05-07"), desktop: 388, mobile: 300},
|
||||||
|
{date: new Date("2024-05-08"), desktop: 149, mobile: 210},
|
||||||
|
{date: new Date("2024-05-09"), desktop: 227, mobile: 180},
|
||||||
|
{date: new Date("2024-05-10"), desktop: 293, mobile: 330},
|
||||||
|
{date: new Date("2024-05-11"), desktop: 335, mobile: 270},
|
||||||
|
{date: new Date("2024-05-12"), desktop: 197, mobile: 240},
|
||||||
|
{date: new Date("2024-05-13"), desktop: 197, mobile: 160},
|
||||||
|
{date: new Date("2024-05-14"), desktop: 448, mobile: 490},
|
||||||
|
{date: new Date("2024-05-15"), desktop: 473, mobile: 380},
|
||||||
|
{date: new Date("2024-05-16"), desktop: 338, mobile: 400},
|
||||||
|
{date: new Date("2024-05-17"), desktop: 499, mobile: 420},
|
||||||
|
{date: new Date("2024-05-18"), desktop: 315, mobile: 350},
|
||||||
|
{date: new Date("2024-05-19"), desktop: 235, mobile: 180},
|
||||||
|
{date: new Date("2024-05-20"), desktop: 177, mobile: 230},
|
||||||
|
{date: new Date("2024-05-21"), desktop: 82, mobile: 140},
|
||||||
|
{date: new Date("2024-05-22"), desktop: 81, mobile: 120},
|
||||||
|
{date: new Date("2024-05-23"), desktop: 252, mobile: 290},
|
||||||
|
{date: new Date("2024-05-24"), desktop: 294, mobile: 220},
|
||||||
|
{date: new Date("2024-05-25"), desktop: 201, mobile: 250},
|
||||||
|
{date: new Date("2024-05-26"), desktop: 213, mobile: 170},
|
||||||
|
{date: new Date("2024-05-27"), desktop: 420, mobile: 460},
|
||||||
|
{date: new Date("2024-05-28"), desktop: 233, mobile: 190},
|
||||||
|
{date: new Date("2024-05-29"), desktop: 78, mobile: 130},
|
||||||
|
{date: new Date("2024-05-30"), desktop: 340, mobile: 280},
|
||||||
|
{date: new Date("2024-05-31"), desktop: 178, mobile: 230},
|
||||||
|
{date: new Date("2024-06-01"), desktop: 178, mobile: 200},
|
||||||
|
{date: new Date("2024-06-02"), desktop: 470, mobile: 410},
|
||||||
|
{date: new Date("2024-06-03"), desktop: 103, mobile: 160},
|
||||||
|
{date: new Date("2024-06-04"), desktop: 439, mobile: 380},
|
||||||
|
{date: new Date("2024-06-05"), desktop: 88, mobile: 140},
|
||||||
|
{date: new Date("2024-06-06"), desktop: 294, mobile: 250},
|
||||||
|
{date: new Date("2024-06-07"), desktop: 323, mobile: 370},
|
||||||
|
{date: new Date("2024-06-08"), desktop: 385, mobile: 320},
|
||||||
|
{date: new Date("2024-06-09"), desktop: 438, mobile: 480},
|
||||||
|
{date: new Date("2024-06-10"), desktop: 155, mobile: 200},
|
||||||
|
{date: new Date("2024-06-11"), desktop: 92, mobile: 150},
|
||||||
|
{date: new Date("2024-06-12"), desktop: 492, mobile: 420},
|
||||||
|
{date: new Date("2024-06-13"), desktop: 81, mobile: 130},
|
||||||
|
{date: new Date("2024-06-14"), desktop: 426, mobile: 380},
|
||||||
|
{date: new Date("2024-06-15"), desktop: 307, mobile: 350},
|
||||||
|
{date: new Date("2024-06-16"), desktop: 371, mobile: 310},
|
||||||
|
{date: new Date("2024-06-17"), desktop: 475, mobile: 520},
|
||||||
|
{date: new Date("2024-06-18"), desktop: 107, mobile: 170},
|
||||||
|
{date: new Date("2024-06-19"), desktop: 341, mobile: 290},
|
||||||
|
{date: new Date("2024-06-20"), desktop: 408, mobile: 450},
|
||||||
|
{date: new Date("2024-06-21"), desktop: 169, mobile: 210},
|
||||||
|
{date: new Date("2024-06-22"), desktop: 317, mobile: 270},
|
||||||
|
{date: new Date("2024-06-23"), desktop: 480, mobile: 530},
|
||||||
|
{date: new Date("2024-06-24"), desktop: 132, mobile: 180},
|
||||||
|
{date: new Date("2024-06-25"), desktop: 141, mobile: 190},
|
||||||
|
{date: new Date("2024-06-26"), desktop: 434, mobile: 380},
|
||||||
|
{date: new Date("2024-06-27"), desktop: 448, mobile: 490},
|
||||||
|
{date: new Date("2024-06-28"), desktop: 149, mobile: 200},
|
||||||
|
{date: new Date("2024-06-29"), desktop: 103, mobile: 160},
|
||||||
|
{date: new Date("2024-06-30"), desktop: 446, mobile: 400},
|
||||||
|
];
|
||||||
|
let timeRange = $state("90d");
|
||||||
|
const selectedLabel = $derived.by(() => {
|
||||||
|
switch (timeRange) {
|
||||||
|
case "90d":
|
||||||
|
return "Last 3 months";
|
||||||
|
case "30d":
|
||||||
|
return "Last 30 days";
|
||||||
|
case "7d":
|
||||||
|
return "Last 7 days";
|
||||||
|
default:
|
||||||
|
return "Last 3 months";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const filteredData = $derived(
|
||||||
|
chartData.filter((item) => {
|
||||||
|
const referenceDate = new Date("2024-06-30");
|
||||||
|
let daysToSubtract = 90;
|
||||||
|
if (timeRange === "30d") {
|
||||||
|
daysToSubtract = 30;
|
||||||
|
} else if (timeRange === "7d") {
|
||||||
|
daysToSubtract = 7;
|
||||||
|
}
|
||||||
|
referenceDate.setDate(referenceDate.getDate() - daysToSubtract);
|
||||||
|
return item.date >= referenceDate;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const chartConfig = {
|
||||||
|
desktop: {label: "Desktop", color: "var(--primary)"},
|
||||||
|
mobile: {label: "Mobile", color: "var(--primary)"},
|
||||||
|
} satisfies Chart.ChartConfig;
|
||||||
|
</script>
|
||||||
|
<Card.Root class="@container/card">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>Total Visitors</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
<span class="@[540px]/card:block hidden"> Total for the last 3 months </span>
|
||||||
|
<span class="@[540px]/card:hidden">Last 3 months</span>
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Action>
|
||||||
|
<ToggleGroup.Root
|
||||||
|
type="single"
|
||||||
|
bind:value={timeRange}
|
||||||
|
variant="outline"
|
||||||
|
class="@[767px]/card:flex hidden *:data-[slot=toggle-group-item]:!px-4"
|
||||||
|
>
|
||||||
|
<ToggleGroup.Item value="90d">Last 3 months</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item value="30d">Last 30 days</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item value="7d">Last 7 days</ToggleGroup.Item>
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
<Select.Root type="single" bind:value={timeRange}>
|
||||||
|
<Select.Trigger
|
||||||
|
size="sm"
|
||||||
|
class="**:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden flex w-40"
|
||||||
|
aria-label="Select a value"
|
||||||
|
>
|
||||||
|
<span data-slot="select-value">
|
||||||
|
{selectedLabel}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content class="rounded-xl">
|
||||||
|
<Select.Item value="90d" class="rounded-lg">Last 3 months</Select.Item>
|
||||||
|
<Select.Item value="30d" class="rounded-lg">Last 30 days</Select.Item>
|
||||||
|
<Select.Item value="7d" class="rounded-lg">Last 7 days</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Card.Action>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||||
|
<Chart.Container config={chartConfig} class="aspect-auto h-[250px] w-full">
|
||||||
|
<AreaChart
|
||||||
|
legend
|
||||||
|
data={filteredData}
|
||||||
|
x="date"
|
||||||
|
xScale={scaleUtc()}
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
key: "mobile",
|
||||||
|
label: "Mobile",
|
||||||
|
color: chartConfig.mobile.color,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "desktop",
|
||||||
|
label: "Desktop",
|
||||||
|
color: chartConfig.desktop.color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
seriesLayout="stack"
|
||||||
|
props={{
|
||||||
|
area: {
|
||||||
|
curve: curveNatural,
|
||||||
|
"fill-opacity": 0.4,
|
||||||
|
line: { class: "stroke-1" },
|
||||||
|
motion: "tween",
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
ticks: timeRange === "7d" ? 7 : undefined,
|
||||||
|
format: (v) => {
|
||||||
|
return v.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: { format: () => "" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet marks({series, getAreaProps})}
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop
|
||||||
|
offset="5%"
|
||||||
|
stop-color="var(--color-desktop)"
|
||||||
|
stop-opacity={1.0}
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="95%"
|
||||||
|
stop-color="var(--color-desktop)"
|
||||||
|
stop-opacity={0.1}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stop-color="var(--color-mobile)" stop-opacity={0.8}/>
|
||||||
|
<stop
|
||||||
|
offset="95%"
|
||||||
|
stop-color="var(--color-mobile)"
|
||||||
|
stop-opacity={0.1}
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{#each series as s, i (s.key)}
|
||||||
|
<Area
|
||||||
|
{...getAreaProps(s, i)}
|
||||||
|
fill={s.key === "desktop" ? "url(#fillDesktop)" : "url(#fillMobile)"}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/snippet}
|
||||||
|
{#snippet tooltip()}
|
||||||
|
<Chart.Tooltip
|
||||||
|
labelFormatter={(v: Date) => {
|
||||||
|
return v.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
indicator="line"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AreaChart>
|
||||||
|
</Chart.Container>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
189
src/lib/components/data-table-cell-viewer.svelte
Normal file
189
src/lib/components/data-table-cell-viewer.svelte
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TrendingUpIcon from "@tabler/icons-svelte/icons/trending-up";
|
||||||
|
import { AreaChart } from "layerchart";
|
||||||
|
import { scaleUtc } from "d3-scale";
|
||||||
|
import { curveNatural } from "d3-shape";
|
||||||
|
import * as Drawer from "$lib/components/ui/drawer/index.js";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Chart from "$lib/components/ui/chart/index.js";
|
||||||
|
import { IsMobile } from "$lib/hooks/is-mobile.svelte.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import { Input } from "$lib/components/ui/input/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import type { Schema } from "./schemas.js";
|
||||||
|
const chartData = [
|
||||||
|
{ date: new Date("2024-01-01"), desktop: 186, mobile: 80 },
|
||||||
|
{ date: new Date("2024-02-01"), desktop: 305, mobile: 200 },
|
||||||
|
{ date: new Date("2024-03-01"), desktop: 237, mobile: 120 },
|
||||||
|
{ date: new Date("2024-04-01"), desktop: 73, mobile: 190 },
|
||||||
|
{ date: new Date("2024-05-01"), desktop: 209, mobile: 130 },
|
||||||
|
{ date: new Date("2024-06-01"), desktop: 214, mobile: 140 },
|
||||||
|
];
|
||||||
|
const chartConfig = {
|
||||||
|
desktop: {
|
||||||
|
label: "Desktop",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
mobile: {
|
||||||
|
label: "Mobile",
|
||||||
|
color: "var(--primary)",
|
||||||
|
},
|
||||||
|
} satisfies Chart.ChartConfig;
|
||||||
|
const isMobile = new IsMobile();
|
||||||
|
let { item }: { item: Schema } = $props();
|
||||||
|
let type = $state(item.type);
|
||||||
|
let status = $state(item.status);
|
||||||
|
let reviewer = $state(item.reviewer);
|
||||||
|
</script>
|
||||||
|
<Drawer.Root direction={isMobile.current ? "bottom" : "right"}>
|
||||||
|
<Drawer.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="link" class="text-foreground w-fit px-0 text-left" {...props}>
|
||||||
|
{item.header}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Drawer.Trigger>
|
||||||
|
<Drawer.Content>
|
||||||
|
<Drawer.Header class="gap-1">
|
||||||
|
<Drawer.Title>{item.header}</Drawer.Title>
|
||||||
|
<Drawer.Description>Showing total visitors for the last 6 months</Drawer.Description>
|
||||||
|
</Drawer.Header>
|
||||||
|
<div class="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||||
|
{#if !isMobile.current}
|
||||||
|
<Chart.Container config={chartConfig}>
|
||||||
|
<AreaChart
|
||||||
|
data={chartData}
|
||||||
|
x="date"
|
||||||
|
xScale={scaleUtc()}
|
||||||
|
yDomain={[0, 600]}
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
key: "mobile",
|
||||||
|
label: "Mobile",
|
||||||
|
color: chartConfig.mobile.color,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "desktop",
|
||||||
|
label: "Desktop",
|
||||||
|
color: chartConfig.desktop.color,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
seriesLayout="stack"
|
||||||
|
props={{
|
||||||
|
area: {
|
||||||
|
curve: curveNatural,
|
||||||
|
"fill-opacity": 0.4,
|
||||||
|
line: { class: "stroke-1" },
|
||||||
|
motion: "tween",
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
format: (v) => v.toLocaleDateString("en-US", { month: "short" }),
|
||||||
|
},
|
||||||
|
yAxis: { ticks: [0, 300, 600] },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#snippet tooltip()}
|
||||||
|
<Chart.Tooltip
|
||||||
|
labelFormatter={(v: Date) => {
|
||||||
|
return v.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
indicator="dot"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
</AreaChart>
|
||||||
|
</Chart.Container>
|
||||||
|
<Separator />
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="flex gap-2 font-medium leading-none">
|
||||||
|
Trending up by 5.2% this month
|
||||||
|
<TrendingUpIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
Showing total visitors for the last 6 months. This is just some random text
|
||||||
|
to test the layout. It spans multiple lines and should wrap around.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
{/if}
|
||||||
|
<form class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="header">Header</Label>
|
||||||
|
<Input id="header" value={item.header} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="type">Type</Label>
|
||||||
|
<Select.Root type="single" bind:value={type}>
|
||||||
|
<Select.Trigger id="type" class="w-full">
|
||||||
|
{type ?? "Select a type"}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="Table of Contents"
|
||||||
|
>Table of Contents</Select.Item
|
||||||
|
>
|
||||||
|
<Select.Item value="Executive Summary"
|
||||||
|
>Executive Summary</Select.Item
|
||||||
|
>
|
||||||
|
<Select.Item value="Technical Approach">
|
||||||
|
Technical Approach
|
||||||
|
</Select.Item>
|
||||||
|
<Select.Item value="Design">Design</Select.Item>
|
||||||
|
<Select.Item value="Capabilities">Capabilities</Select.Item>
|
||||||
|
<Select.Item value="Focus Documents">Focus Documents</Select.Item>
|
||||||
|
<Select.Item value="Narrative">Narrative</Select.Item>
|
||||||
|
<Select.Item value="Cover Page">Cover Page</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="status">Status</Label>
|
||||||
|
<Select.Root type="single" bind:value={status}>
|
||||||
|
<Select.Trigger id="status" class="w-full">
|
||||||
|
{status ?? "Select a status"}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="Done">Done</Select.Item>
|
||||||
|
<Select.Item value="In Progress">In Progress</Select.Item>
|
||||||
|
<Select.Item value="Not Started">Not Started</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="target">Target</Label>
|
||||||
|
<Input id="target" value={item.target} />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="limit">Limit</Label>
|
||||||
|
<Input id="limit" value={item.limit} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Label for="reviewer">Reviewer</Label>
|
||||||
|
<Select.Root type="single" bind:value={reviewer}>
|
||||||
|
<Select.Trigger id="reviewer" class="w-full">
|
||||||
|
{reviewer ?? "Select a reviewer"}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="Eddie Lake">Eddie Lake</Select.Item>
|
||||||
|
<Select.Item value="Jamik Tashpulatov">Jamik Tashpulatov</Select.Item>
|
||||||
|
<Select.Item value="Emily Whalen">Emily Whalen</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<Drawer.Footer>
|
||||||
|
<Button>Submit</Button>
|
||||||
|
<Drawer.Close>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" {...props}>Done</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Drawer.Close>
|
||||||
|
</Drawer.Footer>
|
||||||
|
</Drawer.Content>
|
||||||
|
</Drawer.Root>
|
||||||
12
src/lib/components/data-table-checkbox.svelte
Normal file
12
src/lib/components/data-table-checkbox.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
let {
|
||||||
|
checked = false,
|
||||||
|
onCheckedChange = (v) => (checked = v),
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Checkbox> = $props();
|
||||||
|
</script>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<Checkbox bind:checked={() => checked, onCheckedChange} {...restProps} />
|
||||||
|
</div>
|
||||||
29
src/lib/components/data-table-reviewer.svelte
Normal file
29
src/lib/components/data-table-reviewer.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Row } from "@tanstack/table-core";
|
||||||
|
import type { Schema } from "./schemas.js";
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
let { row }: { row: Row<Schema> } = $props();
|
||||||
|
const isAssigned = $derived(row.original.reviewer !== "Assign reviewer");
|
||||||
|
let reviewer = $state("");
|
||||||
|
</script>
|
||||||
|
{#if isAssigned}
|
||||||
|
{row.original.reviewer}
|
||||||
|
{:else}
|
||||||
|
<Label for="{row.original.id}-reviewer" class="sr-only">Reviewer</Label>
|
||||||
|
<Select.Root type="single" bind:value={reviewer}>
|
||||||
|
<Select.Trigger
|
||||||
|
class="w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate"
|
||||||
|
size="sm"
|
||||||
|
id="{row.original.id}-reviewer"
|
||||||
|
>
|
||||||
|
<span data-slot="select-value">
|
||||||
|
{reviewer ?? "Assign reviewer"}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content align="end">
|
||||||
|
<Select.Item value="Eddie Lake">Eddie Lake</Select.Item>
|
||||||
|
<Select.Item value="Jamik Tashpulatov">Jamik Tashpulatov</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
{/if}
|
||||||
547
src/lib/components/data-table.svelte
Normal file
547
src/lib/components/data-table.svelte
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
export const columns: ColumnDef<Schema>[] = [
|
||||||
|
{
|
||||||
|
id: "drag",
|
||||||
|
header: () => null,
|
||||||
|
cell: ({row}) => renderSnippet(DragHandle, {id: row.original.id}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({table}) =>
|
||||||
|
renderComponent(DataTableCheckbox, {
|
||||||
|
checked: table.getIsAllPageRowsSelected(),
|
||||||
|
indeterminate:
|
||||||
|
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
|
||||||
|
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
|
||||||
|
"aria-label": "Select all",
|
||||||
|
}),
|
||||||
|
cell: ({row}) =>
|
||||||
|
renderComponent(DataTableCheckbox, {
|
||||||
|
checked: row.getIsSelected(),
|
||||||
|
onCheckedChange: (value) => row.toggleSelected(!!value),
|
||||||
|
"aria-label": "Select row",
|
||||||
|
}),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "header",
|
||||||
|
header: "Header",
|
||||||
|
cell: ({row}) => renderComponent(DataTableCellViewer, {item: row.original}),
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "type",
|
||||||
|
header: "Section Type",
|
||||||
|
cell: ({row}) => renderSnippet(DataTableType, {row}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({row}) => renderSnippet(DataTableStatus, {row}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "target",
|
||||||
|
header: () =>
|
||||||
|
renderSnippet(
|
||||||
|
createRawSnippet(() => ({
|
||||||
|
render: () => '<div class="w-full text-right">Target</div>',
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
cell: ({row}) => renderSnippet(DataTableTarget, {row}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "limit",
|
||||||
|
header: () =>
|
||||||
|
renderSnippet(
|
||||||
|
createRawSnippet(() => ({
|
||||||
|
render: () => '<div class="w-full text-right">Limit</div>',
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
cell: ({row}) => renderSnippet(DataTableLimit, {row}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "reviewer",
|
||||||
|
header: "Reviewer",
|
||||||
|
cell: ({row}) => renderComponent(DataTableReviewer, {row}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
cell: () => renderSnippet(DataTableActions),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
getCoreRowModel,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type PaginationState,
|
||||||
|
type Row,
|
||||||
|
type RowSelectionState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
|
import type {Schema} from "./schemas.js";
|
||||||
|
import {
|
||||||
|
useSensors,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
KeyboardSensor,
|
||||||
|
useSensor,
|
||||||
|
type DragEndEvent,
|
||||||
|
type UniqueIdentifier,
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
} from "@dnd-kit-svelte/core";
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
useSortable,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit-svelte/sortable";
|
||||||
|
import {restrictToVerticalAxis} from "@dnd-kit-svelte/modifiers";
|
||||||
|
import {createSvelteTable} from "$lib/components/ui/data-table/data-table.svelte.js";
|
||||||
|
import * as Tabs from "$lib/components/ui/tabs/index.js";
|
||||||
|
import * as Table from "$lib/components/ui/table/index.js";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import {Button} from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Select from "$lib/components/ui/select/index.js";
|
||||||
|
import {Label} from "$lib/components/ui/label/index.js";
|
||||||
|
import {Badge} from "$lib/components/ui/badge/index.js";
|
||||||
|
import {Input} from "$lib/components/ui/input/index.js";
|
||||||
|
import {
|
||||||
|
FlexRender,
|
||||||
|
renderComponent,
|
||||||
|
renderSnippet,
|
||||||
|
} from "$lib/components/ui/data-table/index.js";
|
||||||
|
import LayoutColumnsIcon from "@tabler/icons-svelte/icons/layout-columns";
|
||||||
|
import GripVerticalIcon from "@tabler/icons-svelte/icons/grip-vertical";
|
||||||
|
import ChevronDownIcon from "@tabler/icons-svelte/icons/chevron-down";
|
||||||
|
import PlusIcon from "@tabler/icons-svelte/icons/plus";
|
||||||
|
import ChevronsLeftIcon from "@tabler/icons-svelte/icons/chevrons-left";
|
||||||
|
import ChevronLeftIcon from "@tabler/icons-svelte/icons/chevron-left";
|
||||||
|
import ChevronRightIcon from "@tabler/icons-svelte/icons/chevron-right";
|
||||||
|
import ChevronsRightIcon from "@tabler/icons-svelte/icons/chevrons-right";
|
||||||
|
import CircleCheckFilledIcon from "@tabler/icons-svelte/icons/circle-check-filled";
|
||||||
|
import LoaderIcon from "@tabler/icons-svelte/icons/loader";
|
||||||
|
import DotsVerticalIcon from "@tabler/icons-svelte/icons/dots-vertical";
|
||||||
|
import {toast} from "svelte-sonner";
|
||||||
|
import DataTableCheckbox from "./data-table-checkbox.svelte";
|
||||||
|
import DataTableCellViewer from "./data-table-cell-viewer.svelte";
|
||||||
|
import {createRawSnippet} from "svelte";
|
||||||
|
import DataTableReviewer from "./data-table-reviewer.svelte";
|
||||||
|
import {CSS} from "@dnd-kit-svelte/utilities";
|
||||||
|
|
||||||
|
let {data}: { data: Schema[] } = $props();
|
||||||
|
let pagination = $state<PaginationState>({pageIndex: 0, pageSize: 10});
|
||||||
|
let sorting = $state<SortingState>([]);
|
||||||
|
let columnFilters = $state<ColumnFiltersState>([]);
|
||||||
|
let rowSelection = $state<RowSelectionState>({});
|
||||||
|
let columnVisibility = $state<VisibilityState>({});
|
||||||
|
const sortableId = $props.id();
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(MouseSensor, {}),
|
||||||
|
useSensor(TouchSensor, {}),
|
||||||
|
useSensor(KeyboardSensor, {})
|
||||||
|
);
|
||||||
|
const dataIds: UniqueIdentifier[] = $derived(data.map((item) => item.id));
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
get pagination() {
|
||||||
|
return pagination;
|
||||||
|
},
|
||||||
|
get sorting() {
|
||||||
|
return sorting;
|
||||||
|
},
|
||||||
|
get columnVisibility() {
|
||||||
|
return columnVisibility;
|
||||||
|
},
|
||||||
|
get rowSelection() {
|
||||||
|
return rowSelection;
|
||||||
|
},
|
||||||
|
get columnFilters() {
|
||||||
|
return columnFilters;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id.toString(),
|
||||||
|
enableRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFacetedRowModel: getFacetedRowModel(),
|
||||||
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
pagination = updater(pagination);
|
||||||
|
} else {
|
||||||
|
pagination = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
sorting = updater(sorting);
|
||||||
|
} else {
|
||||||
|
sorting = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onColumnFiltersChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
columnFilters = updater(columnFilters);
|
||||||
|
} else {
|
||||||
|
columnFilters = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onColumnVisibilityChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
columnVisibility = updater(columnVisibility);
|
||||||
|
} else {
|
||||||
|
columnVisibility = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRowSelectionChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
rowSelection = updater(rowSelection);
|
||||||
|
} else {
|
||||||
|
rowSelection = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDragEnd(event: DragEndEvent) {
|
||||||
|
const {active, over} = event;
|
||||||
|
if (active && over && active.id !== over.id) {
|
||||||
|
const oldIndex = dataIds.indexOf(active.id);
|
||||||
|
const newIndex = dataIds.indexOf(over.id);
|
||||||
|
data = arrayMove(data, oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let views = [
|
||||||
|
{
|
||||||
|
id: "outline",
|
||||||
|
label: "Outline",
|
||||||
|
badge: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "past-performance",
|
||||||
|
label: "Past Performance",
|
||||||
|
badge: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "key-personnel",
|
||||||
|
label: "Key Personnel",
|
||||||
|
badge: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "focus-documents",
|
||||||
|
label: "Focus Documents",
|
||||||
|
badge: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let view = $state("outline");
|
||||||
|
let viewLabel = $derived(views.find((v) => view === v.id)?.label ?? "Select a view");
|
||||||
|
</script>
|
||||||
|
<Tabs.Root value="outline" class="w-full flex-col justify-start gap-6">
|
||||||
|
<div class="flex items-center justify-between px-4 lg:px-6">
|
||||||
|
<Label for="view-selector" class="sr-only">View</Label>
|
||||||
|
<Select.Root type="single" bind:value={view}>
|
||||||
|
<Select.Trigger class="@4xl/main:hidden flex w-fit" size="sm" id="view-selector">
|
||||||
|
{viewLabel}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each views as view (view.id)}
|
||||||
|
<Select.Item value={view.id}>{view.label}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
<Tabs.List
|
||||||
|
class="**:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex hidden"
|
||||||
|
>
|
||||||
|
{#each views as view (view.id)}
|
||||||
|
<Tabs.Trigger value={view.id}>
|
||||||
|
{view.label}
|
||||||
|
{#if view.badge > 0}
|
||||||
|
<Badge variant="secondary">{view.badge}</Badge>
|
||||||
|
{/if}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
{/each}
|
||||||
|
</Tabs.List>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({props})}
|
||||||
|
<Button variant="outline" size="sm" {...props}>
|
||||||
|
<LayoutColumnsIcon/>
|
||||||
|
<span class="hidden lg:inline">Customize Columns</span>
|
||||||
|
<span class="lg:hidden">Columns</span>
|
||||||
|
<ChevronDownIcon/>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end" class="w-56">
|
||||||
|
{#each table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((col) => typeof col.accessorFn !== "undefined" && col.getCanHide()) as column (column.id)}
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
class="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
{/each}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<PlusIcon/>
|
||||||
|
<span class="hidden lg:inline">Add Section</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tabs.Content value="outline" class="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
|
||||||
|
<div class="overflow-hidden rounded-lg border">
|
||||||
|
<DndContext
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
{sensors}
|
||||||
|
id={sortableId}
|
||||||
|
>
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header class="bg-muted sticky top-0 z-10">
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<Table.Row>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<Table.Head colspan={header.colSpan}>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender
|
||||||
|
content={header.column.columnDef.header}
|
||||||
|
context={header.getContext()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Table.Head>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body class="**:data-[slot=table-cell]:first:w-8">
|
||||||
|
{#if table.getRowModel().rows?.length}
|
||||||
|
<SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
{@render DraggableRow({row})}
|
||||||
|
{/each}
|
||||||
|
</SortableContext>
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between px-4">
|
||||||
|
<div class="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} of
|
||||||
|
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full items-center gap-8 lg:w-fit">
|
||||||
|
<div class="hidden items-center gap-2 lg:flex">
|
||||||
|
<Label for="rows-per-page" class="text-sm font-medium">Rows per page</Label>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
bind:value={
|
||||||
|
() => `${table.getState().pagination.pageSize}`,
|
||||||
|
(v) => table.setPageSize(Number(v))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select.Trigger size="sm" class="w-20" id="rows-per-page">
|
||||||
|
{table.getState().pagination.pageSize}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content side="top">
|
||||||
|
{#each [10, 20, 30, 40, 50] as pageSize (pageSize)}
|
||||||
|
<Select.Item value={pageSize.toString()}>
|
||||||
|
{pageSize}
|
||||||
|
</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-fit items-center justify-center text-sm font-medium">
|
||||||
|
Page {table.getState().pagination.pageIndex + 1} of
|
||||||
|
{table.getPageCount()}
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto flex items-center gap-2 lg:ml-0">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onclick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeftIcon/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="size-8"
|
||||||
|
size="icon"
|
||||||
|
onclick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeftIcon/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="size-8"
|
||||||
|
size="icon"
|
||||||
|
onclick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to next page</span>
|
||||||
|
<ChevronRightIcon/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="hidden size-8 lg:flex"
|
||||||
|
size="icon"
|
||||||
|
onclick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span class="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRightIcon/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="past-performance" class="flex flex-col px-4 lg:px-6">
|
||||||
|
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="key-personnel" class="flex flex-col px-4 lg:px-6">
|
||||||
|
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</Tabs.Content>
|
||||||
|
<Tabs.Content value="focus-documents" class="flex flex-col px-4 lg:px-6">
|
||||||
|
<div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
{#snippet DataTableLimit({row}: { row: Row<Schema> })}
|
||||||
|
<form
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||||
|
loading: `Saving ${row.original.header}`,
|
||||||
|
success: "Done",
|
||||||
|
error: "Error",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label for="{row.original.id}-limit" class="sr-only">Limit</Label>
|
||||||
|
<Input
|
||||||
|
class="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||||
|
value={row.original.limit}
|
||||||
|
id="{row.original.id}-limit"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DataTableTarget({row}: { row: Row<Schema> })}
|
||||||
|
<form
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
toast.promise(new Promise((resolve) => setTimeout(resolve, 1000)), {
|
||||||
|
loading: `Saving ${row.original.header}`,
|
||||||
|
success: "Done",
|
||||||
|
error: "Error",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label for="{row.original.id}-target" class="sr-only">Target</Label>
|
||||||
|
<Input
|
||||||
|
class="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||||
|
value={row.original.target}
|
||||||
|
id="{row.original.id}-target"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DataTableType({row}: { row: Row<Schema> })}
|
||||||
|
<div class="w-32">
|
||||||
|
<Badge variant="outline" class="text-muted-foreground px-1.5">
|
||||||
|
{row.original.type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DataTableStatus({row}: { row: Row<Schema> })}
|
||||||
|
<Badge variant="outline" class="text-muted-foreground px-1.5">
|
||||||
|
{#if row.original.status === "Done"}
|
||||||
|
<CircleCheckFilledIcon class="fill-green-500 dark:fill-green-400"/>
|
||||||
|
{:else}
|
||||||
|
<LoaderIcon/>
|
||||||
|
{/if}
|
||||||
|
{row.original.status}
|
||||||
|
</Badge>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DataTableActions()}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger class="data-[state=open]:bg-muted text-muted-foreground flex size-8">
|
||||||
|
{#snippet child({props})}
|
||||||
|
<Button variant="ghost" size="icon" {...props}>
|
||||||
|
<DotsVerticalIcon/>
|
||||||
|
<span class="sr-only">Open menu</span>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align="end" class="w-32">
|
||||||
|
<DropdownMenu.Item>Edit</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Make a copy</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>Favorite</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator/>
|
||||||
|
<DropdownMenu.Item variant="destructive">Delete</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DraggableRow({row}: { row: Row<Schema> })}
|
||||||
|
{@const {transform, transition, node, isDragging} = useSortable({
|
||||||
|
id: () => row.original.id,
|
||||||
|
})}
|
||||||
|
<Table.Row
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
data-dragging={isDragging.current}
|
||||||
|
bind:ref={node.current}
|
||||||
|
class="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
|
||||||
|
style="transition: {transition.current}; transform: {CSS.Transform.toString(
|
||||||
|
transform.current
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<Table.Cell>
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()}/>
|
||||||
|
</Table.Cell>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet DragHandle({id}: { id: number })}
|
||||||
|
{@const {attributes, listeners} = useSortable({id: () => id})}
|
||||||
|
<Button
|
||||||
|
{...attributes.current}
|
||||||
|
{...listeners.current}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="text-muted-foreground size-7 hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<GripVerticalIcon class="text-muted-foreground size-3"/>
|
||||||
|
<span class="sr-only">Drag to reorder</span>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
67
src/lib/components/nav-documents.svelte
Normal file
67
src/lib/components/nav-documents.svelte
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DotsIcon from "@tabler/icons-svelte/icons/dots";
|
||||||
|
import FolderIcon from "@tabler/icons-svelte/icons/folder";
|
||||||
|
import Share3Icon from "@tabler/icons-svelte/icons/share-3";
|
||||||
|
import TrashIcon from "@tabler/icons-svelte/icons/trash";
|
||||||
|
import type { Icon } from "@tabler/icons-svelte";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
let { items }: { items: { name: string; url: string; icon: Icon }[] } = $props();
|
||||||
|
const sidebar = Sidebar.useSidebar();
|
||||||
|
</script>
|
||||||
|
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||||
|
<Sidebar.GroupLabel>Documents</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.name)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a {...props} href={item.url}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuAction
|
||||||
|
{...props}
|
||||||
|
showOnHover
|
||||||
|
class="data-[state=open]:bg-accent rounded-sm"
|
||||||
|
>
|
||||||
|
<DotsIcon />
|
||||||
|
<span class="sr-only">More</span>
|
||||||
|
</Sidebar.MenuAction>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-24 rounded-lg"
|
||||||
|
side={sidebar.isMobile ? "bottom" : "right"}
|
||||||
|
align={sidebar.isMobile ? "end" : "start"}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<FolderIcon />
|
||||||
|
<span>Open</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<Share3Icon />
|
||||||
|
<span>Share</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item variant="destructive">
|
||||||
|
<TrashIcon />
|
||||||
|
<span>Delete</span>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton class="text-sidebar-foreground/70">
|
||||||
|
<DotsIcon class="text-sidebar-foreground/70" />
|
||||||
|
<span>More</span>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.Group>
|
||||||
43
src/lib/components/nav-main.svelte
Normal file
43
src/lib/components/nav-main.svelte
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CirclePlusFilledIcon from "@tabler/icons-svelte/icons/circle-plus-filled";
|
||||||
|
import MailIcon from "@tabler/icons-svelte/icons/mail";
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import type { Icon } from "@tabler/icons-svelte";
|
||||||
|
let { items }: { items: { title: string; url: string; icon?: Icon }[] } = $props();
|
||||||
|
</script>
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupContent class="flex flex-col gap-2">
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem class="flex items-center gap-2">
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
class="bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground min-w-8 duration-200 ease-linear"
|
||||||
|
tooltipContent="Quick create"
|
||||||
|
>
|
||||||
|
<CirclePlusFilledIcon />
|
||||||
|
<span>Quick Create</span>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
class="size-8 group-data-[collapsible=icon]:opacity-0"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<MailIcon />
|
||||||
|
<span class="sr-only">Inbox</span>
|
||||||
|
</Button>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton tooltipContent={item.title}>
|
||||||
|
{#if item.icon}
|
||||||
|
<item.icon />
|
||||||
|
{/if}
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
30
src/lib/components/nav-secondary.svelte
Normal file
30
src/lib/components/nav-secondary.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import type { WithoutChildren } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { Icon } from "@tabler/icons-svelte";
|
||||||
|
let {
|
||||||
|
items,
|
||||||
|
...restProps
|
||||||
|
}: { items: { title: string; url: string; icon: Icon }[] } & WithoutChildren<
|
||||||
|
ComponentProps<typeof Sidebar.Group>
|
||||||
|
> = $props();
|
||||||
|
</script>
|
||||||
|
<Sidebar.Group {...restProps}>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={item.url} {...props}>
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
80
src/lib/components/nav-user.svelte
Normal file
80
src/lib/components/nav-user.svelte
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CreditCardIcon from "@tabler/icons-svelte/icons/credit-card";
|
||||||
|
import DotsVerticalIcon from "@tabler/icons-svelte/icons/dots-vertical";
|
||||||
|
import LogoutIcon from "@tabler/icons-svelte/icons/logout";
|
||||||
|
import NotificationIcon from "@tabler/icons-svelte/icons/notification";
|
||||||
|
import UserCircleIcon from "@tabler/icons-svelte/icons/user-circle";
|
||||||
|
import * as Avatar from "$lib/components/ui/avatar/index.js";
|
||||||
|
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
let { user }: { user: { name: string; email: string; avatar: string } } = $props();
|
||||||
|
const sidebar = Sidebar.useSidebar();
|
||||||
|
</script>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Sidebar.MenuButton
|
||||||
|
{...props}
|
||||||
|
size="lg"
|
||||||
|
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<Avatar.Root class="size-8 rounded-lg grayscale">
|
||||||
|
<Avatar.Image src={user.avatar} alt={user.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{user.name}</span>
|
||||||
|
<span class="text-muted-foreground truncate text-xs">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<DotsVerticalIcon class="ml-auto size-4" />
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
{/snippet}
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
|
||||||
|
side={sidebar.isMobile ? "bottom" : "right"}
|
||||||
|
align="end"
|
||||||
|
sideOffset={4}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Label class="p-0 font-normal">
|
||||||
|
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
|
<Avatar.Root class="size-8 rounded-lg">
|
||||||
|
<Avatar.Image src={user.avatar} alt={user.name} />
|
||||||
|
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||||
|
<span class="truncate font-medium">{user.name}</span>
|
||||||
|
<span class="text-muted-foreground truncate text-xs">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<UserCircleIcon />
|
||||||
|
Account
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<CreditCardIcon />
|
||||||
|
Billing
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<NotificationIcon />
|
||||||
|
Notifications
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Group>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item>
|
||||||
|
<LogoutIcon />
|
||||||
|
Log out
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
</Sidebar.Menu>
|
||||||
11
src/lib/components/schemas.ts
Normal file
11
src/lib/components/schemas.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
export const schema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
header: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
target: z.string(),
|
||||||
|
limit: z.string(),
|
||||||
|
reviewer: z.string(),
|
||||||
|
});
|
||||||
|
export type Schema = z.infer<typeof schema>;
|
||||||
90
src/lib/components/section-cards.svelte
Normal file
90
src/lib/components/section-cards.svelte
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TrendingDownIcon from "@tabler/icons-svelte/icons/trending-down";
|
||||||
|
import TrendingUpIcon from "@tabler/icons-svelte/icons/trending-up";
|
||||||
|
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||||
|
import * as Card from "$lib/components/ui/card/index.js";
|
||||||
|
</script>
|
||||||
|
<div
|
||||||
|
class="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card *:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4 grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t lg:px-6"
|
||||||
|
>
|
||||||
|
<Card.Root class="@container/card">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Description>Total Revenue</Card.Description>
|
||||||
|
<Card.Title class="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
$1,250.00
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Action>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<TrendingUpIcon />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</Card.Action>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Trending up this month <TrendingUpIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">Visitors for the last 6 months</div>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
<Card.Root class="@container/card">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Description>New Customers</Card.Description>
|
||||||
|
<Card.Title class="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
1,234
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Action>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<TrendingDownIcon />
|
||||||
|
-20%
|
||||||
|
</Badge>
|
||||||
|
</Card.Action>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Down 20% this period <TrendingDownIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">Acquisition needs attention</div>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
<Card.Root class="@container/card">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Description>Active Accounts</Card.Description>
|
||||||
|
<Card.Title class="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
45,678
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Action>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<TrendingUpIcon />
|
||||||
|
+12.5%
|
||||||
|
</Badge>
|
||||||
|
</Card.Action>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Strong user retention <TrendingUpIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">Engagement exceed targets</div>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
<Card.Root class="@container/card">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Description>Growth Rate</Card.Description>
|
||||||
|
<Card.Title class="@[250px]/card:text-3xl text-2xl font-semibold tabular-nums">
|
||||||
|
4.5%
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Action>
|
||||||
|
<Badge variant="outline">
|
||||||
|
<TrendingUpIcon />
|
||||||
|
+4.5%
|
||||||
|
</Badge>
|
||||||
|
</Card.Action>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
|
||||||
|
<div class="line-clamp-1 flex gap-2 font-medium">
|
||||||
|
Steady performance increase <TrendingUpIcon class="size-4" />
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">Meets growth projections</div>
|
||||||
|
</Card.Footer>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
26
src/lib/components/site-header.svelte
Normal file
26
src/lib/components/site-header.svelte
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
</script>
|
||||||
|
<header
|
||||||
|
class="h-(--header-height) group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height) flex shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||||
|
<Sidebar.Trigger class="-ml-1" />
|
||||||
|
<Separator orientation="vertical" class="mx-2 data-[orientation=vertical]:h-4" />
|
||||||
|
<h1 class="text-base font-medium">Documents</h1>
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
href="https://github.com/shadcn-ui/ui/tree/main/apps/v4/app/(examples)/dashboard"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="dark:text-foreground hidden sm:flex"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@ -2,6 +2,25 @@
|
|||||||
import '../app.css';
|
import '../app.css';
|
||||||
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
import { Toaster } from "$lib/components/ui/sonner/index.js";
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||||
|
import AppSidebar from "$lib/components/app-sidebar.svelte";
|
||||||
|
import SiteHeader from "$lib/components/site-header.svelte";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
{@render children()}
|
<Sidebar.Provider
|
||||||
|
style="--sidebar-width: calc(var(--spacing) * 72); --header-height: calc(var(--spacing) * 12);"
|
||||||
|
>
|
||||||
|
<AppSidebar variant="inset" />
|
||||||
|
<Sidebar.Inset>
|
||||||
|
<SiteHeader />
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div class="@container/main flex flex-1 flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Inset>
|
||||||
|
</Sidebar.Provider>
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button} from "$lib/components/ui/button/index.js";
|
import data from "./data.js";
|
||||||
|
import SectionCards from "$lib/components/section-cards.svelte";
|
||||||
|
import ChartAreaInteractive from "$lib/components/chart-area-interactive.svelte";
|
||||||
|
import DataTable from "$lib/components/data-table.svelte";
|
||||||
</script>
|
</script>
|
||||||
<div>
|
<SectionCards />
|
||||||
<p>FROVIDE.COM</p>
|
<div class="px-4 lg:px-6">
|
||||||
<p>안녕하세요 반갑습니다.</p>
|
<ChartAreaInteractive />
|
||||||
<Button>버튼 테스트</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DataTable {data} />
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
614
src/routes/data.ts
Normal file
614
src/routes/data.ts
Normal file
@ -0,0 +1,614 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
header: "Cover page",
|
||||||
|
type: "Cover page",
|
||||||
|
status: "In Process",
|
||||||
|
target: "18",
|
||||||
|
limit: "5",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
header: "Table of contents",
|
||||||
|
type: "Table of contents",
|
||||||
|
status: "Done",
|
||||||
|
target: "29",
|
||||||
|
limit: "24",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
header: "Executive summary",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "10",
|
||||||
|
limit: "13",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
header: "Technical approach",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "27",
|
||||||
|
limit: "23",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
header: "Design",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "In Process",
|
||||||
|
target: "2",
|
||||||
|
limit: "16",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
header: "Capabilities",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "In Process",
|
||||||
|
target: "20",
|
||||||
|
limit: "8",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
header: "Integration with existing systems",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "In Process",
|
||||||
|
target: "19",
|
||||||
|
limit: "21",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
header: "Innovation and Advantages",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "25",
|
||||||
|
limit: "26",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
header: "Overview of EMR's Innovative Solutions",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "7",
|
||||||
|
limit: "23",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
header: "Advanced Algorithms and Machine Learning",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "30",
|
||||||
|
limit: "28",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
header: "Adaptive Communication Protocols",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "9",
|
||||||
|
limit: "31",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
header: "Advantages Over Current Technologies",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "12",
|
||||||
|
limit: "0",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
header: "Past Performance",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "22",
|
||||||
|
limit: "33",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
header: "Customer Feedback and Satisfaction Levels",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "15",
|
||||||
|
limit: "34",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
header: "Implementation Challenges and Solutions",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "3",
|
||||||
|
limit: "35",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
header: "Security Measures and Data Protection Policies",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "In Process",
|
||||||
|
target: "6",
|
||||||
|
limit: "36",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
header: "Scalability and Future Proofing",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "4",
|
||||||
|
limit: "37",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
header: "Cost-Benefit Analysis",
|
||||||
|
type: "Plain language",
|
||||||
|
status: "Done",
|
||||||
|
target: "14",
|
||||||
|
limit: "38",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
header: "User Training and Onboarding Experience",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "17",
|
||||||
|
limit: "39",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
header: "Future Development Roadmap",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "11",
|
||||||
|
limit: "40",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
header: "System Architecture Overview",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "24",
|
||||||
|
limit: "18",
|
||||||
|
reviewer: "Maya Johnson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
header: "Risk Management Plan",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "15",
|
||||||
|
limit: "22",
|
||||||
|
reviewer: "Carlos Rodriguez",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
header: "Compliance Documentation",
|
||||||
|
type: "Legal",
|
||||||
|
status: "In Process",
|
||||||
|
target: "31",
|
||||||
|
limit: "27",
|
||||||
|
reviewer: "Sarah Chen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 24,
|
||||||
|
header: "API Documentation",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "8",
|
||||||
|
limit: "12",
|
||||||
|
reviewer: "Raj Patel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 25,
|
||||||
|
header: "User Interface Mockups",
|
||||||
|
type: "Visual",
|
||||||
|
status: "In Process",
|
||||||
|
target: "19",
|
||||||
|
limit: "25",
|
||||||
|
reviewer: "Leila Ahmadi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 26,
|
||||||
|
header: "Database Schema",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "22",
|
||||||
|
limit: "20",
|
||||||
|
reviewer: "Thomas Wilson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 27,
|
||||||
|
header: "Testing Methodology",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "17",
|
||||||
|
limit: "14",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 28,
|
||||||
|
header: "Deployment Strategy",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "26",
|
||||||
|
limit: "30",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 29,
|
||||||
|
header: "Budget Breakdown",
|
||||||
|
type: "Financial",
|
||||||
|
status: "In Process",
|
||||||
|
target: "13",
|
||||||
|
limit: "16",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30,
|
||||||
|
header: "Market Analysis",
|
||||||
|
type: "Research",
|
||||||
|
status: "Done",
|
||||||
|
target: "29",
|
||||||
|
limit: "32",
|
||||||
|
reviewer: "Sophia Martinez",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 31,
|
||||||
|
header: "Competitor Comparison",
|
||||||
|
type: "Research",
|
||||||
|
status: "In Process",
|
||||||
|
target: "21",
|
||||||
|
limit: "19",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 32,
|
||||||
|
header: "Maintenance Plan",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "16",
|
||||||
|
limit: "23",
|
||||||
|
reviewer: "Alex Thompson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 33,
|
||||||
|
header: "User Personas",
|
||||||
|
type: "Research",
|
||||||
|
status: "In Process",
|
||||||
|
target: "27",
|
||||||
|
limit: "24",
|
||||||
|
reviewer: "Nina Patel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 34,
|
||||||
|
header: "Accessibility Compliance",
|
||||||
|
type: "Legal",
|
||||||
|
status: "Done",
|
||||||
|
target: "18",
|
||||||
|
limit: "21",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 35,
|
||||||
|
header: "Performance Metrics",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "23",
|
||||||
|
limit: "26",
|
||||||
|
reviewer: "David Kim",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 36,
|
||||||
|
header: "Disaster Recovery Plan",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "14",
|
||||||
|
limit: "17",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 37,
|
||||||
|
header: "Third-party Integrations",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "25",
|
||||||
|
limit: "28",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 38,
|
||||||
|
header: "User Feedback Summary",
|
||||||
|
type: "Research",
|
||||||
|
status: "Done",
|
||||||
|
target: "20",
|
||||||
|
limit: "15",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 39,
|
||||||
|
header: "Localization Strategy",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "In Process",
|
||||||
|
target: "12",
|
||||||
|
limit: "19",
|
||||||
|
reviewer: "Maria Garcia",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 40,
|
||||||
|
header: "Mobile Compatibility",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "28",
|
||||||
|
limit: "31",
|
||||||
|
reviewer: "James Wilson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 41,
|
||||||
|
header: "Data Migration Plan",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "19",
|
||||||
|
limit: "22",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 42,
|
||||||
|
header: "Quality Assurance Protocols",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "30",
|
||||||
|
limit: "33",
|
||||||
|
reviewer: "Priya Singh",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 43,
|
||||||
|
header: "Stakeholder Analysis",
|
||||||
|
type: "Research",
|
||||||
|
status: "In Process",
|
||||||
|
target: "11",
|
||||||
|
limit: "14",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 44,
|
||||||
|
header: "Environmental Impact Assessment",
|
||||||
|
type: "Research",
|
||||||
|
status: "Done",
|
||||||
|
target: "24",
|
||||||
|
limit: "27",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 45,
|
||||||
|
header: "Intellectual Property Rights",
|
||||||
|
type: "Legal",
|
||||||
|
status: "In Process",
|
||||||
|
target: "17",
|
||||||
|
limit: "20",
|
||||||
|
reviewer: "Sarah Johnson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 46,
|
||||||
|
header: "Customer Support Framework",
|
||||||
|
type: "Narrative",
|
||||||
|
status: "Done",
|
||||||
|
target: "22",
|
||||||
|
limit: "25",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 47,
|
||||||
|
header: "Version Control Strategy",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "15",
|
||||||
|
limit: "18",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 48,
|
||||||
|
header: "Continuous Integration Pipeline",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "26",
|
||||||
|
limit: "29",
|
||||||
|
reviewer: "Michael Chen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 49,
|
||||||
|
header: "Regulatory Compliance",
|
||||||
|
type: "Legal",
|
||||||
|
status: "In Process",
|
||||||
|
target: "13",
|
||||||
|
limit: "16",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 50,
|
||||||
|
header: "User Authentication System",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "28",
|
||||||
|
limit: "31",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 51,
|
||||||
|
header: "Data Analytics Framework",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "21",
|
||||||
|
limit: "24",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 52,
|
||||||
|
header: "Cloud Infrastructure",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "16",
|
||||||
|
limit: "19",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 53,
|
||||||
|
header: "Network Security Measures",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "29",
|
||||||
|
limit: "32",
|
||||||
|
reviewer: "Lisa Wong",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 54,
|
||||||
|
header: "Project Timeline",
|
||||||
|
type: "Planning",
|
||||||
|
status: "Done",
|
||||||
|
target: "14",
|
||||||
|
limit: "17",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 55,
|
||||||
|
header: "Resource Allocation",
|
||||||
|
type: "Planning",
|
||||||
|
status: "In Process",
|
||||||
|
target: "27",
|
||||||
|
limit: "30",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 56,
|
||||||
|
header: "Team Structure and Roles",
|
||||||
|
type: "Planning",
|
||||||
|
status: "Done",
|
||||||
|
target: "20",
|
||||||
|
limit: "23",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 57,
|
||||||
|
header: "Communication Protocols",
|
||||||
|
type: "Planning",
|
||||||
|
status: "In Process",
|
||||||
|
target: "15",
|
||||||
|
limit: "18",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 58,
|
||||||
|
header: "Success Metrics",
|
||||||
|
type: "Planning",
|
||||||
|
status: "Done",
|
||||||
|
target: "30",
|
||||||
|
limit: "33",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 59,
|
||||||
|
header: "Internationalization Support",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "23",
|
||||||
|
limit: "26",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 60,
|
||||||
|
header: "Backup and Recovery Procedures",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "18",
|
||||||
|
limit: "21",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 61,
|
||||||
|
header: "Monitoring and Alerting System",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "25",
|
||||||
|
limit: "28",
|
||||||
|
reviewer: "Daniel Park",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 62,
|
||||||
|
header: "Code Review Guidelines",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "12",
|
||||||
|
limit: "15",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 63,
|
||||||
|
header: "Documentation Standards",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "In Process",
|
||||||
|
target: "27",
|
||||||
|
limit: "30",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 64,
|
||||||
|
header: "Release Management Process",
|
||||||
|
type: "Planning",
|
||||||
|
status: "Done",
|
||||||
|
target: "22",
|
||||||
|
limit: "25",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 65,
|
||||||
|
header: "Feature Prioritization Matrix",
|
||||||
|
type: "Planning",
|
||||||
|
status: "In Process",
|
||||||
|
target: "19",
|
||||||
|
limit: "22",
|
||||||
|
reviewer: "Emma Davis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 66,
|
||||||
|
header: "Technical Debt Assessment",
|
||||||
|
type: "Technical content",
|
||||||
|
status: "Done",
|
||||||
|
target: "24",
|
||||||
|
limit: "27",
|
||||||
|
reviewer: "Eddie Lake",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 67,
|
||||||
|
header: "Capacity Planning",
|
||||||
|
type: "Planning",
|
||||||
|
status: "In Process",
|
||||||
|
target: "21",
|
||||||
|
limit: "24",
|
||||||
|
reviewer: "Jamik Tashpulatov",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 68,
|
||||||
|
header: "Service Level Agreements",
|
||||||
|
type: "Legal",
|
||||||
|
status: "Done",
|
||||||
|
target: "26",
|
||||||
|
limit: "29",
|
||||||
|
reviewer: "Assign reviewer",
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -5,7 +5,7 @@
|
|||||||
let { data }: { data: PageServerData } = $props();
|
let { data }: { data: PageServerData } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Hi, {data.user.username}!</h1>
|
<h1>Hi, {data.user.email}!</h1>
|
||||||
<p>Your user ID is {data.user.id}.</p>
|
<p>Your user ID is {data.user.id}.</p>
|
||||||
<form method='post' action='?/logout' use:enhance>
|
<form method='post' action='?/logout' use:enhance>
|
||||||
<button>Sign out</button>
|
<button>Sign out</button>
|
||||||
|
|||||||
@ -22,46 +22,42 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
login: async (event) => {
|
register: async (event) => {
|
||||||
const form = await superValidate(event, zod(formSchema));
|
const form = await superValidate(event, zod(formSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
form,
|
form,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const email = form.data.email;
|
const email = form.data.email;
|
||||||
const password = form.data.password;
|
const password = form.data.password;
|
||||||
|
|
||||||
const results = await db
|
const userId = generateUserId();
|
||||||
.select()
|
const passwordHash = await hash(password, {
|
||||||
.from(table.user)
|
// recommended minimum parameters
|
||||||
.where(eq(table.user.email, email));
|
|
||||||
|
|
||||||
const existingUser = results.at(0);
|
|
||||||
if (!existingUser) {
|
|
||||||
return setError(form, 'email', '등록된 이메일이 아닙니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPassword = await verify(existingUser.passwordHash, password, {
|
|
||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
outputLen: 32,
|
outputLen: 32,
|
||||||
parallelism: 1,
|
parallelism: 1,
|
||||||
});
|
});
|
||||||
if (!validPassword) {
|
|
||||||
return setError(form, 'password', '비밀번호가 일치하지 않습니다.');
|
try {
|
||||||
|
await db.insert(table.user).values({ id: userId, email, passwordHash });
|
||||||
|
|
||||||
|
const sessionToken = auth.generateSessionToken();
|
||||||
|
const session = await auth.createSession(sessionToken, userId);
|
||||||
|
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||||
|
} catch (e) {
|
||||||
|
return fail(500, { message: 'An error has occurred' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionToken = auth.generateSessionToken();
|
|
||||||
const session = await auth.createSession(sessionToken, existingUser.id);
|
|
||||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
|
||||||
|
|
||||||
return redirect(302, '/demo/lucia');
|
return redirect(302, '/demo/lucia');
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function generateUserId() {
|
||||||
|
// ID with 120 bits of entropy, or about the same as UUID v4.
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
||||||
|
const id = encodeBase32LowerCase(bytes);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
superForm,
|
superForm,
|
||||||
} from "sveltekit-superforms";
|
} from "sveltekit-superforms";
|
||||||
import {zodClient} from "sveltekit-superforms/adapters";
|
import {zodClient} from "sveltekit-superforms/adapters";
|
||||||
import { dev } from '$app/environment';
|
import {dev} from '$app/environment';
|
||||||
import {Label} from "@/components/ui/label";
|
import {Label} from "@/components/ui/label";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
import {cn} from "@/utils.js";
|
import {cn} from "@/utils.js";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user