diff --git a/client/package-lock.json b/client/package-lock.json index 954bb72..0022e52 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.15.2", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.14.2" }, "devDependencies": { @@ -274,9 +275,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -286,9 +287,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, @@ -600,9 +601,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.126.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", - "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -610,9 +611,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "cpu": [ "arm64" ], @@ -627,9 +628,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "cpu": [ "arm64" ], @@ -644,9 +645,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "cpu": [ "x64" ], @@ -661,9 +662,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", - "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "cpu": [ "x64" ], @@ -678,9 +679,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", - "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "cpu": [ "arm" ], @@ -695,9 +696,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "cpu": [ "arm64" ], @@ -712,9 +713,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "cpu": [ "arm64" ], @@ -729,9 +730,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "cpu": [ "ppc64" ], @@ -746,9 +747,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "cpu": [ "s390x" ], @@ -763,9 +764,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", - "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "cpu": [ "x64" ], @@ -780,9 +781,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", - "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "cpu": [ "x64" ], @@ -797,9 +798,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", - "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "cpu": [ "arm64" ], @@ -814,9 +815,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", - "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "cpu": [ "wasm32" ], @@ -824,8 +825,8 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { @@ -833,9 +834,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ "arm64" ], @@ -850,9 +851,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", - "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -1575,9 +1576,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1676,9 +1677,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", - "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "version": "2.10.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", + "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1757,9 +1758,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", "dev": true, "funding": [ { @@ -1872,7 +1873,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -1934,21 +1934,21 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.343", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", - "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -2443,6 +2443,15 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3272,6 +3281,23 @@ "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": { "version": "7.14.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", @@ -3321,14 +3347,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", - "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.126.0", - "@rolldown/pluginutils": "1.0.0-rc.16" + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3337,27 +3363,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", - "@rolldown/binding-darwin-x64": "1.0.0-rc.16", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.16", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", - "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, @@ -3601,16 +3627,16 @@ } }, "node_modules/vite": { - "version": "8.0.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", - "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.16", + "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "bin": { diff --git a/client/package.json b/client/package.json index 5cf6e81..fbbb415 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,7 @@ "axios": "^1.15.2", "react": "^19.2.5", "react-dom": "^19.2.5", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.14.2" }, "devDependencies": { diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx new file mode 100644 index 0000000..ec55611 --- /dev/null +++ b/client/src/components/Navbar.tsx @@ -0,0 +1,36 @@ +// src/components/Navbar.tsx +import { useAuth } from '../context/AuthContext' + +const Navbar = () => { + const { user, logout } = useAuth() + + return ( + + ) +} + +export default Navbar \ No newline at end of file diff --git a/client/src/components/todos/TodoForm.tsx b/client/src/components/todos/TodoForm.tsx new file mode 100644 index 0000000..e8f7367 --- /dev/null +++ b/client/src/components/todos/TodoForm.tsx @@ -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 +} + +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 ( +
+ {/* Main input row */} +
+ 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" + /> + +
+ + {/* Expanded options — show when input is focused */} + {isExpanded && ( +
+ + {/* Priority selector */} + + + {/* Due date */} + setDueDate(e.target.value)} + className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 outline-none text-gray-700" + /> + + {/* Description */} + 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" + /> + +
+ )} +
+ ) +} + +export default TodoForm \ No newline at end of file diff --git a/client/src/components/todos/TodoItem.tsx b/client/src/components/todos/TodoItem.tsx new file mode 100644 index 0000000..60e6544 --- /dev/null +++ b/client/src/components/todos/TodoItem.tsx @@ -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) => 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 ( +
+ + 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" + /> + + + + {/* Save button */} + + + {/* Cancel button */} + + +
+ ) + } + + // ── Normal Mode UI ───────────────────────────────── + return ( +
+ + {/* Checkbox */} + + + {/* Content */} +
+

+ {todo.title} +

+ +
+ {todo.description && ( + + {todo.description} + + )} + {todo.dueDate && ( + + )} +
+
+ + {/* Priority badge */} + + {priority.label} + + + {/* Edit button — visible on hover */} + + + {/* Delete button */} + + +
+ ) +} + +// ── 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 ( + + 📅 {isOverdue && '⚠️ '} + {date.toLocaleDateString()} + {isToday && ' (Today)'} + + ) +} + +export default TodoItem \ No newline at end of file diff --git a/client/src/components/todos/TodoList.tsx b/client/src/components/todos/TodoList.tsx new file mode 100644 index 0000000..31dec4f --- /dev/null +++ b/client/src/components/todos/TodoList.tsx @@ -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) => void // ← add this + isLoading: boolean +} + +const TodoList = ({ todos, onToggle, onDelete, onUpdate, isLoading }: TodoListProps) => { + + if (isLoading) { + return ( +
+ {[1,2,3].map(i => ( +
+ ))} +
+ ) + } + + if (todos.length === 0) { + return ( +
+

✅

+

No todos here!

+

Add one above to get started

+
+ ) + } + + return ( +
+ {todos.map(todo => ( + + ))} +
+ ) +} + +export default TodoList \ No newline at end of file diff --git a/client/src/hooks/useTodos.ts b/client/src/hooks/useTodos.ts new file mode 100644 index 0000000..579c4da --- /dev/null +++ b/client/src/hooks/useTodos.ts @@ -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([]) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + + 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) => { + 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 + } +} \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index bef5202..af3cb0f 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,10 +1,24 @@ +// src/main.tsx import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { Toaster } from 'react-hot-toast' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( - , -) + + +) \ No newline at end of file diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 8193b38..6d0ec19 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -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 = () => { - return
Dashboard Page
+ const { user } = useAuth() + const { todos, isLoading, createTodo, toggleTodo, deleteTodo, updateTodo } = useTodos() + const [filter, setFilter] = useState('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 ( +
+ + +
+ +
+

+ Good day, {user?.name}! +

+

+ You have {todos.filter(t => !t.completed).length} pending todos +

+
+ + + +
+ {filters.map(f => ( + + ))} +
+ + + +
+
+ ) } + export default DashboardPage \ No newline at end of file diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index b48d362..f04e4b1 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -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 = () => { - return
Login Page
+ // ── State ────────────────────────────────────────── + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [isLoading, setIsLoading] = useState(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('/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 +
+ + {/* Card container */} +
+ + {/* Header */} +
+

+ Welcome back +

+

+ Sign in to your account +

+
+ + {/* Error message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} +
+ + {/* Email field */} +
+ + 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" + /> +
+ + {/* Password field */} +
+ + 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" + /> +
+ + {/* Submit button */} + + +
+ + {/* Register link */} +

+ Don't have an account?{' '} + + Register + +

+ +
+
+ ) } -export default LoginPage + +export default LoginPage \ No newline at end of file diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx index 7b7a74d..dc0c567 100644 --- a/client/src/pages/RegisterPage.tsx +++ b/client/src/pages/RegisterPage.tsx @@ -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 = () => { - return
Register Page
+ const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState('') + const [isLoading, setIsLoading] = useState(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('/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 ( +
+
+ +
+

+ Create account +

+

+ Sign up for a new account +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ +
+ + 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" + /> +
+ + +
+ +

+ Already have an account?{' '} + + Sign in + +

+
+
+ ) } + export default RegisterPage \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27d912e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "ToDo", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}