Completed the ToDo App
This commit is contained in:
226
client/package-lock.json
generated
226
client/package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-router-dom": "^7.14.2"
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -274,9 +275,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -286,9 +287,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -600,9 +601,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.126.0",
|
"version": "0.127.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -610,9 +611,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -627,9 +628,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -644,9 +645,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -661,9 +662,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -678,9 +679,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -695,9 +696,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -712,9 +713,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -729,9 +730,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -746,9 +747,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -763,9 +764,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -780,9 +781,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -797,9 +798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -814,9 +815,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -824,8 +825,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.10.0",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.10.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -833,9 +834,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -850,9 +851,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1575,9 +1576,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1676,9 +1677,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.20",
|
"version": "2.10.22",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz",
|
||||||
"integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
|
"integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1757,9 +1758,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001788",
|
"version": "1.0.30001790",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
|
||||||
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
|
"integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1872,7 +1873,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@@ -1934,21 +1934,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.343",
|
"version": "1.5.344",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
||||||
"integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==",
|
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.20.1",
|
"version": "5.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
|
||||||
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
|
"integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.4",
|
"graceful-fs": "^4.2.4",
|
||||||
"tapable": "^2.3.0"
|
"tapable": "^2.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
@@ -2443,6 +2443,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/goober": {
|
||||||
|
"version": "2.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||||
|
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"csstype": "^3.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -3272,6 +3281,23 @@
|
|||||||
"react": "^19.2.5"
|
"react": "^19.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hot-toast": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.1.3",
|
||||||
|
"goober": "^2.1.16"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.14.2",
|
"version": "7.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
||||||
@@ -3321,14 +3347,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.126.0",
|
"@oxc-project/types": "=0.127.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -3337,27 +3363,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -3601,16 +3627,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.9",
|
"version": "8.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.10",
|
||||||
"rolldown": "1.0.0-rc.16",
|
"rolldown": "1.0.0-rc.17",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-router-dom": "^7.14.2"
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
36
client/src/components/Navbar.tsx
Normal file
36
client/src/components/Navbar.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/components/Navbar.tsx
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const { user, logout } = useAuth()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="bg-white border-b border-gray-200 px-6 py-4">
|
||||||
|
<div className="max-w-3xl mx-auto flex items-center justify-between">
|
||||||
|
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-gray-900 text-lg">
|
||||||
|
TodoApp
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side — user + logout */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{user?.name}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="text-sm text-gray-500 hover:text-red-500 transition font-medium"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navbar
|
||||||
105
client/src/components/todos/TodoForm.tsx
Normal file
105
client/src/components/todos/TodoForm.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// src/components/todos/TodoForm.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
// Props — what this component receives from parent
|
||||||
|
interface TodoFormProps {
|
||||||
|
onSubmit: (data: {
|
||||||
|
title: string
|
||||||
|
priority: 'low' | 'medium' | 'high'
|
||||||
|
description?: string
|
||||||
|
dueDate?: string
|
||||||
|
}) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoForm = ({ onSubmit }: TodoFormProps) => {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium')
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [dueDate, setDueDate] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!title.trim()) return // dont submit empty title
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
await onSubmit({
|
||||||
|
title: title.trim(),
|
||||||
|
priority,
|
||||||
|
description: description || undefined,
|
||||||
|
dueDate: dueDate || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset form after submit
|
||||||
|
setTitle('')
|
||||||
|
setDescription('')
|
||||||
|
setDueDate('')
|
||||||
|
setPriority('medium')
|
||||||
|
setIsExpanded(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="bg-white rounded-xl border border-gray-200 p-4 mb-6"
|
||||||
|
>
|
||||||
|
{/* Main input row */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
onFocus={() => setIsExpanded(true)}
|
||||||
|
placeholder="Add a new todo..."
|
||||||
|
className="flex-1 text-sm outline-none text-gray-800 placeholder-gray-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!title.trim() || isLoading}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white text-sm font-medium px-4 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Adding...' : 'Add'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded options — show when input is focused */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-100 flex flex-wrap gap-3">
|
||||||
|
|
||||||
|
{/* Priority selector */}
|
||||||
|
<select
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) => setPriority(e.target.value as 'low' | 'medium' | 'high')}
|
||||||
|
className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 outline-none text-gray-700"
|
||||||
|
>
|
||||||
|
<option value="low">🟢 Low</option>
|
||||||
|
<option value="medium">🟡 Medium</option>
|
||||||
|
<option value="high">🔴 High</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Due date */}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dueDate}
|
||||||
|
onChange={(e) => setDueDate(e.target.value)}
|
||||||
|
className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 outline-none text-gray-700"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Add description..."
|
||||||
|
className="flex-1 text-sm border border-gray-200 rounded-lg px-3 py-1.5 outline-none text-gray-700"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TodoForm
|
||||||
198
client/src/components/todos/TodoItem.tsx
Normal file
198
client/src/components/todos/TodoItem.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
// src/components/todos/TodoItem.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import type { Todo } from '../../types'
|
||||||
|
|
||||||
|
interface TodoItemProps {
|
||||||
|
todo: Todo
|
||||||
|
onToggle: (id: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
onUpdate: (id: string, data: Partial<Todo>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityConfig = {
|
||||||
|
high: { label: 'High', className: 'bg-red-100 text-red-600' },
|
||||||
|
medium: { label: 'Medium', className: 'bg-yellow-100 text-yellow-600' },
|
||||||
|
low: { label: 'Low', className: 'bg-green-100 text-green-600' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoItem = ({ todo, onToggle, onDelete, onUpdate }: TodoItemProps) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [editTitle, setEditTitle] = useState(todo.title)
|
||||||
|
const [editPriority, setEditPriority] = useState(todo.priority)
|
||||||
|
|
||||||
|
const priority = priorityConfig[todo.priority]
|
||||||
|
|
||||||
|
// Save edit
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!editTitle.trim()) return
|
||||||
|
onUpdate(todo._id, {
|
||||||
|
title: editTitle.trim(),
|
||||||
|
priority: editPriority
|
||||||
|
})
|
||||||
|
setIsEditing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel edit — reset to original values
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditTitle(todo.title)
|
||||||
|
setEditPriority(todo.priority)
|
||||||
|
setIsEditing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save on Enter, cancel on Escape
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') handleSave()
|
||||||
|
if (e.key === 'Escape') handleCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Edit Mode UI ───────────────────────────────────
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 p-4 rounded-xl border border-blue-300 bg-blue-50">
|
||||||
|
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
value={editTitle}
|
||||||
|
onChange={(e) => setEditTitle(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="flex-1 text-sm bg-white border border-gray-200 rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={editPriority}
|
||||||
|
onChange={(e) => setEditPriority(e.target.value as 'low' | 'medium' | 'high')}
|
||||||
|
className="text-sm border border-gray-200 rounded-lg px-2 py-2 outline-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="low">🟢 Low</option>
|
||||||
|
<option value="medium">🟡 Medium</option>
|
||||||
|
<option value="high">🔴 High</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Save button */}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="text-sm bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded-lg transition font-medium"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Cancel button */}
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-700 px-3 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Normal Mode UI ─────────────────────────────────
|
||||||
|
return (
|
||||||
|
<div className={`
|
||||||
|
flex items-center gap-4 p-4 rounded-xl border transition group
|
||||||
|
${todo.completed
|
||||||
|
? 'bg-gray-50 border-gray-100'
|
||||||
|
: 'bg-white border-gray-200 hover:border-gray-300'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
|
||||||
|
{/* Checkbox */}
|
||||||
|
<button
|
||||||
|
onClick={() => onToggle(todo._id)}
|
||||||
|
className={`
|
||||||
|
w-5 h-5 rounded-full border-2 flex items-center
|
||||||
|
justify-center flex-shrink-0 transition
|
||||||
|
${todo.completed
|
||||||
|
? 'bg-blue-600 border-blue-600'
|
||||||
|
: 'border-gray-300 hover:border-blue-400'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{todo.completed && (
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className={`
|
||||||
|
text-sm font-medium truncate
|
||||||
|
${todo.completed ? 'line-through text-gray-400' : 'text-gray-800'}
|
||||||
|
`}>
|
||||||
|
{todo.title}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 mt-0.5">
|
||||||
|
{todo.description && (
|
||||||
|
<span className="text-xs text-gray-400 truncate">
|
||||||
|
{todo.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{todo.dueDate && (
|
||||||
|
<DueDateBadge dueDate={todo.dueDate} completed={todo.completed} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority badge */}
|
||||||
|
<span className={`
|
||||||
|
text-xs font-medium px-2.5 py-1 rounded-full flex-shrink-0
|
||||||
|
${priority.className}
|
||||||
|
`}>
|
||||||
|
{priority.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Edit button — visible on hover */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className="text-gray-300 hover:text-blue-500 transition flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Delete button */}
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(todo._id)}
|
||||||
|
className="text-gray-300 hover:text-red-500 transition flex-shrink-0 opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Due Date Badge ─────────────────────────────────────
|
||||||
|
// Separate small component — single responsibility
|
||||||
|
const DueDateBadge = ({ dueDate, completed }: { dueDate: string, completed: boolean }) => {
|
||||||
|
const date = new Date(dueDate)
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
const isOverdue = date < today && !completed
|
||||||
|
const isToday = date.toDateString() === today.toDateString()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`
|
||||||
|
text-xs flex items-center gap-1
|
||||||
|
${isOverdue ? 'text-red-500 font-medium' : ''}
|
||||||
|
${isToday ? 'text-orange-500 font-medium' : ''}
|
||||||
|
${!isOverdue && !isToday ? 'text-gray-400' : ''}
|
||||||
|
`}>
|
||||||
|
📅 {isOverdue && '⚠️ '}
|
||||||
|
{date.toLocaleDateString()}
|
||||||
|
{isToday && ' (Today)'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TodoItem
|
||||||
50
client/src/components/todos/TodoList.tsx
Normal file
50
client/src/components/todos/TodoList.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// src/components/todos/TodoList.tsx
|
||||||
|
import type { Todo } from '../../types'
|
||||||
|
import TodoItem from './TodoItem'
|
||||||
|
|
||||||
|
interface TodoListProps {
|
||||||
|
todos: Todo[]
|
||||||
|
onToggle: (id: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
onUpdate: (id: string, data: Partial<Todo>) => void // ← add this
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodoList = ({ todos, onToggle, onDelete, onUpdate, isLoading }: TodoListProps) => {
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{[1,2,3].map(i => (
|
||||||
|
<div key={i} className="h-16 bg-white rounded-xl border border-gray-200 animate-pulse" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todos.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-4xl mb-3">✅</p>
|
||||||
|
<p className="text-gray-500 text-sm font-medium">No todos here!</p>
|
||||||
|
<p className="text-gray-400 text-xs mt-1">Add one above to get started</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{todos.map(todo => (
|
||||||
|
<TodoItem
|
||||||
|
key={todo._id}
|
||||||
|
todo={todo}
|
||||||
|
onToggle={onToggle}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onUpdate={onUpdate} // ← pass down
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TodoList
|
||||||
94
client/src/hooks/useTodos.ts
Normal file
94
client/src/hooks/useTodos.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// src/hooks/useTodos.ts
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import api from '../api/axios'
|
||||||
|
import type { Todo } from '../types'
|
||||||
|
|
||||||
|
export const useTodos = () => {
|
||||||
|
const [todos, setTodos] = useState<Todo[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
|
||||||
|
const fetchTodos = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await api.get<{ todos: Todo[] }>('/todos')
|
||||||
|
setTodos(response.data.todos)
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to load todos')
|
||||||
|
setError('Failed to fetch todos')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTodo = async (data: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
priority?: 'low' | 'medium' | 'high'
|
||||||
|
dueDate?: string
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post<{ todo: Todo }>('/todos', data)
|
||||||
|
setTodos(prev => [response.data.todo, ...prev])
|
||||||
|
toast.success('Todo created!')
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to create todo')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleTodo = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await api.patch<{ todo: Todo }>(`/todos/${id}/toggle`)
|
||||||
|
setTodos(prev =>
|
||||||
|
prev.map(todo =>
|
||||||
|
todo._id === id ? response.data.todo : todo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Dynamic message based on new state
|
||||||
|
const isNowComplete = response.data.todo.completed
|
||||||
|
toast.success(isNowComplete ? 'Todo completed! 🎉' : 'Todo reopened')
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to update todo')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteTodo = async (id: string) => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/todos/${id}`)
|
||||||
|
setTodos(prev => prev.filter(todo => todo._id !== id))
|
||||||
|
toast.success('Todo deleted')
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to delete todo')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTodo = async (id: string, data: Partial<Todo>) => {
|
||||||
|
try {
|
||||||
|
const response = await api.put<{ todo: Todo }>(`/todos/${id}`, data)
|
||||||
|
setTodos(prev =>
|
||||||
|
prev.map(todo =>
|
||||||
|
todo._id === id ? response.data.todo : todo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
toast.success('Todo updated!')
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to update todo')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTodos()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
todos,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
createTodo,
|
||||||
|
toggleTodo,
|
||||||
|
deleteTodo,
|
||||||
|
updateTodo,
|
||||||
|
fetchTodos
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
|
// src/main.tsx
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { Toaster } from 'react-hot-toast'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 3000,
|
||||||
|
style: {
|
||||||
|
background: '#1f2937',
|
||||||
|
color: '#f9fafb',
|
||||||
|
fontSize: '14px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StrictMode>
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,86 @@
|
|||||||
|
// src/pages/DashboardPage.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { useTodos } from '../hooks/useTodos'
|
||||||
|
import Navbar from '../components/Navbar'
|
||||||
|
import TodoForm from '../components/todos/TodoForm'
|
||||||
|
import TodoList from '../components/todos/TodoList'
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'pending' | 'completed'
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
return <div>Dashboard Page</div>
|
const { user } = useAuth()
|
||||||
|
const { todos, isLoading, createTodo, toggleTodo, deleteTodo, updateTodo } = useTodos()
|
||||||
|
const [filter, setFilter] = useState<FilterType>('all')
|
||||||
|
|
||||||
|
const filteredTodos = todos.filter(todo => {
|
||||||
|
if (filter === 'completed') return todo.completed
|
||||||
|
if (filter === 'pending') return !todo.completed
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const filters: { label: string; value: FilterType; count: number }[] = [
|
||||||
|
{ label: 'All', value: 'all', count: todos.length },
|
||||||
|
{ label: 'Pending', value: 'pending', count: todos.filter(t => !t.completed).length },
|
||||||
|
{ label: 'Completed', value: 'completed', count: todos.filter(t => t.completed).length },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<main className="max-w-3xl mx-auto px-4 py-8">
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
Good day, {user?.name}!
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 text-sm mt-1">
|
||||||
|
You have {todos.filter(t => !t.completed).length} pending todos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TodoForm onSubmit={createTodo} />
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
{filters.map(f => (
|
||||||
|
<button
|
||||||
|
key={f.value}
|
||||||
|
onClick={() => setFilter(f.value)}
|
||||||
|
className={`
|
||||||
|
px-4 py-2 rounded-lg text-sm font-medium transition
|
||||||
|
flex items-center gap-2
|
||||||
|
${filter === f.value
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white text-gray-600 border border-gray-200 hover:border-gray-300'
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{f.label}
|
||||||
|
<span className={`
|
||||||
|
text-xs px-1.5 py-0.5 rounded-full
|
||||||
|
${filter === f.value
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-100 text-gray-500'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{f.count}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TodoList
|
||||||
|
todos={filteredTodos}
|
||||||
|
onToggle={toggleTodo}
|
||||||
|
onDelete={deleteTodo}
|
||||||
|
onUpdate={updateTodo}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default DashboardPage
|
export default DashboardPage
|
||||||
@@ -1,4 +1,132 @@
|
|||||||
|
// src/pages/LoginPage.tsx
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import api from '../api/axios'
|
||||||
|
import type { AuthResponse } from '../types'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
return <div>Login Page</div>
|
// ── State ──────────────────────────────────────────
|
||||||
|
const [email, setEmail] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
// ── Hooks ──────────────────────────────────────────
|
||||||
|
const { login } = useAuth() // get login function from context
|
||||||
|
const navigate = useNavigate() // programmatic navigation
|
||||||
|
|
||||||
|
// ── Submit Handler ─────────────────────────────────
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('') // clear previous errors
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post<AuthResponse>('/auth/login', {
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
login(response.data) // save token + user to context
|
||||||
|
navigate('/dashboard') // redirect to dashboard
|
||||||
|
} catch (err: unknown) {
|
||||||
|
// Axios wraps errors — need to extract message
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || 'Login failed'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setError('Something went wrong')
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI ─────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
// Full screen centered layout
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
|
||||||
|
{/* Card container */}
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 w-full max-w-md p-8">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
Welcome back
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-2 text-sm">
|
||||||
|
Sign in to your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error message */}
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 text-sm px-4 py-3 rounded-lg mb-6">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
|
||||||
|
{/* Email field */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password field */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium py-2.5 px-4 rounded-lg text-sm transition"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Register link */}
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
Don't have an account?{' '}
|
||||||
|
<Link
|
||||||
|
to="/register"
|
||||||
|
className="text-blue-600 hover:underline font-medium"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default LoginPage
|
export default LoginPage
|
||||||
@@ -1,4 +1,145 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import api from '../api/axios'
|
||||||
|
import type { AuthResponse } from '../types'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
const RegisterPage = () => {
|
const RegisterPage = () => {
|
||||||
return <div>Register Page</div>
|
const [name, setName] = useState<string>('')
|
||||||
|
const [email, setEmail] = useState<string>('')
|
||||||
|
const [password, setPassword] = useState<string>('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState<string>('')
|
||||||
|
const [error, setError] = useState<string>('')
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const { login } = useAuth()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
// Validate passwords match BEFORE api call
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match')
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await api.post<AuthResponse>('/auth/register', {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
login(response.data) // ← auto login
|
||||||
|
navigate('/dashboard') // ← redirect
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
setError(err.response?.data?.message || 'Register failed')
|
||||||
|
} else {
|
||||||
|
setError('Something went wrong')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 w-full max-w-md p-8">
|
||||||
|
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
Create account
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-2 text-sm">
|
||||||
|
Sign up for a new account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 text-red-600 text-sm px-4 py-3 rounded-lg mb-6">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2.5 rounded-lg border border-gray-300 text-sm outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed text-white font-medium py-2.5 px-4 rounded-lg text-sm transition"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Creating account...' : 'Create account'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-gray-500 mt-6">
|
||||||
|
Already have an account?{' '}
|
||||||
|
<Link to="/login" className="text-blue-600 hover:underline font-medium">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default RegisterPage
|
export default RegisterPage
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "ToDo",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user