please change example data into sqlite db

This commit is contained in:
Indigo Tang 2025-07-06 04:12:50 +00:00
parent a1be73d2e6
commit 1b3424655b
13 changed files with 514 additions and 306 deletions

274
package-lock.json generated
View File

@ -31,6 +31,7 @@
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@ -52,6 +53,7 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.11",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
@ -4151,6 +4153,16 @@
], ],
"license": "MIT" "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": { "node_modules/@types/caseless": {
"version": "0.12.5", "version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "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": { "node_modules/bignumber.js": {
"version": "9.1.2", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
@ -4601,11 +4624,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"buffer": "^5.5.0", "buffer": "^5.5.0",
@ -4660,7 +4691,6 @@
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -4856,6 +4886,12 @@
"node": ">= 6" "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": { "node_modules/ci-info": {
"version": "3.9.0", "version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "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", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" "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": { "node_modules/deeks": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz", "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz",
@ -5393,6 +5444,15 @@
"node": ">= 16" "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": { "node_modules/defaults": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
@ -5455,7 +5515,6 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -5607,7 +5666,6 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": { "dependencies": {
"once": "^1.4.0" "once": "^1.4.0"
} }
@ -5760,6 +5818,15 @@
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" "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": { "node_modules/express": {
"version": "4.21.2", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@ -5999,6 +6066,12 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -6191,6 +6264,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fs-extra": {
"version": "9.1.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "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" "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": { "node_modules/glob": {
"version": "10.4.5", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@ -6779,7 +6864,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -6833,6 +6917,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "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": { "node_modules/inquirer": {
"version": "8.2.6", "version": "8.2.6",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz",
@ -7517,6 +7607,18 @@
"node": ">=6" "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": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@ -7547,6 +7649,12 @@
"node": ">=16 || 14 >=14.17" "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": { "node_modules/module-details-from-path": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", "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": "^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": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -7709,6 +7823,18 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/node-domexception": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" "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": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -8306,7 +8458,6 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dev": true,
"dependencies": { "dependencies": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"once": "^1.3.1" "once": "^1.3.1"
@ -8370,6 +8521,21 @@
"node": ">= 0.8" "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": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@ -8533,7 +8699,6 @@
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
@ -9097,6 +9262,51 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/simple-swizzle": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@ -9202,7 +9412,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
@ -9296,6 +9505,15 @@
"node": ">=8" "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": { "node_modules/stubs": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
@ -9421,6 +9639,34 @@
"tailwindcss": ">=3.0.0 || insiders" "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": { "node_modules/teeny-request": {
"version": "9.0.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
@ -9652,6 +9898,18 @@
"fsevents": "~2.3.3" "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": { "node_modules/type-fest": {
"version": "0.21.3", "version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",

View File

@ -35,6 +35,7 @@
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"better-sqlite3": "^11.1.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@ -56,6 +57,7 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.11",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",

View File

@ -2,7 +2,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 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 { getI18n } from "@/locales/server";
import { Edit3 } from "lucide-react"; import { Edit3 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@ -12,8 +12,7 @@ import Image from "next/image";
export default async function AdminToyManagementPage({ params }: { params: { locale: Locale } }) { export default async function AdminToyManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n(); const t = await getI18n();
const locale = params.locale; const locale = params.locale;
// In a real app, you might want pagination for a large number of toys const allToys = getAllToys();
const allToys = mockToys;
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -55,8 +54,6 @@ export default async function AdminToyManagementPage({ params }: { params: { loc
<TableCell>{toy.ownerName}</TableCell> <TableCell>{toy.ownerName}</TableCell>
<TableCell>{toy.category}</TableCell> <TableCell>{toy.category}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{/* Link to the existing edit page, which uses user-level permissions.
A true admin edit might need a separate form or enhanced permissions. */}
<Link href={`/${locale}/dashboard/my-toys/edit/${toy.id}`} passHref> <Link href={`/${locale}/dashboard/my-toys/edit/${toy.id}`} passHref>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
<Edit3 className="mr-2 h-4 w-4" /> <Edit3 className="mr-2 h-4 w-4" />

View File

@ -2,109 +2,19 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 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 { getI18n } from "@/locales/server";
import { UserCog } from "lucide-react"; import { UserCog } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import type { Locale } from "@/locales/server"; 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<string, { id: string; name: string; email: string; role: string }>();
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 } }) { export default async function AdminUserManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n(); const t = await getI18n();
const locale = params.locale; 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 ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -118,7 +28,7 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
<CardTitle className="text-xl font-headline">{t('admin.users.title')}</CardTitle> <CardTitle className="text-xl font-headline">{t('admin.users.title')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{mockUsers.length > 0 ? ( {allUsers.length > 0 ? (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -129,13 +39,13 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{mockUsers.map((user) => ( {allUsers.map((user) => (
<TableRow key={user.id}> {/* Key is user.id, needs to be unique */} <TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell> <TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell> <TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell> <TableCell>{user.role}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button variant="outline" size="sm" disabled> {/* Placeholder for future edit functionality */} <Button variant="outline" size="sm" disabled>
<UserCog className="mr-2 h-4 w-4" /> <UserCog className="mr-2 h-4 w-4" />
{t('admin.users.edit_button')} {t('admin.users.edit_button')}
</Button> </Button>

View File

@ -1,24 +1,19 @@
import AddToyForm from '@/components/toys/AddToyForm'; import AddToyForm from '@/components/toys/AddToyForm';
import { mockToys } from '@/lib/mockData'; import { getToyById, getAllToys } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Link from 'next/link'; import Link from 'next/link';
import { ArrowLeft } from 'lucide-react'; import { ArrowLeft } from 'lucide-react';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server'; import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
interface EditToyPageProps { interface EditToyPageProps {
params: { id: string, locale: string }; params: { id: string, locale: string };
} }
async function getToyForEdit(id: string): Promise<Partial<Toy> | undefined> {
await new Promise(resolve => setTimeout(resolve, 100));
return mockToys.find(toy => toy.id === id);
}
export default async function EditToyPage({ params }: EditToyPageProps) { export default async function EditToyPage({ params }: EditToyPageProps) {
const t = await getI18n(); const t = await getI18n();
const toyData = await getToyForEdit(params.id); const toyData = getToyById(params.id);
if (!toyData) { if (!toyData) {
return ( return (
@ -41,16 +36,15 @@ export default async function EditToyPage({ params }: EditToyPageProps) {
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" /> <ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" />
{t('general.back_to_my_toys')} {t('general.back_to_my_toys')}
</Link> </Link>
{/* AddToyForm is a client component and will handle its own translations via useI18n */}
<AddToyForm initialData={toyData} isEditMode={true} /> <AddToyForm initialData={toyData} isEditMode={true} />
</div> </div>
); );
} }
// For static generation of edit pages if desired, similar to toy details page
export function generateStaticParams() { export function generateStaticParams() {
const localeParams = getLocaleStaticParams(); 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 => return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id })) toyParams.map(toy => ({ ...lang, id: toy.id }))

View File

@ -1,22 +1,23 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 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 { getToysByOwner } from "@/data/operations";
import { mockToys, mockRentalHistory } from "@/lib/mockData"; import { mockRentalHistory } from "@/lib/mockData";
import Link from "next/link"; 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 Image from "next/image";
import type { Toy } from "@/types"; import type { Toy } from "@/types";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { getI18n } from "@/locales/server"; import { getI18n } from "@/locales/server";
const currentUserId = 'user1'; const currentUserId = 'user1';
const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
export default async function MyToysPage() { export default async function MyToysPage() {
const t = await getI18n(); const t = await getI18n();
const userToys = getToysByOwner(currentUserId);
const getRentalCountForToy = (toyId: string): number => { 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; return mockRentalHistory.filter(entry => entry.toy.id === toyId && entry.toy.ownerId === currentUserId).length;
}; };

View File

@ -2,8 +2,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import ToyList from '@/components/toys/ToyList'; import ToyList from '@/components/toys/ToyList';
import { mockToys } from '@/lib/mockData'; import { getToysByOwner, getOwnerProfile, getAllToys } from '@/data/operations';
import type { Toy } from '@/types';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server'; import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import { Home, UserCircle } from 'lucide-react'; import { Home, UserCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@ -14,36 +13,11 @@ interface OwnerToysPageProps {
params: { ownerId: string; locale: string }; params: { ownerId: string; locale: string };
} }
// Mock owner profiles - can be expanded or moved to a data file later
const mockOwnerProfiles: Record<string, { avatarUrl: string; bio: string; name?: string }> = {
'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<Toy[]> {
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate fetch
return mockToys.filter(toy => toy.ownerId === ownerId);
}
export default async function OwnerToysPage({ params }: OwnerToysPageProps) { export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
const t = await getI18n(); 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; const ownerNameFromToys = ownerToys.length > 0 ? ownerToys[0].ownerName : undefined;
let displayOwnerName = ownerProfile?.name || ownerNameFromToys || t('owner_toys.unknown_owner'); let displayOwnerName = ownerProfile?.name || ownerNameFromToys || t('owner_toys.unknown_owner');
@ -78,7 +52,6 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
<CardTitle className="text-2xl font-headline text-accent"> <CardTitle className="text-2xl font-headline text-accent">
{t('owner_toys.about_owner', { ownerName: displayOwnerName })} {t('owner_toys.about_owner', { ownerName: displayOwnerName })}
</CardTitle> </CardTitle>
{/* Display the original ownerName from toy data if it's different and available */}
{ownerNameFromToys && ownerNameFromToys !== displayOwnerName && ( {ownerNameFromToys && ownerNameFromToys !== displayOwnerName && (
<CardDescription>{t('toy_details.owner')}: {ownerNameFromToys}</CardDescription> <CardDescription>{t('toy_details.owner')}: {ownerNameFromToys}</CardDescription>
)} )}
@ -92,7 +65,7 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
)} )}
{ownerToys.length > 0 ? ( {ownerToys.length > 0 ? (
<ToyList toys={ownerToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} t={t} /> <ToyList toys={ownerToys} t={t} />
) : ( ) : (
<Card className="text-center py-12 shadow-md"> <Card className="text-center py-12 shadow-md">
<CardHeader> <CardHeader>
@ -119,8 +92,8 @@ export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
export async function generateStaticParams() { export async function generateStaticParams() {
const localeParams = getLocaleStaticParams(); const localeParams = getLocaleStaticParams();
const allToys = getAllToys();
const ownerIds = Array.from(new Set(mockToys.map(toy => toy.ownerId))); const ownerIds = Array.from(new Set(allToys.map(toy => toy.ownerId)));
const ownerParams = ownerIds.map(id => ({ ownerId: id })); const ownerParams = ownerIds.map(id => ({ ownerId: id }));
return localeParams.flatMap(lang => return localeParams.flatMap(lang =>

View File

@ -1,13 +1,14 @@
import ToyList from '@/components/toys/ToyList'; import ToyList from '@/components/toys/ToyList';
import { mockToys } from '@/lib/mockData';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Link from 'next/link'; import Link from 'next/link';
import { PlusCircle } from 'lucide-react'; import { PlusCircle } from 'lucide-react';
import { getI18n } from '@/locales/server'; import { getI18n } from '@/locales/server';
import { getAllToys } from '@/data/operations';
export default async function HomePage() { export default async function HomePage() {
const t = await getI18n(); const t = await getI18n();
const toys = getAllToys();
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@ -37,7 +38,7 @@ export default async function HomePage() {
<h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary"> <h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary">
{t('home.available_toys')} {t('home.available_toys')}
</h2> </h2>
<ToyList toys={mockToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} t={t} /> <ToyList toys={toys} t={t} />
</section> </section>
</div> </div>
); );

View File

@ -1,9 +1,9 @@
import Image from 'next/image'; import Image from 'next/image';
import { mockToys } from '@/lib/mockData'; import { getToyById } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; 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 { ArrowLeft, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Badge } from '@/components/ui/badge'; 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 { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import type { Locale } from '@/locales/server'; import type { Locale } from '@/locales/server';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 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 { interface ToyPageProps {
params: { id: string, locale: Locale }; params: { id: string, locale: Locale };
} }
async function getToyById(id: string): Promise<Toy | undefined> {
await new Promise(resolve => setTimeout(resolve, 200));
return mockToys.find(toy => toy.id === id);
}
export default async function ToyPage({ params }: ToyPageProps) { export default async function ToyPage({ params }: ToyPageProps) {
const t = await getI18n(); const t = await getI18n();
const toy = await getToyById(params.id); const toy = getToyById(params.id);
if (!toy) { if (!toy) {
return ( return (
@ -44,16 +40,11 @@ export default async function ToyPage({ params }: ToyPageProps) {
const placeholderHint = toy.category.toLowerCase() || "toy detail"; const placeholderHint = toy.category.toLowerCase() || "toy detail";
const disabledDates = toy.unavailableRanges.map(range => { const disabledDates = toy.unavailableRanges.map(range => {
// react-day-picker expects Date objects for ranges
const from = parseISO(range.startDate); const from = parseISO(range.startDate);
const to = parseISO(range.endDate); 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 { from, to };
}); });
return ( return (
<div className="container mx-auto py-8 px-4"> <div className="container mx-auto py-8 px-4">
<Link href={`/${params.locale}/`} className="inline-flex items-center text-primary hover:underline mb-6 group"> <Link href={`/${params.locale}/`} className="inline-flex items-center text-primary hover:underline mb-6 group">
@ -140,9 +131,9 @@ export default async function ToyPage({ params }: ToyPageProps) {
</CardHeader> </CardHeader>
<CardContent className="flex justify-center"> <CardContent className="flex justify-center">
<Calendar <Calendar
mode="single" // "single" is fine for display, range selection would be for booking mode="single"
disabled={disabledDates} // Disable booked ranges disabled={disabledDates}
month={new Date()} // Show current month by default month={new Date()}
className="rounded-md border" className="rounded-md border"
/> />
</CardContent> </CardContent>
@ -162,7 +153,8 @@ export default async function ToyPage({ params }: ToyPageProps) {
export function generateStaticParams() { export function generateStaticParams() {
const localeParams = getLocaleStaticParams(); 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 => return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id })) toyParams.map(toy => ({ ...lang, id: toy.id }))

69
src/data/operations.ts Normal file
View File

@ -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;
}

88
src/lib/db.ts Normal file
View File

@ -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;

View File

@ -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'; import { addDays, formatISO, subDays } from 'date-fns';
const today = new Date(); const today = new Date();
export const mockToys: Toy[] = [ export const rawUsers: Omit<User, 'email'>[] = [
{ 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<Toy, 'ownerName' | 'dataAiHint'>[] = [
{ {
id: '1', id: '1',
name: 'Colorful Building Blocks Set', 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, 5), { representation: 'date' }), endDate: formatISO(addDays(today, 7), { representation: 'date' }) },
{ startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) },
], ],
ownerName: 'Alice Wonderland',
ownerId: 'user1', ownerId: 'user1',
pricePerDay: 5, pricePerDay: 5,
location: 'Springfield Gardens', location: 'Springfield Gardens',
dataAiHint: 'building blocks'
}, },
{ {
id: '2', id: '2',
@ -30,11 +38,9 @@ export const mockToys: Toy[] = [
unavailableRanges: [ unavailableRanges: [
{ startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) },
], ],
ownerName: 'Bob The Builder',
ownerId: 'user2', ownerId: 'user2',
pricePerDay: 8, pricePerDay: 8,
location: 'Willow Creek', location: 'Willow Creek',
dataAiHint: 'remote car'
}, },
{ {
id: '3', id: '3',
@ -43,11 +49,9 @@ export const mockToys: Toy[] = [
category: 'Electronics', category: 'Electronics',
images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'], images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'],
unavailableRanges: [], unavailableRanges: [],
ownerName: 'Carol Danvers',
ownerId: 'user3', ownerId: 'user3',
pricePerDay: 7, pricePerDay: 7,
location: 'Metro City', location: 'Metro City',
dataAiHint: 'learning tablet'
}, },
{ {
id: '4', id: '4',
@ -58,11 +62,9 @@ export const mockToys: Toy[] = [
unavailableRanges: [ unavailableRanges: [
{ startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) },
], ],
ownerName: 'Alice Wonderland',
ownerId: 'user1', ownerId: 'user1',
pricePerDay: 3, pricePerDay: 3,
location: 'Springfield Gardens', location: 'Springfield Gardens',
dataAiHint: 'teddy bear'
}, },
{ {
id: '5', id: '5',
@ -71,11 +73,9 @@ export const mockToys: Toy[] = [
category: 'Musical', category: 'Musical',
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'], images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
unavailableRanges: [], unavailableRanges: [],
ownerName: 'Bob The Builder',
ownerId: 'user2', ownerId: 'user2',
pricePerDay: 10, pricePerDay: 10,
location: 'Willow Creek', location: 'Willow Creek',
dataAiHint: 'acoustic guitar'
}, },
{ {
id: '6', id: '6',
@ -84,111 +84,33 @@ export const mockToys: Toy[] = [
category: 'Outdoor', category: 'Outdoor',
images: ['https://placehold.co/600x400.png?text=Sports+Kit'], images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
unavailableRanges: [], unavailableRanges: [],
ownerName: 'Carol Danvers',
ownerId: 'user3', ownerId: 'user3',
pricePerDay: 6, pricePerDay: 6,
location: 'Metro City', location: 'Metro City',
dataAiHint: 'outdoor sports'
} }
]; ];
export const mockRentalHistory: RentalHistoryEntry[] = [ // NOTE: The data below is not yet migrated to the database.
{ // These pages will be updated in subsequent steps.
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(),
}
];
export const mockRentalRequests: RentalRequest[] = [ export const mockToys = []; // Legacy, will be removed later
{ export const mockRentalHistory: RentalHistoryEntry[] = [];
id: 'req1', export const mockRentalRequests: RentalRequest[] = [];
toy: mockToys[0], // Owned by user1 (Alice) export const mockMessages: (MessageEntry & { rentalRequestId: string })[] = [];
requesterName: 'Charlie Brown', export const mockOwnerProfiles: Record<string, { avatarUrl: string; bio: string; name?: string }> = {
requesterId: 'user4', 'user1': {
requestedDates: 'August 10, 2024 - August 17, 2024', name: 'Alice W.',
status: 'pending', avatarUrl: 'https://placehold.co/100x100.png?text=AW',
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?', 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!"
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(),
}, },
{ 'user2': {
id: 'req2', name: 'Bob T.B.',
toy: mockToys[3], // Owned by user1 (Alice) avatarUrl: 'https://placehold.co/100x100.png?text=BT',
requesterName: 'Diana Prince', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder."
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(),
}, },
{ 'user3': {
id: 'req3', name: 'Captain C.',
toy: mockToys[0], // Owned by user1 (Alice) avatarUrl: 'https://placehold.co/100x100.png?text=CD',
requesterName: 'Edward Nigma', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets."
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(),
} }
]; };
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];
}
});

View File

@ -2,7 +2,7 @@
export interface MessageEntry { export interface MessageEntry {
id: string; id: string;
senderId: string; senderId: string;
senderName: string; // For easier display without needing to look up user by ID constantly in UI senderName: string;
text: string; text: string;
timestamp: string; // ISO date string timestamp: string; // ISO date string
} }
@ -12,37 +12,38 @@ export interface Toy {
name: string; name: string;
description: string; description: string;
category: string; category: string;
images: string[]; // Array of image URLs images: string[];
unavailableRanges: { startDate: string; endDate: string }[]; // New field for booked/unavailable date ranges unavailableRanges: { startDate: string; endDate: string }[];
ownerName: string; // Simplified for now ownerName: string;
ownerId: string; ownerId: string;
pricePerDay?: number; // Optional daily rental price pricePerDay?: number;
location?: string; // Optional, e.g., "City, State" or "Neighborhood" location?: string;
dataAiHint?: string; // For placeholder image keyword hint dataAiHint?: string;
} }
export interface User { export interface User {
id: string; id: string;
name: string; name: string;
email: 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 { export interface DailyAvailability {
date: Date; date: Date;
isAvailable: boolean; isAvailable: boolean;
bookedBy?: string; // User ID of the renter if booked bookedBy?: string;
} }
export interface RentalHistoryEntry { export interface RentalHistoryEntry {
id:string; id:string;
userId: string; // ID of the user who rented userId: string;
toy: Toy; toy: Toy;
rentalStartDate: string; // ISO date string rentalStartDate: string;
rentalEndDate: string; // ISO date string rentalEndDate: string;
totalCost: number; totalCost: number;
status: 'Completed' | 'Returned'; // Example statuses status: 'Completed' | 'Returned';
dataAiHint?: string; dataAiHint?: string;
} }
@ -53,7 +54,7 @@ export interface RentalRequest {
requesterId: string; requesterId: string;
requestedDates: string; requestedDates: string;
status: 'pending' | 'approved' | 'declined'; status: 'pending' | 'approved' | 'declined';
message?: string; // Initial request message message?: string;
messages?: MessageEntry[]; // Conversation thread messages?: MessageEntry[];
dataAiHint?: string; dataAiHint?: string;
} }