diff --git a/package-lock.json b/package-lock.json
index 50deb3b..9e1d54b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
+ "better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
@@ -52,6 +53,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
+ "@types/better-sqlite3": "^7.6.11",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
@@ -4151,6 +4153,16 @@
],
"license": "MIT"
},
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/caseless": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
@@ -4582,6 +4594,17 @@
}
]
},
+ "node_modules/better-sqlite3": {
+ "version": "11.10.0",
+ "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
+ "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "prebuild-install": "^7.1.1"
+ }
+ },
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
@@ -4601,11 +4624,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
@@ -4660,7 +4691,6 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -4856,6 +4886,12 @@
"node": ">= 6"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -5383,6 +5419,21 @@
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/deeks": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz",
@@ -5393,6 +5444,15 @@
"node": ">= 16"
}
},
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@@ -5455,7 +5515,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
- "optional": true,
"engines": {
"node": ">=8"
}
@@ -5607,7 +5666,6 @@
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "dev": true,
"dependencies": {
"once": "^1.4.0"
}
@@ -5760,6 +5818,15 @@
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -5999,6 +6066,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -6191,6 +6264,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -6430,6 +6509,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -6779,7 +6864,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -6833,6 +6917,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/inquirer": {
"version": "8.2.6",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
@@ -7517,6 +7607,18 @@
"node": ">=6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -7547,6 +7649,12 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/module-details-from-path": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
@@ -7593,6 +7701,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -7709,6 +7823,18 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/node-abi": {
+ "version": "3.75.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
+ "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -8215,6 +8341,32 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8306,7 +8458,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
- "dev": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
@@ -8370,6 +8521,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -8533,7 +8699,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
@@ -9097,6 +9262,51 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -9202,7 +9412,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
@@ -9296,6 +9505,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
@@ -9421,6 +9639,34 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
+ "node_modules/tar-fs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+ "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/teeny-request": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
@@ -9652,6 +9898,18 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
diff --git a/package.json b/package.json
index 7e18003..77e2896 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8",
+ "better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
@@ -56,6 +57,7 @@
"zod": "^3.24.2"
},
"devDependencies": {
+ "@types/better-sqlite3": "^7.6.11",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
diff --git a/src/app/[locale]/admin/toys/page.tsx b/src/app/[locale]/admin/toys/page.tsx
index 24f971c..7b1c9ca 100644
--- a/src/app/[locale]/admin/toys/page.tsx
+++ b/src/app/[locale]/admin/toys/page.tsx
@@ -2,7 +2,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
-import { mockToys } from "@/lib/mockData";
+import { getAllToys } from "@/data/operations";
import { getI18n } from "@/locales/server";
import { Edit3 } from "lucide-react";
import Link from "next/link";
@@ -12,8 +12,7 @@ import Image from "next/image";
export default async function AdminToyManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
- // In a real app, you might want pagination for a large number of toys
- const allToys = mockToys;
+ const allToys = getAllToys();
return (
@@ -55,8 +54,6 @@ export default async function AdminToyManagementPage({ params }: { params: { loc
{toy.ownerName}
{toy.category}
- {/* Link to the existing edit page, which uses user-level permissions.
- A true admin edit might need a separate form or enhanced permissions. */}
diff --git a/src/app/[locale]/admin/users/page.tsx b/src/app/[locale]/admin/users/page.tsx
index 303ac63..1b1c1fc 100644
--- a/src/app/[locale]/admin/users/page.tsx
+++ b/src/app/[locale]/admin/users/page.tsx
@@ -2,109 +2,19 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
-import { mockToys } from "@/lib/mockData"; // Using mockToys to derive some mock users
+import { getAllUsers } from "@/data/operations";
import { getI18n } from "@/locales/server";
import { UserCog } from "lucide-react";
import Link from "next/link";
import type { Locale } from "@/locales/server";
-// Create mock users based on toy owners for demonstration
-
-// 1. Derive users from toy owners, ensuring 'user1' gets the primary admin email
-const usersFromToys = Array.from(new Set(mockToys.map(toy => toy.ownerId))).map(ownerId => {
- const userToy = mockToys.find(toy => toy.ownerId === ownerId);
- const isUser1FromToys = ownerId === 'user1'; // 'user1' corresponds to Alice, the primary admin
-
- return {
- id: ownerId,
- name: userToy?.ownerName || `User ${ownerId}`,
- // If ownerId is 'user1', assign the primary admin email. Otherwise, derive email.
- email: isUser1FromToys ? 'user@example.com' : `${ownerId}@example.com`,
- // 'user1' is an Admin, others are User by default from toys
- role: isUser1FromToys ? 'Admin' : 'User',
- };
-});
-
-// 2. Use a Map to ensure unique users by ID, and to easily update specific users
-const userMap = new Map();
-
-usersFromToys.forEach(user => {
- userMap.set(user.id, user);
-});
-
-// 3. Ensure primary admin (user@example.com, corresponds to 'user1') is correctly represented
-const primaryAdminId = 'user1';
-const primaryAdminEmail = 'user@example.com';
-if (userMap.has(primaryAdminId)) {
- const user = userMap.get(primaryAdminId)!;
- user.email = primaryAdminEmail; // Ensure correct email
- user.name = user.name || 'Alice Wonderland (Admin)'; // Set/ensure name
- user.role = 'Admin'; // Ensure role is Admin
-} else {
- // If 'user1' wasn't an ownerId in mockToys, add Alice/primary admin.
- userMap.set(primaryAdminId, {
- id: primaryAdminId,
- name: 'Alice Wonderland (Admin)',
- email: primaryAdminEmail,
- role: 'Admin',
- });
-}
-
-// 4. Ensure secondary admin (admin@example.com) is present with a unique ID
-const secondaryAdminId = 'admin-main';
-const secondaryAdminEmail = 'admin@example.com';
-
-// Check if a user with the secondary admin email already exists
-let existingSecondaryAdmin = Array.from(userMap.values()).find(u => u.email === secondaryAdminEmail);
-
-if (existingSecondaryAdmin) {
- // If user with this email exists, ensure their role is Admin.
- // If their ID is not 'admin-main' and not 'user1' (primary admin), update their ID to 'admin-main'
- // if 'admin-main' ID is not already taken by someone else.
- existingSecondaryAdmin.role = 'Admin';
- if (existingSecondaryAdmin.id !== primaryAdminId && existingSecondaryAdmin.id !== secondaryAdminId) {
- if (!userMap.has(secondaryAdminId) || userMap.get(secondaryAdminId)?.email === secondaryAdminEmail) {
- // If 'admin-main' is free or already belongs to this email, consolidate.
- if (existingSecondaryAdmin.id !== secondaryAdminId) { // avoid deleting and re-adding if id is already correct
- userMap.delete(existingSecondaryAdmin.id); // remove old entry if ID was different
- existingSecondaryAdmin.id = secondaryAdminId; // update ID
- userMap.set(secondaryAdminId, existingSecondaryAdmin); // re-add with correct ID
- }
- }
- // If 'admin-main' ID is taken by someone else with a different email, it implies a conflict.
- // For this mock setup, we'll prioritize the email match.
- } else if (existingSecondaryAdmin.id === primaryAdminId && primaryAdminEmail !== secondaryAdminEmail) {
- // This means primary admin 'user1' somehow got the secondary admin's email, which is an inconsistency.
- // For now, we assume this won't happen with the current logic flow.
- }
-} else {
- // No user with this email exists. Add the secondary admin.
- // Ensure the ID 'admin-main' is not already used by someone else with a different email.
- if (userMap.has(secondaryAdminId) && userMap.get(secondaryAdminId)!.email !== secondaryAdminEmail) {
- // ID 'admin-main' is taken by another user. This is a data conflict.
- // For now, we'll log or handle this as an edge case. Let's give a new unique ID.
- // console.warn("ID 'admin-main' is taken by a different user. Generating new ID for secondary admin.");
- // For simplicity in mock data, we'll assume 'admin-main' is available or correctly assigned if email matches.
- // If we strictly need 'admin-main' and it's taken, it's a setup issue.
- // Let's assume 'admin-main' will be used if available.
- }
- userMap.set(secondaryAdminId, {
- id: secondaryAdminId,
- name: 'Main Admin',
- email: secondaryAdminEmail,
- role: 'Admin',
- });
-}
-
-
-const mockUsers = Array.from(userMap.values());
-// Sort users for consistent display, e.g., by name or role
-mockUsers.sort((a, b) => a.name.localeCompare(b.name));
-
-
export default async function AdminUserManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
+ const allUsers = getAllUsers();
+
+ // Sort users for consistent display, e.g., by name or role
+ allUsers.sort((a, b) => a.name.localeCompare(b.name));
return (
@@ -118,7 +28,7 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
{t('admin.users.title')}
- {mockUsers.length > 0 ? (
+ {allUsers.length > 0 ? (
@@ -129,13 +39,13 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
- {mockUsers.map((user) => (
- {/* Key is user.id, needs to be unique */}
+ {allUsers.map((user) => (
+
{user.name}
{user.email}
{user.role}
- {/* Placeholder for future edit functionality */}
+
{t('admin.users.edit_button')}
diff --git a/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx
index dab59f1..70db6a1 100644
--- a/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx
+++ b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx
@@ -1,24 +1,19 @@
+
import AddToyForm from '@/components/toys/AddToyForm';
-import { mockToys } from '@/lib/mockData';
+import { getToyById, getAllToys } from '@/data/operations';
import type { Toy } from '@/types';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
-
interface EditToyPageProps {
params: { id: string, locale: string };
}
-async function getToyForEdit(id: string): Promise | undefined> {
- await new Promise(resolve => setTimeout(resolve, 100));
- return mockToys.find(toy => toy.id === id);
-}
-
export default async function EditToyPage({ params }: EditToyPageProps) {
const t = await getI18n();
- const toyData = await getToyForEdit(params.id);
+ const toyData = getToyById(params.id);
if (!toyData) {
return (
@@ -41,16 +36,15 @@ export default async function EditToyPage({ params }: EditToyPageProps) {
{t('general.back_to_my_toys')}
- {/* AddToyForm is a client component and will handle its own translations via useI18n */}
);
}
-// For static generation of edit pages if desired, similar to toy details page
export function generateStaticParams() {
const localeParams = getLocaleStaticParams();
- const toyParams = mockToys.map((toy) => ({ id: toy.id }));
+ const toys = getAllToys();
+ const toyParams = toys.map((toy) => ({ id: toy.id }));
return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id }))
diff --git a/src/app/[locale]/dashboard/my-toys/page.tsx b/src/app/[locale]/dashboard/my-toys/page.tsx
index 86f4575..8bd71fe 100644
--- a/src/app/[locale]/dashboard/my-toys/page.tsx
+++ b/src/app/[locale]/dashboard/my-toys/page.tsx
@@ -1,22 +1,23 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import ToyCard from "@/components/toys/ToyCard"; // This component would also need translation if it has text
-import { mockToys, mockRentalHistory } from "@/lib/mockData";
+import { getToysByOwner } from "@/data/operations";
+import { mockRentalHistory } from "@/lib/mockData";
import Link from "next/link";
-import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon, BarChartHorizontalBig } from "lucide-react"; // Renamed ToyBrick to avoid conflict
+import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon, BarChartHorizontalBig } from "lucide-react";
import Image from "next/image";
import type { Toy } from "@/types";
import { Badge } from "@/components/ui/badge";
import { getI18n } from "@/locales/server";
const currentUserId = 'user1';
-const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
export default async function MyToysPage() {
const t = await getI18n();
+ const userToys = getToysByOwner(currentUserId);
const getRentalCountForToy = (toyId: string): number => {
+ // NOTE: This part still uses mock data and will need to be migrated.
return mockRentalHistory.filter(entry => entry.toy.id === toyId && entry.toy.ownerId === currentUserId).length;
};
diff --git a/src/app/[locale]/owner/[ownerId]/toys/page.tsx b/src/app/[locale]/owner/[ownerId]/toys/page.tsx
index 872a4ea..71b40d0 100644
--- a/src/app/[locale]/owner/[ownerId]/toys/page.tsx
+++ b/src/app/[locale]/owner/[ownerId]/toys/page.tsx
@@ -2,8 +2,7 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import ToyList from '@/components/toys/ToyList';
-import { mockToys } from '@/lib/mockData';
-import type { Toy } from '@/types';
+import { getToysByOwner, getOwnerProfile, getAllToys } from '@/data/operations';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import { Home, UserCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -14,36 +13,11 @@ interface OwnerToysPageProps {
params: { ownerId: string; locale: string };
}
-// Mock owner profiles - can be expanded or moved to a data file later
-const mockOwnerProfiles: Record = {
- 'user1': { // Alice Wonderland
- name: 'Alice W.', // Can be different from toy.ownerName for display
- avatarUrl: 'https://placehold.co/100x100.png?text=AW',
- bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!"
- },
- 'user2': { // Bob The Builder
- name: 'Bob T.B.',
- avatarUrl: 'https://placehold.co/100x100.png?text=BT',
- bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder."
- },
- 'user3': { // Carol Danvers
- name: 'Captain C.',
- avatarUrl: 'https://placehold.co/100x100.png?text=CD',
- bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets."
- }
-};
-
-
-async function getOwnerToys(ownerId: string): Promise {
- await new Promise(resolve => setTimeout(resolve, 100)); // Simulate fetch
- return mockToys.filter(toy => toy.ownerId === ownerId);
-}
-
export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
const t = await getI18n();
- const ownerToys = await getOwnerToys(params.ownerId);
+ const ownerToys = getToysByOwner(params.ownerId);
+ const ownerProfile = getOwnerProfile(params.ownerId);
- const ownerProfile = mockOwnerProfiles[params.ownerId];
const ownerNameFromToys = ownerToys.length > 0 ? ownerToys[0].ownerName : undefined;
let displayOwnerName = ownerProfile?.name || ownerNameFromToys || t('owner_toys.unknown_owner');
@@ -78,7 +52,6 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
{t('owner_toys.about_owner', { ownerName: displayOwnerName })}
- {/* Display the original ownerName from toy data if it's different and available */}
{ownerNameFromToys && ownerNameFromToys !== displayOwnerName && (
{t('toy_details.owner')}: {ownerNameFromToys}
)}
@@ -92,7 +65,7 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
)}
{ownerToys.length > 0 ? (
- ({...toy, dataAiHint: toy.category.toLowerCase()}))} t={t} />
+
) : (
@@ -119,8 +92,8 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
export async function generateStaticParams() {
const localeParams = getLocaleStaticParams();
-
- const ownerIds = Array.from(new Set(mockToys.map(toy => toy.ownerId)));
+ const allToys = getAllToys();
+ const ownerIds = Array.from(new Set(allToys.map(toy => toy.ownerId)));
const ownerParams = ownerIds.map(id => ({ ownerId: id }));
return localeParams.flatMap(lang =>
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index 67db1b9..e6d58c2 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -1,13 +1,14 @@
import ToyList from '@/components/toys/ToyList';
-import { mockToys } from '@/lib/mockData';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { PlusCircle } from 'lucide-react';
import { getI18n } from '@/locales/server';
+import { getAllToys } from '@/data/operations';
export default async function HomePage() {
const t = await getI18n();
+ const toys = getAllToys();
return (
@@ -37,7 +38,7 @@ export default async function HomePage() {
{t('home.available_toys')}
- ({...toy, dataAiHint: toy.category.toLowerCase()}))} t={t} />
+
);
diff --git a/src/app/[locale]/toys/[id]/page.tsx b/src/app/[locale]/toys/[id]/page.tsx
index bf7a203..51d28cc 100644
--- a/src/app/[locale]/toys/[id]/page.tsx
+++ b/src/app/[locale]/toys/[id]/page.tsx
@@ -1,9 +1,9 @@
import Image from 'next/image';
-import { mockToys } from '@/lib/mockData';
+import { getToyById } from '@/data/operations';
import type { Toy } from '@/types';
import { Button } from '@/components/ui/button';
-import { Calendar } from '@/components/ui/calendar'; // Using ShadCN calendar
+import { Calendar } from '@/components/ui/calendar';
import { ArrowLeft, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
@@ -11,20 +11,16 @@ import { Separator } from '@/components/ui/separator';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import type { Locale } from '@/locales/server';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { addDays, parseISO } from 'date-fns'; // For date manipulation
+import { addDays, parseISO } from 'date-fns';
+import { getAllToys } from '@/data/operations';
interface ToyPageProps {
params: { id: string, locale: Locale };
}
-async function getToyById(id: string): Promise {
- await new Promise(resolve => setTimeout(resolve, 200));
- return mockToys.find(toy => toy.id === id);
-}
-
export default async function ToyPage({ params }: ToyPageProps) {
const t = await getI18n();
- const toy = await getToyById(params.id);
+ const toy = getToyById(params.id);
if (!toy) {
return (
@@ -44,16 +40,11 @@ export default async function ToyPage({ params }: ToyPageProps) {
const placeholderHint = toy.category.toLowerCase() || "toy detail";
const disabledDates = toy.unavailableRanges.map(range => {
- // react-day-picker expects Date objects for ranges
const from = parseISO(range.startDate);
const to = parseISO(range.endDate);
- // If 'to' is inclusive, add a day for the range. react-day-picker's 'to' is exclusive for the visual range.
- // However, for disabling, if a range is "2023-08-10" to "2023-08-12", all 3 days should be disabled.
- // The `disabled` prop for DayPicker usually treats `to` as inclusive.
return { from, to };
});
-
return (
@@ -140,9 +131,9 @@ export default async function ToyPage({ params }: ToyPageProps) {
@@ -162,7 +153,8 @@ export default async function ToyPage({ params }: ToyPageProps) {
export function generateStaticParams() {
const localeParams = getLocaleStaticParams();
- const toyParams = mockToys.map((toy) => ({ id: toy.id }));
+ const toys = getAllToys();
+ const toyParams = toys.map((toy) => ({ id: toy.id }));
return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id }))
diff --git a/src/data/operations.ts b/src/data/operations.ts
new file mode 100644
index 0000000..a08dccc
--- /dev/null
+++ b/src/data/operations.ts
@@ -0,0 +1,69 @@
+
+'use server';
+
+import db from '@/lib/db';
+import type { Toy, User } from '@/types';
+import { mockOwnerProfiles } from '@/lib/mockData';
+
+// Helper to parse toy data from DB, converting JSON strings back to objects
+const parseToy = (toyData: any): Toy => {
+ if (!toyData) return null;
+ return {
+ ...toyData,
+ images: toyData.images ? JSON.parse(toyData.images) : [],
+ unavailableRanges: toyData.unavailableRanges ? JSON.parse(toyData.unavailableRanges) : [],
+ pricePerDay: Number(toyData.pricePerDay),
+ dataAiHint: toyData.category?.toLowerCase() || 'toy'
+ };
+};
+
+// --- TOY OPERATIONS ---
+
+export function getAllToys(): Toy[] {
+ const stmt = db.prepare(`
+ SELECT t.*, u.name as ownerName
+ FROM toys t
+ JOIN users u ON t.ownerId = u.id
+ `);
+ const toys = stmt.all();
+ return toys.map(parseToy);
+}
+
+export function getToyById(id: string): Toy | undefined {
+ const stmt = db.prepare(`
+ SELECT t.*, u.name as ownerName
+ FROM toys t
+ JOIN users u ON t.ownerId = u.id
+ WHERE t.id = ?
+ `);
+ const toy = stmt.get(id);
+ return toy ? parseToy(toy) : undefined;
+}
+
+export function getToysByOwner(ownerId: string): Toy[] {
+ const stmt = db.prepare(`
+ SELECT t.*, u.name as ownerName
+ FROM toys t
+ JOIN users u ON t.ownerId = u.id
+ WHERE t.ownerId = ?
+ `);
+ const toys = stmt.all(ownerId);
+ return toys.map(parseToy);
+}
+
+// For now, we keep the mock profiles for the owner page bio/avatar
+export function getOwnerProfile(ownerId: string) {
+ return mockOwnerProfiles[ownerId] ?? null;
+}
+
+// --- USER OPERATIONS ---
+
+export function getAllUsers(): User[] {
+ const stmt = db.prepare('SELECT id, name, email, role FROM users');
+ return stmt.all() as User[];
+}
+
+export function getUserById(id: string): User | undefined {
+ const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
+ return stmt.get(id) as User | undefined;
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100644
index 0000000..0003b98
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,88 @@
+
+import Database from 'better-sqlite3';
+import path from 'path';
+import { rawUsers, rawToys } from './mockData';
+import type { User, Toy } from '@/types';
+
+// Define the path for the database file in the project root
+const dbPath = process.env.NODE_ENV === 'development'
+ ? path.join(process.cwd(), 'toyshare.db')
+ : '/tmp/toyshare.db'; // Use a writable directory in other environments
+
+// Add { verbose: console.log } for debugging SQL statements
+const db = new Database(dbPath);
+db.pragma('journal_mode = WAL');
+db.pragma('foreign_keys = ON');
+
+function initDb() {
+ // Check if tables exist
+ const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'users'").get();
+
+ if (tableCheck) {
+ return;
+ }
+
+ console.log("Initializing database...");
+
+ // Create Tables
+ const createUsersTable = `
+ CREATE TABLE users (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL UNIQUE,
+ role TEXT,
+ avatarUrl TEXT,
+ bio TEXT
+ );
+ `;
+ const createToysTable = `
+ CREATE TABLE toys (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL,
+ category TEXT NOT NULL,
+ images TEXT,
+ unavailableRanges TEXT,
+ ownerId TEXT NOT NULL,
+ pricePerDay REAL,
+ location TEXT,
+ FOREIGN KEY (ownerId) REFERENCES users(id) ON DELETE CASCADE
+ );
+ `;
+
+ db.exec(createUsersTable);
+ db.exec(createToysTable);
+ console.log("Tables created.");
+
+ // Seed Data
+ const insertUser = db.prepare('INSERT INTO users (id, name, email, role, avatarUrl, bio) VALUES (@id, @name, @email, @role, @avatarUrl, @bio)');
+ const insertToy = db.prepare('INSERT INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location) VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)');
+
+ const insertManyUsers = db.transaction((users) => {
+ for (const user of users) {
+ insertUser.run({
+ ...user,
+ email: `${user.id}@example.com`, // Generate mock email
+ });
+ }
+ });
+
+ const insertManyToys = db.transaction((toys) => {
+ for (const toy of toys) {
+ insertToy.run({
+ ...toy,
+ images: JSON.stringify(toy.images),
+ unavailableRanges: JSON.stringify(toy.unavailableRanges),
+ });
+ }
+ });
+
+ insertManyUsers(rawUsers);
+ insertManyToys(rawToys);
+
+ console.log("Database seeded with users and toys.");
+}
+
+// Initialize and export db
+initDb();
+export default db;
diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts
index 377c274..e617635 100644
--- a/src/lib/mockData.ts
+++ b/src/lib/mockData.ts
@@ -1,10 +1,20 @@
-import type { Toy, RentalHistoryEntry, RentalRequest, MessageEntry } from '@/types';
+import type { Toy, RentalHistoryEntry, RentalRequest, MessageEntry, User } from '@/types';
import { addDays, formatISO, subDays } from 'date-fns';
const today = new Date();
-export const mockToys: Toy[] = [
+export const rawUsers: Omit[] = [
+ { id: 'user1', name: 'Alice Wonderland', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=AW', bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!" },
+ { id: 'user2', name: 'Bob The Builder', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=BT', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder." },
+ { id: 'user3', name: 'Carol Danvers', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=CD', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets." },
+ { id: 'user4', name: 'Charlie Brown', role: 'User' },
+ { id: 'user5', name: 'Diana Prince', role: 'User' },
+ { id: 'user6', name: 'Edward Nigma', role: 'User' },
+ { id: 'admin-main', name: 'Main Admin', role: 'Admin' },
+];
+
+export const rawToys: Omit[] = [
{
id: '1',
name: 'Colorful Building Blocks Set',
@@ -15,11 +25,9 @@ export const mockToys: Toy[] = [
{ startDate: formatISO(addDays(today, 5), { representation: 'date' }), endDate: formatISO(addDays(today, 7), { representation: 'date' }) },
{ startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) },
],
- ownerName: 'Alice Wonderland',
ownerId: 'user1',
pricePerDay: 5,
location: 'Springfield Gardens',
- dataAiHint: 'building blocks'
},
{
id: '2',
@@ -30,11 +38,9 @@ export const mockToys: Toy[] = [
unavailableRanges: [
{ startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) },
],
- ownerName: 'Bob The Builder',
ownerId: 'user2',
pricePerDay: 8,
location: 'Willow Creek',
- dataAiHint: 'remote car'
},
{
id: '3',
@@ -43,11 +49,9 @@ export const mockToys: Toy[] = [
category: 'Electronics',
images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'],
unavailableRanges: [],
- ownerName: 'Carol Danvers',
ownerId: 'user3',
pricePerDay: 7,
location: 'Metro City',
- dataAiHint: 'learning tablet'
},
{
id: '4',
@@ -58,11 +62,9 @@ export const mockToys: Toy[] = [
unavailableRanges: [
{ startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) },
],
- ownerName: 'Alice Wonderland',
ownerId: 'user1',
pricePerDay: 3,
location: 'Springfield Gardens',
- dataAiHint: 'teddy bear'
},
{
id: '5',
@@ -71,11 +73,9 @@ export const mockToys: Toy[] = [
category: 'Musical',
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
unavailableRanges: [],
- ownerName: 'Bob The Builder',
ownerId: 'user2',
pricePerDay: 10,
location: 'Willow Creek',
- dataAiHint: 'acoustic guitar'
},
{
id: '6',
@@ -84,111 +84,33 @@ export const mockToys: Toy[] = [
category: 'Outdoor',
images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
unavailableRanges: [],
- ownerName: 'Carol Danvers',
ownerId: 'user3',
pricePerDay: 6,
location: 'Metro City',
- dataAiHint: 'outdoor sports'
}
];
-export const mockRentalHistory: RentalHistoryEntry[] = [
- {
- id: 'hist1',
- userId: 'user1',
- toy: mockToys[2],
- rentalStartDate: '2024-05-01',
- rentalEndDate: '2024-05-07',
- totalCost: mockToys[2].pricePerDay! * 7,
- status: 'Completed',
- dataAiHint: mockToys[2].category.toLowerCase(),
- },
- {
- id: 'hist2',
- userId: 'user1',
- toy: mockToys[5],
- rentalStartDate: '2024-06-10',
- rentalEndDate: '2024-06-15',
- totalCost: mockToys[5].pricePerDay! * 5,
- status: 'Returned',
- dataAiHint: mockToys[5].category.toLowerCase(),
- },
- {
- id: 'hist3',
- userId: 'user2',
- toy: mockToys[0],
- rentalStartDate: '2024-07-01',
- rentalEndDate: '2024-07-10',
- totalCost: mockToys[0].pricePerDay! * 10,
- status: 'Completed',
- dataAiHint: mockToys[0].category.toLowerCase(),
- }
-];
+// NOTE: The data below is not yet migrated to the database.
+// These pages will be updated in subsequent steps.
-export const mockRentalRequests: RentalRequest[] = [
- {
- id: 'req1',
- toy: mockToys[0], // Owned by user1 (Alice)
- requesterName: 'Charlie Brown',
- requesterId: 'user4',
- requestedDates: 'August 10, 2024 - August 17, 2024',
- status: 'pending',
- message: 'My son would love to play with these for his birthday week! We are very careful with toys and will ensure it is returned in perfect condition. Could we possibly pick it up on the 9th evening?',
- messages: [
- { id: 'm1-1', senderId: 'user4', senderName: 'Charlie Brown', text: 'Hi Alice, is this available for my son next week?', timestamp: subDays(new Date(), 2).toISOString() },
- { id: 'm1-2', senderId: 'user1', senderName: 'Alice Wonderland', text: 'Hi Charlie! Yes, it should be. Let me double check the dates.', timestamp: subDays(new Date(), 1).toISOString() },
- { id: 'm1-3', senderId: 'user4', senderName: 'Charlie Brown', text: 'Great, thanks!', timestamp: new Date().toISOString() },
- ],
- dataAiHint: mockToys[0]?.category.toLowerCase(),
+export const mockToys = []; // Legacy, will be removed later
+export const mockRentalHistory: RentalHistoryEntry[] = [];
+export const mockRentalRequests: RentalRequest[] = [];
+export const mockMessages: (MessageEntry & { rentalRequestId: string })[] = [];
+export const mockOwnerProfiles: Record = {
+ 'user1': {
+ name: 'Alice W.',
+ avatarUrl: 'https://placehold.co/100x100.png?text=AW',
+ bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes that my kids have outgrown but still have lots of life left in them!"
},
- {
- id: 'req2',
- toy: mockToys[3], // Owned by user1 (Alice)
- requesterName: 'Diana Prince',
- requesterId: 'user5',
- requestedDates: 'September 1, 2024 - September 5, 2024',
- status: 'approved',
- messages: [
- { id: 'm2-1', senderId: 'user5', senderName: 'Diana Prince', text: 'Hello, I would like to rent the Teddy Bear for the first week of September.', timestamp: subDays(new Date(), 3).toISOString() },
- { id: 'm2-2', senderId: 'user1', senderName: 'Alice Wonderland', text: 'Hi Diana, that works! I\'ve approved your request.', timestamp: subDays(new Date(), 2).toISOString() },
- ],
- dataAiHint: mockToys[3]?.category.toLowerCase(),
+ 'user2': {
+ name: 'Bob T.B.',
+ avatarUrl: 'https://placehold.co/100x100.png?text=BT',
+ bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder."
},
- {
- id: 'req3',
- toy: mockToys[0], // Owned by user1 (Alice)
- requesterName: 'Edward Nigma',
- requesterId: 'user6',
- requestedDates: 'July 20, 2024 - July 22, 2024',
- status: 'declined',
- message: 'Looking for a weekend rental.',
- // No follow-up messages for this one
- dataAiHint: mockToys[0]?.category.toLowerCase(),
- },
- {
- id: 'req4', // User1 is the requester here
- toy: mockToys[1], // Owned by user2 (Bob)
- requesterName: 'Alice Wonderland',
- requesterId: 'user1',
- requestedDates: 'October 5, 2024 - October 10, 2024',
- status: 'pending',
- message: 'Hi Bob, I\'d like to rent your RC Car for my nephew.',
- messages: [
- { id: 'm4-1', senderId: 'user1', senderName: 'Alice Wonderland', text: 'Hi Bob, is the RC car available in early October?', timestamp: new Date().toISOString() },
- ],
- dataAiHint: mockToys[1]?.category.toLowerCase(),
+ 'user3': {
+ name: 'Captain C.',
+ avatarUrl: 'https://placehold.co/100x100.png?text=CD',
+ bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets."
}
-];
-
-
-mockToys.forEach(toy => {
- if (!toy.dataAiHint) {
- toy.dataAiHint = toy.category.toLowerCase().split(' ')[0];
- }
-});
-
-mockRentalHistory.forEach(entry => {
- if (!entry.dataAiHint) {
- entry.dataAiHint = entry.toy.category.toLowerCase().split(' ')[0];
- }
-});
+};
diff --git a/src/types/index.ts b/src/types/index.ts
index e56f120..c1aa5b1 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -2,7 +2,7 @@
export interface MessageEntry {
id: string;
senderId: string;
- senderName: string; // For easier display without needing to look up user by ID constantly in UI
+ senderName: string;
text: string;
timestamp: string; // ISO date string
}
@@ -12,37 +12,38 @@ export interface Toy {
name: string;
description: string;
category: string;
- images: string[]; // Array of image URLs
- unavailableRanges: { startDate: string; endDate: string }[]; // New field for booked/unavailable date ranges
- ownerName: string; // Simplified for now
+ images: string[];
+ unavailableRanges: { startDate: string; endDate: string }[];
+ ownerName: string;
ownerId: string;
- pricePerDay?: number; // Optional daily rental price
- location?: string; // Optional, e.g., "City, State" or "Neighborhood"
- dataAiHint?: string; // For placeholder image keyword hint
+ pricePerDay?: number;
+ location?: string;
+ dataAiHint?: string;
}
export interface User {
id: string;
name: string;
email: string;
- // a real app would have hashed passwords, etc.
+ role?: 'Admin' | 'User';
+ avatarUrl?: string;
+ bio?: string;
}
-// Represents the availability of a toy for a specific day
export interface DailyAvailability {
date: Date;
isAvailable: boolean;
- bookedBy?: string; // User ID of the renter if booked
+ bookedBy?: string;
}
export interface RentalHistoryEntry {
id:string;
- userId: string; // ID of the user who rented
+ userId: string;
toy: Toy;
- rentalStartDate: string; // ISO date string
- rentalEndDate: string; // ISO date string
+ rentalStartDate: string;
+ rentalEndDate: string;
totalCost: number;
- status: 'Completed' | 'Returned'; // Example statuses
+ status: 'Completed' | 'Returned';
dataAiHint?: string;
}
@@ -53,7 +54,7 @@ export interface RentalRequest {
requesterId: string;
requestedDates: string;
status: 'pending' | 'approved' | 'declined';
- message?: string; // Initial request message
- messages?: MessageEntry[]; // Conversation thread
+ message?: string;
+ messages?: MessageEntry[];
dataAiHint?: string;
}