Completed the ToDo App

This commit is contained in:
Mosiur
2026-04-25 22:45:00 +06:00
parent c295362495
commit 4e6a1ef71f
12 changed files with 987 additions and 106 deletions

226
client/package-lock.json generated
View File

@@ -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": {

View File

@@ -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": {

View 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

View 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

View 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

View 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

View 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
}
}

View File

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

View File

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

View File

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

View File

@@ -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
View File

@@ -0,0 +1,6 @@
{
"name": "ToDo",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}