From 682339de43939d96a3cb8c67d0d0d028c4312555 Mon Sep 17 00:00:00 2001 From: Shraddha Rao Date: Fri, 17 Oct 2025 01:36:41 -0500 Subject: [PATCH] added a rough ux/ui --- client/package-lock.json | 549 +++++++++++- client/package.json | 8 +- client/src/App.css | 858 ++++++++++++++++++- client/src/App.js | 81 +- client/src/components/Analytics.js | 299 +++++++ client/src/components/Auth.js | 281 ++++++ client/src/components/Budgets.js | 253 ++++++ client/src/components/Categories.js | 254 +++++- client/src/components/Dashboard.js | 244 ++++++ client/src/components/Layout.js | 163 ++++ client/src/components/Transactions.js | 387 +++++++++ client/src/components/auth/ForgotPassword.js | 164 ++++ client/src/components/auth/Login.js | 136 +++ client/src/components/auth/ProtectedRoute.js | 26 + client/src/components/auth/Signup.js | 162 ++++ client/src/contexts/AuthContext.js | 179 ++++ client/src/index.css | 774 ++++++++++++++++- client/src/services/api.js | 78 +- 18 files changed, 4791 insertions(+), 105 deletions(-) create mode 100644 client/src/components/Analytics.js create mode 100644 client/src/components/Auth.js create mode 100644 client/src/components/Budgets.js create mode 100644 client/src/components/Dashboard.js create mode 100644 client/src/components/Layout.js create mode 100644 client/src/components/Transactions.js create mode 100644 client/src/components/auth/ForgotPassword.js create mode 100644 client/src/components/auth/Login.js create mode 100644 client/src/components/auth/ProtectedRoute.js create mode 100644 client/src/components/auth/Signup.js create mode 100644 client/src/contexts/AuthContext.js diff --git a/client/package-lock.json b/client/package-lock.json index 3475387..a4c05bf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.2.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -15,9 +16,12 @@ "lucide-react": "^0.544.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.9.4", + "react-hook-form": "^7.65.0", + "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "recharts": "^3.2.1", + "web-vitals": "^2.1.4", + "yup": "^1.7.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2379,6 +2383,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3355,6 +3371,51 @@ } } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.1.tgz", + "integrity": "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3455,6 +3516,18 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4067,6 +4140,69 @@ "@types/node": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -4550,6 +4686,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -5445,6 +5587,7 @@ "version": "1.12.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -5455,6 +5598,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5979,6 +6123,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -6177,6 +6322,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6842,6 +6996,127 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6881,6 +7156,12 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -7217,6 +7498,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -7391,6 +7673,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -7456,6 +7739,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -7467,6 +7751,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -7501,6 +7786,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -8562,6 +8857,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -8891,6 +9187,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -8927,6 +9224,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -9077,6 +9375,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -9160,6 +9459,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -9185,6 +9485,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -9589,6 +9890,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", @@ -12544,6 +12854,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -14775,6 +15086,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14798,7 +15115,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/psl": { "version": "1.9.0", @@ -15083,11 +15401,50 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-hook-form": { + "version": "7.65.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", + "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15097,47 +15454,35 @@ } }, "node_modules/react-router": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", - "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "@remix-run/router": "1.23.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", "dependencies": { - "react-router": "7.9.4" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "engines": { - "node": ">=18" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-scripts": { @@ -15244,6 +15589,49 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/recharts/node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -15267,6 +15655,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", @@ -15414,6 +15817,12 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -15960,11 +16369,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" - }, "node_modules/set-function-length": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", @@ -16958,6 +17362,18 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16990,6 +17406,12 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -17366,6 +17788,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17432,6 +17863,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -18448,6 +18901,30 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/client/package.json b/client/package.json index 5b089fa..216dcd0 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@hookform/resolvers": "^5.2.2", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -10,9 +11,12 @@ "lucide-react": "^0.544.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^7.9.4", + "react-hook-form": "^7.65.0", + "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "recharts": "^3.2.1", + "web-vitals": "^2.1.4", + "yup": "^1.7.1" }, "scripts": { "start": "react-scripts start", diff --git a/client/src/App.css b/client/src/App.css index c09837b..063ad0b 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,38 +1,858 @@ -.App { +/* App Loading */ +.app-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + background-color: var(--background); + color: var(--text-color); +} + +.app-loading p { + margin-top: 1rem; + font-size: 1.1rem; +} + +/* Layout Styles */ +.layout { + display: flex; + min-height: 100vh; + background-color: var(--background); +} + +/* Sidebar */ +.sidebar { + width: 280px; + background-color: var(--sidebar-bg); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + left: -280px; + transition: left 0.3s ease; + z-index: 1000; +} + +.sidebar-open { + left: 0; +} + +.sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 999; +} + +.sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.logo-icon { + color: var(--primary-color); +} + +.logo-text { + color: var(--text-color); +} + +.sidebar-close { + background: none; + border: none; + color: var(--text-color); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.5rem; + transition: background-color 0.2s ease; +} + +.sidebar-close:hover { + background-color: var(--hover-bg); +} + +.sidebar-nav { + flex: 1; + padding: 1rem 0; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1.5rem; + color: var(--text-color); + text-decoration: none; + transition: all 0.2s ease; + border-left: 3px solid transparent; +} + +.nav-item:hover { + background-color: var(--hover-bg); + color: var(--primary-color); +} + +.nav-item-active { + background-color: var(--active-bg); + color: var(--primary-color); + border-left-color: var(--primary-color); +} + +.sidebar-footer { + padding: 1.5rem; + border-top: 1px solid var(--border-color); +} + +/* Main Content */ +.main-content { + flex: 1; + margin-left: 0; + display: flex; + flex-direction: column; +} + +/* Top Bar */ +.top-bar { + height: 64px; + background-color: var(--header-bg); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.5rem; + position: sticky; + top: 0; + z-index: 100; +} + +.menu-button { + background: none; + border: none; + color: var(--text-color); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.5rem; + transition: background-color 0.2s ease; +} + +.menu-button:hover { + background-color: var(--hover-bg); +} + +.user-menu { + display: flex; + align-items: center; + gap: 0.75rem; + position: relative; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + cursor: pointer; +} + +.user-dropdown { + position: absolute; + top: 0; + left: calc(100% + 0.5rem); + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 1rem; + min-width: 200px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + z-index: 50; +} + +.user-name { + font-weight: 600; + color: var(--text-color); + display: block; +} + +.user-email { + font-size: 0.875rem; + color: var(--text-muted); + display: block; + margin-bottom: 0.5rem; +} + +.logout-btn { + background: none; + border: none; + color: var(--danger-color); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.25rem; + transition: background-color 0.2s ease; + width: 100%; + text-align: left; +} + +.logout-btn:hover { + background-color: var(--danger-bg); +} + +/* Page Content */ +.page-content { + flex: 1; + padding: 2rem; + overflow-y: auto; +} + +/* Page Headers */ +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 2rem; +} + +.header-content h1 { + font-size: 2rem; + font-weight: 700; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.header-content p { + color: var(--text-muted); + margin: 0; +} + +.header-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-hover); +} + +.btn-secondary { + background-color: var(--secondary-color); + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background-color: var(--hover-bg); +} + +.btn-outline { + background-color: transparent; + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.btn-outline:hover { + background-color: var(--hover-bg); +} + +.btn-full { + width: 100%; + justify-content: center; +} + +.btn-icon { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.25rem; + transition: all 0.2s ease; +} + +.btn-icon:hover { + background-color: var(--hover-bg); + color: var(--text-color); +} + +.btn-icon:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Forms */ +.form-container { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 2rem; + margin-bottom: 2rem; +} + +.form-header { + margin-bottom: 1.5rem; +} + +.form-header h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + margin: 0; +} + +.form-group { + margin-bottom: 1.5rem; + display: block; + visibility: visible; + opacity: 1; +} + +.form-group label { + display: block; + font-weight: 500; + color: var(--text-color); + margin-bottom: 0.5rem; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--input-bg); + color: var(--text-color); + font-size: 0.875rem; + transition: border-color 0.2s ease; + display: block; + visibility: visible; + opacity: 1; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--primary-color); +} + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.form-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 2rem; +} + +.error { + color: var(--danger-color); + font-size: 0.75rem; + margin-top: 0.25rem; + display: block; +} + +/* Cards */ +.card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.summary-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.card-icon { + width: 48px; + height: 48px; + border-radius: 0.75rem; + background-color: var(--primary-bg); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary-color); +} + +.card-icon.spent { + background-color: var(--danger-bg); + color: var(--danger-color); +} + +.card-icon.remaining { + background-color: var(--success-bg); + color: var(--success-color); +} + +.card-content h3 { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); + margin: 0 0 0.25rem 0; +} + +.card-amount { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-color); + margin: 0; +} + +.card-subtitle { + font-size: 0.75rem; + color: var(--text-muted); +} + +/* Grids */ +.budgets-grid, +.categories-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.budget-card, +.category-card { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.2s ease; +} + +.budget-card:hover, +.category-card:hover { + border-color: var(--primary-color); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +.budget-header, +.category-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; +} + +.budget-title h3, +.category-content h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.25rem 0; +} + +.budget-date { + font-size: 0.75rem; + color: var(--text-muted); +} + +.budget-actions, +.category-actions { + display: flex; + gap: 0.5rem; +} + +.budget-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1rem; +} + +.stat-item { text-align: center; } -.App-logo { - height: 40vmin; - pointer-events: none; +.stat-label { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 0.25rem; } -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } +.stat-value { + display: block; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-color); } -.App-header { - background-color: var(--background); - min-height: 100vh; +.stat-value.spent { + color: var(--danger-color); +} + +.stat-value.remaining { + color: var(--success-color); +} + +.budget-progress { + margin-bottom: 1rem; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: var(--progress-bg); + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-fill { + height: 100%; + background-color: var(--primary-color); + transition: width 0.3s ease; +} + +.progress-text { + font-size: 0.75rem; + color: var(--text-muted); +} + +.budget-footer { + margin-top: 1rem; +} + +/* Lists */ +.transactions-list { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + overflow: hidden; +} + +.transaction-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + transition: background-color 0.2s ease; +} + +.transaction-item:last-child { + border-bottom: none; +} + +.transaction-item:hover { + background-color: var(--hover-bg); +} + +.transaction-main { + display: flex; + align-items: center; + justify-content: space-between; + flex: 1; +} + +.transaction-info h4 { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color); + margin: 0 0 0.25rem 0; +} + +.transaction-meta { + display: flex; + gap: 0.75rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +.transaction-amount { + font-size: 1rem; + font-weight: 600; + color: var(--danger-color); +} + +.transaction-actions { + display: flex; + gap: 0.5rem; + margin-left: 1rem; +} + +/* Charts */ +.charts-section, +.charts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.chart-container { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.chart-container.large { + grid-column: 1 / -1; +} + +.chart-header { + margin-bottom: 1rem; +} + +.chart-header h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.25rem 0; +} + +.chart-header p { + font-size: 0.875rem; + color: var(--text-muted); + margin: 0; +} + +/* Filters */ +.filters-section { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 2rem; +} + +.filters-row { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; +} + +.search-box { + position: relative; + flex: 1; +} + +.search-box input { + width: 100%; + padding: 0.75rem 0.75rem 0.75rem 2.5rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--input-bg); + color: var(--text-color); +} + +.search-box svg { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); +} + +.filter-group { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.filter-group select { + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--input-bg); + color: var(--text-color); +} + +.transactions-summary { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.875rem; + color: var(--text-muted); +} + +.total-amount { + font-weight: 600; + color: var(--text-color); +} + +/* Empty States */ +.empty-state { + text-align: center; + padding: 3rem 2rem; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; +} + +.empty-icon { + color: var(--text-muted); + margin-bottom: 1rem; +} + +.empty-state h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.empty-state p { + color: var(--text-muted); + margin: 0 0 1.5rem 0; +} + +/* Loading States */ +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid var(--border-color); + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.dashboard-loading, +.budgets-loading, +.transactions-loading, +.categories-loading, +.analytics-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; - font-size: calc(10px + 2vmin); + min-height: 400px; color: var(--text-color); } -.App-link { - color: #61dafb; +.dashboard-loading p, +.budgets-loading p, +.transactions-loading p, +.categories-loading p, +.analytics-loading p { + margin-top: 1rem; + color: var(--text-muted); } -@keyframes App-logo-spin { - from { - transform: rotate(0deg); +/* Responsive Design */ +@media (min-width: 768px) { + .sidebar { + position: relative; + left: 0; + height: auto; + } + + .main-content { + margin-left: 0; + } + + .menu-button { + display: none; + } + + .sidebar-overlay { + display: none; + } + + .sidebar-close { + display: none; + } +} + +@media (max-width: 768px) { + .page-content { + padding: 1rem; + } + + .summary-cards { + grid-template-columns: 1fr; + } + + .charts-section, + .charts-grid { + grid-template-columns: 1fr; + } + + .budgets-grid, + .categories-grid { + grid-template-columns: 1fr; + } + + .form-row { + grid-template-columns: 1fr; + } + + .filters-row { + flex-direction: column; + align-items: stretch; } - to { - transform: rotate(360deg); + + .page-header { + flex-direction: column; + align-items: stretch; + gap: 1rem; } } diff --git a/client/src/App.js b/client/src/App.js index 87cd88b..5ea738e 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,23 +1,74 @@ +import React from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; import './App.css'; -import ThemeToggle from './components/ThemeToggle'; +import { AuthProvider } from './contexts/AuthContext'; +import Layout from './components/Layout'; +import Dashboard from './components/Dashboard'; +import Budgets from './components/Budgets'; +import Transactions from './components/Transactions'; import Categories from './components/Categories'; -import Login from './components/Login'; -import Signup from './components/Signup'; -import { Routes, Route } from 'react-router-dom'; +import Analytics from './components/Analytics'; +import Login from './components/auth/Login'; +import Signup from './components/auth/Signup'; +import ForgotPassword from './components/auth/ForgotPassword'; +import ProtectedRoute from './components/auth/ProtectedRoute'; function App() { - return ( -
-
- - } /> - } /> - } /> - - -
-
+ +
+ + {/* Public routes */} + } /> + } /> + } /> + + {/* Protected routes */} + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + + + + + + } /> + + {/* Catch all route */} + } /> + +
+
); } diff --git a/client/src/components/Analytics.js b/client/src/components/Analytics.js new file mode 100644 index 0000000..0c7b96c --- /dev/null +++ b/client/src/components/Analytics.js @@ -0,0 +1,299 @@ +import React, { useState, useEffect } from 'react'; +import { + TrendingUp, + TrendingDown, + DollarSign, + Calendar, + Filter, + Download +} from 'lucide-react'; +import { + AreaChart, + Area, + BarChart, + Bar, + PieChart, + Pie, + Cell, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend +} from 'recharts'; + +const Analytics = () => { + const [timeRange, setTimeRange] = useState('6months'); + const [loading, setLoading] = useState(true); + const [analyticsData, setAnalyticsData] = useState({}); + + useEffect(() => { + // Mock data - will be replaced with API calls + setTimeout(() => { + setAnalyticsData({ + spendingTrend: [ + { month: 'Jan', spent: 1850, budget: 3000 }, + { month: 'Feb', spent: 2100, budget: 3000 }, + { month: 'Mar', spent: 1950, budget: 3000 }, + { month: 'Apr', spent: 2200, budget: 3000 }, + { month: 'May', spent: 1800, budget: 3000 }, + { month: 'Jun', spent: 2400, budget: 3000 } + ], + categoryBreakdown: [ + { name: 'Food', value: 850, color: '#8884d8' }, + { name: 'Transportation', value: 450, color: '#82ca9d' }, + { name: 'Entertainment', value: 300, color: '#ffc658' }, + { name: 'Shopping', value: 250, color: '#ff7300' }, + { name: 'Utilities', value: 200, color: '#00ff00' }, + { name: 'Healthcare', value: 150, color: '#0088fe' } + ], + monthlyComparison: [ + { month: 'Jan', thisYear: 1850, lastYear: 1650 }, + { month: 'Feb', thisYear: 2100, lastYear: 1900 }, + { month: 'Mar', thisYear: 1950, lastYear: 1800 }, + { month: 'Apr', thisYear: 2200, lastYear: 2000 }, + { month: 'May', thisYear: 1800, lastYear: 1750 }, + { month: 'Jun', thisYear: 2400, lastYear: 2100 } + ], + budgetPerformance: [ + { budget: 'Monthly Budget', spent: 1850, goal: 3000, percentage: 61.7 }, + { budget: 'Vacation Fund', spent: 1200, goal: 5000, percentage: 24.0 }, + { budget: 'Emergency Fund', spent: 0, goal: 10000, percentage: 0 } + ], + summary: { + totalSpent: 12300, + totalBudget: 18000, + averageMonthly: 2050, + topCategory: 'Food', + savingsRate: 31.7 + } + }); + setLoading(false); + }, 1000); + }, [timeRange]); + + if (loading) { + return ( +
+
+

Loading analytics...

+
+ ); + } + + return ( +
+
+
+

Analytics

+

Insights into your spending patterns and financial trends

+
+
+ + +
+
+ + {/* Summary Cards */} +
+
+
+ +
+
+

Total Spent

+

${analyticsData.summary?.totalSpent?.toLocaleString()}

+ This period +
+
+ +
+
+ +
+
+

Average Monthly

+

${analyticsData.summary?.averageMonthly?.toLocaleString()}

+ Spending +
+
+ +
+
+ +
+
+

Savings Rate

+

{analyticsData.summary?.savingsRate}%

+ Of total budget +
+
+ +
+
+ +
+
+

Top Category

+

{analyticsData.summary?.topCategory}

+ Highest spending +
+
+
+ + {/* Charts Section */} +
+ {/* Spending Trend */} +
+
+

Spending Trend

+

Monthly spending vs budget

+
+ + + + + + + + + + + +
+ + {/* Category Breakdown */} +
+
+

Spending by Category

+

Current period breakdown

+
+ + + `${name} ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {analyticsData.categoryBreakdown?.map((entry, index) => ( + + ))} + + + + +
+ + {/* Year-over-Year Comparison */} +
+
+

Year-over-Year Comparison

+

This year vs last year

+
+ + + + + + + + + + + +
+ + {/* Budget Performance */} +
+
+

Budget Performance

+

Spending vs goals

+
+
+ {analyticsData.budgetPerformance?.map((budget, index) => ( +
+
+

{budget.budget}

+ ${budget.spent.toLocaleString()} / ${budget.goal.toLocaleString()} +
+
+
+
+
+ {budget.percentage.toFixed(1)}% +
+
+ ))} +
+
+
+ + {/* Insights Section */} +
+

Key Insights

+
+
+ +
+

Spending Decreased

+

Your spending in June was 8% lower than May, showing good budget control.

+
+
+ +
+ +
+

Food Spending High

+

Food expenses represent 35% of your total spending. Consider meal planning.

+
+
+ +
+ +
+

Savings Rate Good

+

You're saving 32% of your budget, which is above the recommended 20%.

+
+
+
+
+
+ ); +}; + +export default Analytics; diff --git a/client/src/components/Auth.js b/client/src/components/Auth.js new file mode 100644 index 0000000..24e469f --- /dev/null +++ b/client/src/components/Auth.js @@ -0,0 +1,281 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { Eye, EyeOff, Mail, Lock, User, DollarSign } from 'lucide-react'; + +const Login = ({ onLogin }) => { + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { register, handleSubmit, formState: { errors } } = useForm(); + + const onSubmit = async (data) => { + setLoading(true); + setError(''); + + try { + // Mock API call - will be replaced with actual API integration + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Simulate successful login + localStorage.setItem('user', JSON.stringify({ + id: 1, + email: data.email, + name: 'John Doe' + })); + + onLogin(); + } catch (err) { + setError('Invalid email or password'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ + SpendWise +
+

Welcome Back

+

Sign in to your account to continue

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + +
+ +
+ {errors.email && {errors.email.message}} +
+ +
+ +
+ + + +
+ {errors.password && {errors.password.message}} +
+ +
+ + Forgot password? +
+ + +
+ +
+

Don't have an account? Sign up

+
+
+
+ ); +}; + +const Register = ({ onRegister }) => { + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { register, handleSubmit, formState: { errors } } = useForm(); + + const onSubmit = async (data) => { + if (data.password !== data.confirmPassword) { + setError('Passwords do not match'); + return; + } + + setLoading(true); + setError(''); + + try { + // Mock API call - will be replaced with actual API integration + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Simulate successful registration + localStorage.setItem('user', JSON.stringify({ + id: Date.now(), + email: data.email, + name: data.name + })); + + onRegister(); + } catch (err) { + setError('Registration failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ + SpendWise +
+

Create Account

+

Sign up to start managing your finances

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ + +
+ {errors.name && {errors.name.message}} +
+ +
+ +
+ + +
+ {errors.email && {errors.email.message}} +
+ +
+ +
+ + + +
+ {errors.password && {errors.password.message}} +
+ +
+ +
+ + + +
+ {errors.confirmPassword && {errors.confirmPassword.message}} +
+ +
+ +
+ + +
+ +
+

Already have an account? Sign in

+
+
+
+ ); +}; + +export { Login, Register }; diff --git a/client/src/components/Budgets.js b/client/src/components/Budgets.js new file mode 100644 index 0000000..8592775 --- /dev/null +++ b/client/src/components/Budgets.js @@ -0,0 +1,253 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { PlusCircle, Edit, Trash2, Eye, DollarSign } from 'lucide-react'; + +const Budgets = () => { + const [budgets, setBudgets] = useState([]); + const [showForm, setShowForm] = useState(false); + const [editingBudget, setEditingBudget] = useState(null); + const [loading, setLoading] = useState(true); + + const { register, handleSubmit, reset, formState: { errors } } = useForm(); + + useEffect(() => { + // Mock data - will be replaced with API calls + setTimeout(() => { + setBudgets([ + { + id: 1, + name: 'Monthly Budget', + financial_goal: 3000, + spent: 1850, + remaining: 1150, + created_at: '2024-01-01' + }, + { + id: 2, + name: 'Vacation Fund', + financial_goal: 5000, + spent: 1200, + remaining: 3800, + created_at: '2024-01-15' + }, + { + id: 3, + name: 'Emergency Fund', + financial_goal: 10000, + spent: 0, + remaining: 10000, + created_at: '2024-01-20' + } + ]); + setLoading(false); + }, 1000); + }, []); + + const onSubmit = (data) => { + const budgetData = { + ...data, + financial_goal: parseFloat(data.financial_goal), + spent: 0, + remaining: parseFloat(data.financial_goal) + }; + + if (editingBudget) { + // Update existing budget + setBudgets(budgets.map(budget => + budget.id === editingBudget.id + ? { ...budget, ...budgetData } + : budget + )); + } else { + // Add new budget + const newBudget = { + ...budgetData, + id: Date.now(), + created_at: new Date().toISOString().split('T')[0] + }; + setBudgets([...budgets, newBudget]); + } + + reset(); + setShowForm(false); + setEditingBudget(null); + }; + + const handleEdit = (budget) => { + setEditingBudget(budget); + reset({ + name: budget.name, + financial_goal: budget.financial_goal + }); + setShowForm(true); + }; + + const handleDelete = (budgetId) => { + if (window.confirm('Are you sure you want to delete this budget?')) { + setBudgets(budgets.filter(budget => budget.id !== budgetId)); + } + }; + + const handleCancel = () => { + setShowForm(false); + setEditingBudget(null); + reset(); + }; + + if (loading) { + return ( +
+
+

Loading budgets...

+
+ ); + } + + return ( +
+
+
+

Budgets

+

Manage your financial goals and track your spending

+
+ +
+ + {/* Budget Form */} + {showForm && ( +
+
+

{editingBudget ? 'Edit Budget' : 'Create New Budget'}

+
+ +
+
+ + + {errors.name && {errors.name.message}} +
+ +
+ + + {errors.financial_goal && {errors.financial_goal.message}} +
+ +
+ + +
+
+
+ )} + + {/* Budgets Grid */} +
+ {budgets.length === 0 ? ( +
+ +

No budgets yet

+

Create your first budget to start tracking your spending

+ +
+ ) : ( + budgets.map((budget) => ( +
+
+
+

{budget.name}

+ Created {budget.created_at} +
+
+ + +
+
+ +
+
+ Goal + ${budget.financial_goal.toLocaleString()} +
+
+ Spent + ${budget.spent.toLocaleString()} +
+
+ Remaining + ${budget.remaining.toLocaleString()} +
+
+ +
+
+
+
+
+ {((budget.spent / budget.financial_goal) * 100).toFixed(1)}% used +
+
+ +
+ +
+
+ )) + )} +
+
+ ); +}; + +export default Budgets; diff --git a/client/src/components/Categories.js b/client/src/components/Categories.js index be0522d..41bf873 100644 --- a/client/src/components/Categories.js +++ b/client/src/components/Categories.js @@ -1,35 +1,243 @@ -import { useEffect, useState } from "react"; -import api from "../services/api"; +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { PlusCircle, Edit, Trash2, Tag, AlertCircle } from 'lucide-react'; -function Categories() { +const Categories = () => { const [categories, setCategories] = useState([]); + const [showForm, setShowForm] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + + const { register, handleSubmit, reset, formState: { errors } } = useForm(); useEffect(() => { - api.get("/api/v1/categories") - .then(res => { - setCategories(res.data.data || []); - setLoading(false); - }) - .catch(err => { - setError('Failed to load categories'); - setLoading(false); - console.error(err); - }); + // Mock data - will be replaced with API calls + setTimeout(() => { + setCategories([ + { id: 1, name: 'Food', transaction_count: 15 }, + { id: 2, name: 'Transportation', transaction_count: 8 }, + { id: 3, name: 'Entertainment', transaction_count: 12 }, + { id: 4, name: 'Shopping', transaction_count: 6 }, + { id: 5, name: 'Utilities', transaction_count: 4 }, + { id: 6, name: 'Healthcare', transaction_count: 3 }, + { id: 7, name: 'Education', transaction_count: 2 }, + { id: 8, name: 'Travel', transaction_count: 1 } + ]); + setLoading(false); + }, 1000); }, []); - if (loading) return

Loading categories...

; - if (error) return

{error}

; + const onSubmit = (data) => { + if (editingCategory) { + // Update existing category + setCategories(categories.map(category => + category.id === editingCategory.id + ? { ...category, name: data.name } + : category + )); + } else { + // Add new category + const newCategory = { + id: Date.now(), + name: data.name, + transaction_count: 0 + }; + setCategories([...categories, newCategory]); + } + + reset(); + setShowForm(false); + setEditingCategory(null); + }; + + const handleEdit = (category) => { + setEditingCategory(category); + reset({ + name: category.name + }); + setShowForm(true); + }; + + const handleDelete = (categoryId) => { + const category = categories.find(c => c.id === categoryId); + if (category.transaction_count > 0) { + alert('Cannot delete category with existing transactions. Please reassign or delete transactions first.'); + return; + } + + if (window.confirm('Are you sure you want to delete this category?')) { + setCategories(categories.filter(category => category.id !== categoryId)); + } + }; + + const handleCancel = () => { + setShowForm(false); + setEditingCategory(null); + reset(); + }; + + const totalTransactions = categories.reduce((sum, category) => sum + category.transaction_count, 0); + + if (loading) { + return ( +
+
+

Loading categories...

+
+ ); + } return ( -
-

Categories

- {categories.map(({ id, attributes }) => ( -

{attributes.name}

- ))} +
+
+
+

Categories

+

Organize your transactions with custom categories

+
+ +
+ + {/* Category Form */} + {showForm && ( +
+
+

{editingCategory ? 'Edit Category' : 'Create New Category'}

+
+ +
+
+ + { + const exists = categories.some(cat => + cat.name.toLowerCase() === value.toLowerCase() && + cat.id !== editingCategory?.id + ); + return !exists || 'Category name already exists'; + } + })} + placeholder="e.g., Food, Transportation, Entertainment" + /> + {errors.name && {errors.name.message}} +
+ +
+ + +
+
+
+ )} + + {/* Categories Summary */} +
+
+
+ +
+
+

{categories.length}

+

Total Categories

+
+
+
+
+ +
+
+

{totalTransactions}

+

Total Transactions

+
+
+
+ + {/* Categories Grid */} +
+ {categories.length === 0 ? ( +
+ +

No categories yet

+

Create your first category to organize your transactions

+ +
+ ) : ( + categories.map((category) => ( +
+
+
+ +
+
+ + +
+
+ +
+

{category.name}

+
+ + {category.transaction_count} transaction{category.transaction_count !== 1 ? 's' : ''} + +
+
+ + {category.transaction_count > 0 && ( +
+ + Cannot delete - has transactions +
+ )} +
+ )) + )} +
+ + {/* Usage Tips */} +
+

Tips for Managing Categories

+
    +
  • Create specific categories for different types of expenses
  • +
  • Use consistent naming conventions (e.g., "Food & Dining" instead of "Food" and "Dining")
  • +
  • Categories with transactions cannot be deleted - reassign transactions first
  • +
  • Consider creating subcategories for detailed tracking
  • +
+
); -} +}; -export default Categories; +export default Categories; \ No newline at end of file diff --git a/client/src/components/Dashboard.js b/client/src/components/Dashboard.js new file mode 100644 index 0000000..41feabf --- /dev/null +++ b/client/src/components/Dashboard.js @@ -0,0 +1,244 @@ +import React, { useState, useEffect } from 'react'; +import { + DollarSign, + TrendingUp, + TrendingDown, + PlusCircle, + Eye, + Edit, + Trash2 +} from 'lucide-react'; +import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +const Dashboard = () => { + const [budgets, setBudgets] = useState([]); + const [recentTransactions, setRecentTransactions] = useState([]); + const [loading, setLoading] = useState(true); + + // Mock data for now - will be replaced with API calls + useEffect(() => { + // Simulate API call + setTimeout(() => { + setBudgets([ + { + id: 1, + name: 'Monthly Budget', + financial_goal: 3000, + spent: 1850, + remaining: 1150 + }, + { + id: 2, + name: 'Vacation Fund', + financial_goal: 5000, + spent: 1200, + remaining: 3800 + } + ]); + + setRecentTransactions([ + { id: 1, description: 'Grocery Shopping', amount: 150, category: 'Food', date: '2024-01-15' }, + { id: 2, description: 'Gas Station', amount: 45, category: 'Transportation', date: '2024-01-14' }, + { id: 3, description: 'Coffee Shop', amount: 12, category: 'Food', date: '2024-01-13' }, + { id: 4, description: 'Movie Tickets', amount: 25, category: 'Entertainment', date: '2024-01-12' }, + { id: 5, description: 'Online Shopping', amount: 89, category: 'Shopping', date: '2024-01-11' } + ]); + + setLoading(false); + }, 1000); + }, []); + + const totalBudget = budgets.reduce((sum, budget) => sum + budget.financial_goal, 0); + const totalSpent = budgets.reduce((sum, budget) => sum + budget.spent, 0); + const totalRemaining = totalBudget - totalSpent; + + // Chart data + const categoryData = [ + { name: 'Food', value: 400, color: '#8884d8' }, + { name: 'Transportation', value: 300, color: '#82ca9d' }, + { name: 'Entertainment', value: 200, color: '#ffc658' }, + { name: 'Shopping', value: 150, color: '#ff7300' }, + { name: 'Utilities', value: 100, color: '#00ff00' } + ]; + + const monthlyData = [ + { month: 'Jan', spent: 1850, budget: 3000 }, + { month: 'Feb', spent: 2100, budget: 3000 }, + { month: 'Mar', spent: 1950, budget: 3000 }, + { month: 'Apr', spent: 2200, budget: 3000 }, + { month: 'May', spent: 1800, budget: 3000 }, + { month: 'Jun', spent: 2400, budget: 3000 } + ]; + + if (loading) { + return ( +
+
+

Loading dashboard...

+
+ ); + } + + return ( +
+
+

Dashboard

+

Welcome back! Here's your financial overview.

+
+ + {/* Summary Cards */} +
+
+
+ +
+
+

Total Budget

+

${totalBudget.toLocaleString()}

+
+
+ +
+
+ +
+
+

Total Spent

+

${totalSpent.toLocaleString()}

+
+
+ +
+
+ +
+
+

Remaining

+

${totalRemaining.toLocaleString()}

+
+
+ +
+
+ +
+
+

Quick Add

+

New Transaction

+
+
+
+ + {/* Charts Section */} +
+
+

Spending by Category

+ + + `${name} ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {categoryData.map((entry, index) => ( + + ))} + + + + +
+ +
+

Monthly Spending Trend

+ + + + + + + + + + +
+
+ + {/* Budgets Overview */} +
+
+

Your Budgets

+ +
+ +
+ {budgets.map((budget) => ( +
+
+

{budget.name}

+
+ + + +
+
+ +
+
+
+
+
+ ${budget.spent.toLocaleString()} / ${budget.financial_goal.toLocaleString()} + ${budget.remaining.toLocaleString()} left +
+
+
+ ))} +
+
+ + {/* Recent Transactions */} +
+
+

Recent Transactions

+ +
+ +
+ {recentTransactions.map((transaction) => ( +
+
+

{transaction.description}

+

{transaction.category} • {transaction.date}

+
+
+ -${transaction.amount} +
+
+ ))} +
+
+
+ ); +}; + +export default Dashboard; diff --git a/client/src/components/Layout.js b/client/src/components/Layout.js new file mode 100644 index 0000000..120d520 --- /dev/null +++ b/client/src/components/Layout.js @@ -0,0 +1,163 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { + Home, + Wallet, + List, + Tag, + Settings, + Menu, + X, + TrendingUp, + DollarSign, + LogOut, + User +} from 'lucide-react'; +import ThemeToggle from './ThemeToggle'; +import { useAuth } from '../contexts/AuthContext'; + +const Layout = ({ children }) => { + const [sidebarOpen, setSidebarOpen] = useState(false); + const [showUserMenu, setShowUserMenu] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); + const { user, logout } = useAuth(); + const userMenuRef = useRef(null); + + // Close user menu when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (userMenuRef.current && !userMenuRef.current.contains(event.target)) { + setShowUserMenu(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const navigation = [ + { name: 'Dashboard', href: '/', icon: Home }, + { name: 'Budgets', href: '/budgets', icon: Wallet }, + { name: 'Transactions', href: '/transactions', icon: List }, + { name: 'Categories', href: '/categories', icon: Tag }, + { name: 'Analytics', href: '/analytics', icon: TrendingUp }, + ]; + + const isActive = (path) => { + if (path === '/') { + return location.pathname === '/'; + } + return location.pathname.startsWith(path); + }; + + const handleLogout = async () => { + await logout(); + navigate('/login'); + }; + + return ( +
+ {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
+
+
+ + SpendWise +
+ +
+ + + +
+ +
+
+ + {/* Main content */} +
+ {/* Top bar */} +
+ + +
+
+
setShowUserMenu(!showUserMenu)} + > + {user?.name?.charAt(0) || 'U'} +
+ {showUserMenu && ( +
+
+ {user?.name || 'User'} + {user?.email || 'user@example.com'} +
+
+ + +
+ +
+
+ )} +
+
+
+ + {/* Page content */} +
+ {children} +
+
+
+ ); +}; + +export default Layout; diff --git a/client/src/components/Transactions.js b/client/src/components/Transactions.js new file mode 100644 index 0000000..0025560 --- /dev/null +++ b/client/src/components/Transactions.js @@ -0,0 +1,387 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { PlusCircle, Edit, Trash2, Search, Filter, Calendar } from 'lucide-react'; + +const Transactions = () => { + const [transactions, setTransactions] = useState([]); + const [budgets, setBudgets] = useState([]); + const [categories, setCategories] = useState([]); + const [showForm, setShowForm] = useState(false); + const [editingTransaction, setEditingTransaction] = useState(null); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedBudget, setSelectedBudget] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(''); + + const { register, handleSubmit, reset, formState: { errors } } = useForm(); + + useEffect(() => { + // Mock data - will be replaced with API calls + setTimeout(() => { + setBudgets([ + { id: 1, name: 'Monthly Budget' }, + { id: 2, name: 'Vacation Fund' }, + { id: 3, name: 'Emergency Fund' } + ]); + + setCategories([ + { id: 1, name: 'Food' }, + { id: 2, name: 'Transportation' }, + { id: 3, name: 'Entertainment' }, + { id: 4, name: 'Shopping' }, + { id: 5, name: 'Utilities' }, + { id: 6, name: 'Healthcare' } + ]); + + setTransactions([ + { + id: 1, + description: 'Grocery Shopping', + amount: 150.00, + date: '2024-01-15', + budget_id: 1, + category_id: 1, + budget_name: 'Monthly Budget', + category_name: 'Food' + }, + { + id: 2, + description: 'Gas Station', + amount: 45.00, + date: '2024-01-14', + budget_id: 1, + category_id: 2, + budget_name: 'Monthly Budget', + category_name: 'Transportation' + }, + { + id: 3, + description: 'Coffee Shop', + amount: 12.50, + date: '2024-01-13', + budget_id: 1, + category_id: 1, + budget_name: 'Monthly Budget', + category_name: 'Food' + }, + { + id: 4, + description: 'Movie Tickets', + amount: 25.00, + date: '2024-01-12', + budget_id: 1, + category_id: 3, + budget_name: 'Monthly Budget', + category_name: 'Entertainment' + }, + { + id: 5, + description: 'Online Shopping', + amount: 89.99, + date: '2024-01-11', + budget_id: 1, + category_id: 4, + budget_name: 'Monthly Budget', + category_name: 'Shopping' + } + ]); + + setLoading(false); + }, 1000); + }, []); + + const onSubmit = (data) => { + const transactionData = { + ...data, + amount: parseFloat(data.amount), + budget_id: parseInt(data.budget_id), + category_id: parseInt(data.category_id) + }; + + if (editingTransaction) { + // Update existing transaction + setTransactions(transactions.map(transaction => + transaction.id === editingTransaction.id + ? { + ...transaction, + ...transactionData, + budget_name: budgets.find(b => b.id === parseInt(data.budget_id))?.name || '', + category_name: categories.find(c => c.id === parseInt(data.category_id))?.name || '' + } + : transaction + )); + } else { + // Add new transaction + const newTransaction = { + ...transactionData, + id: Date.now(), + budget_name: budgets.find(b => b.id === parseInt(data.budget_id))?.name || '', + category_name: categories.find(c => c.id === parseInt(data.category_id))?.name || '' + }; + setTransactions([newTransaction, ...transactions]); + } + + reset(); + setShowForm(false); + setEditingTransaction(null); + }; + + const handleEdit = (transaction) => { + setEditingTransaction(transaction); + reset({ + description: transaction.description, + amount: transaction.amount, + date: transaction.date, + budget_id: transaction.budget_id, + category_id: transaction.category_id + }); + setShowForm(true); + }; + + const handleDelete = (transactionId) => { + if (window.confirm('Are you sure you want to delete this transaction?')) { + setTransactions(transactions.filter(transaction => transaction.id !== transactionId)); + } + }; + + const handleCancel = () => { + setShowForm(false); + setEditingTransaction(null); + reset(); + }; + + // Filter transactions + const filteredTransactions = transactions.filter(transaction => { + const matchesSearch = transaction.description.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesBudget = !selectedBudget || transaction.budget_id === parseInt(selectedBudget); + const matchesCategory = !selectedCategory || transaction.category_id === parseInt(selectedCategory); + + return matchesSearch && matchesBudget && matchesCategory; + }); + + const totalAmount = filteredTransactions.reduce((sum, transaction) => sum + transaction.amount, 0); + + if (loading) { + return ( +
+
+

Loading transactions...

+
+ ); + } + + return ( +
+
+
+

Transactions

+

Track and manage your spending

+
+ +
+ + {/* Transaction Form */} + {showForm && ( +
+
+

{editingTransaction ? 'Edit Transaction' : 'Add New Transaction'}

+
+ +
+
+
+ + + {errors.description && {errors.description.message}} +
+ +
+ + + {errors.amount && {errors.amount.message}} +
+
+ +
+
+ + + {errors.date && {errors.date.message}} +
+ +
+ + + {errors.budget_id && {errors.budget_id.message}} +
+ +
+ + + {errors.category_id && {errors.category_id.message}} +
+
+ +
+ + +
+
+
+ )} + + {/* Filters */} +
+
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ + +
+ +
+ +
+
+ +
+ {filteredTransactions.length} transactions + Total: ${totalAmount.toFixed(2)} +
+
+ + {/* Transactions List */} +
+ {filteredTransactions.length === 0 ? ( +
+ +

No transactions found

+

Add your first transaction to start tracking your spending

+ +
+ ) : ( + filteredTransactions.map((transaction) => ( +
+
+
+

{transaction.description}

+
+ {transaction.category_name} + {transaction.budget_name} + {transaction.date} +
+
+
+ -${transaction.amount.toFixed(2)} +
+
+ +
+ + +
+
+ )) + )} +
+
+ ); +}; + +export default Transactions; diff --git a/client/src/components/auth/ForgotPassword.js b/client/src/components/auth/ForgotPassword.js new file mode 100644 index 0000000..70dc2be --- /dev/null +++ b/client/src/components/auth/ForgotPassword.js @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import { Mail, ArrowLeft, CheckCircle, AlertCircle } from 'lucide-react'; + +const ForgotPassword = () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [error, setError] = useState(''); + + const { register, handleSubmit, formState: { errors } } = useForm(); + + const onSubmit = async (data) => { + setIsSubmitting(true); + setError(''); + + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Mock successful password reset request + setIsSubmitted(true); + } catch (err) { + setError('Failed to send reset email. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + if (isSubmitted) { + return ( +
+
+
+
+
💰
+ SpendWise +
+

Check Your Email

+

We've sent password reset instructions to your email address

+
+ +
+ +

Reset Email Sent

+

+ Please check your email inbox and follow the instructions to reset your password. + The link will expire in 1 hour. +

+
+ +
+

+ Didn't receive the email? + +

+

+ + + Back to Sign In + +

+
+
+
+ ); + } + + return ( +
+
+
+
+
💰
+ SpendWise +
+

Forgot Password?

+

No worries! Enter your email and we'll send you reset instructions

+
+ +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+ + +
+ {errors.email && ( + + + {errors.email.message} + + )} +
+ + +
+ +
+

+ Remember your password? + + Sign in here + +

+

+ Don't have an account? + + Create one here + +

+
+ +
+

Need Help?

+

+ If you're having trouble accessing your account, please contact our support team. + We're here to help you get back on track with your financial goals. +

+
+
+
+ ); +}; + +export default ForgotPassword; diff --git a/client/src/components/auth/Login.js b/client/src/components/auth/Login.js new file mode 100644 index 0000000..83af8ed --- /dev/null +++ b/client/src/components/auth/Login.js @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useNavigate, Link } from 'react-router-dom'; +import { Eye, EyeOff, Mail, Lock, AlertCircle } from 'lucide-react'; +import { useAuth } from '../../contexts/AuthContext'; + +const Login = () => { + const [showPassword, setShowPassword] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const navigate = useNavigate(); + const { login, error, clearError, isAuthenticated } = useAuth(); + + const { register, handleSubmit, formState: { errors } } = useForm({ + defaultValues: { + email: '', + password: '' + } + }); + + useEffect(() => { + if (isAuthenticated) navigate('/'); + }, [isAuthenticated, navigate]); + + useEffect(() => { + clearError(); + }, [clearError]); + + const onSubmit = async (data) => { + setIsSubmitting(true); + const result = await login(data); + if (result.success) navigate('/'); + setIsSubmitting(false); + }; + + return ( +
+
+
+
+
💰
+ SpendWise +
+

Welcome Back

+
+ +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+ + +
+ {errors.email && ( + + + {errors.email.message} + + )} +
+ +
+ +
+ + + +
+ {errors.password && ( + + + {errors.password.message} + + )} +
+ + +
+ +
+

+ Don't have an account?{' '} + Create one +

+
+
+
+ ); +}; + +export default Login; diff --git a/client/src/components/auth/ProtectedRoute.js b/client/src/components/auth/ProtectedRoute.js new file mode 100644 index 0000000..5618248 --- /dev/null +++ b/client/src/components/auth/ProtectedRoute.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../contexts/AuthContext'; + +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth(); + const location = useLocation(); + + if (loading) { + return ( +
+
+

Loading...

+
+ ); + } + + if (!isAuthenticated) { + // Redirect to login page with return url + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/client/src/components/auth/Signup.js b/client/src/components/auth/Signup.js new file mode 100644 index 0000000..1f80622 --- /dev/null +++ b/client/src/components/auth/Signup.js @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useNavigate, Link } from 'react-router-dom'; +import { Mail, Lock, User, AlertCircle, Eye, EyeOff } from 'lucide-react'; +import { useAuth } from '../../contexts/AuthContext'; + +const Signup = () => { + const [showPassword, setShowPassword] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const navigate = useNavigate(); + const { register: registerUser, error, clearError, isAuthenticated } = useAuth(); + + const { register, handleSubmit, formState: { errors } } = useForm({ + defaultValues: { + name: '', + email: '', + password: '' + } + }); + + useEffect(() => { + if (isAuthenticated) navigate('/'); + }, [isAuthenticated, navigate]); + + useEffect(() => { + clearError(); + }, [clearError]); + + const onSubmit = async (data) => { + setIsSubmitting(true); + const result = await registerUser({ + name: data.name, + email: data.email, + password: data.password + }); + if (result.success) navigate('/'); + setIsSubmitting(false); + }; + + return ( +
+
+
+
+
💰
+ SpendWise +
+

Create Your Account

+
+ +
+ {error && ( +
+ + {error} +
+ )} + +
+ +
+ + +
+ {errors.name && ( + + + {errors.name.message} + + )} +
+ +
+ +
+ + +
+ {errors.email && ( + + + {errors.email.message} + + )} +
+ +
+ +
+ + + +
+ {errors.password && ( + + + {errors.password.message} + + )} +
+ + +
+ +
+

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

+
+
+
+ ); +}; + +export default Signup; diff --git a/client/src/contexts/AuthContext.js b/client/src/contexts/AuthContext.js new file mode 100644 index 0000000..ea9b952 --- /dev/null +++ b/client/src/contexts/AuthContext.js @@ -0,0 +1,179 @@ +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; + +const AuthContext = createContext(); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const checkAuthStatus = useCallback(async () => { + try { + const savedUser = localStorage.getItem('user'); + const savedToken = localStorage.getItem('authToken'); + + if (savedUser && savedToken) { + // Verify token is still valid + try { + // In a real app, you'd verify the token with the server + const userData = JSON.parse(savedUser); + setUser(userData); + } catch (err) { + // Token is invalid, clear storage + clearAuthData(); + } + } + } catch (err) { + console.error('Auth check failed:', err); + clearAuthData(); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + // Check for existing session on app load + checkAuthStatus(); + }, [checkAuthStatus]); + + const login = async (credentials) => { + setLoading(true); + setError(null); + + try { + // For now, we'll use mock authentication + // In production, this would be: const response = await authAPI.login(credentials); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Mock successful login + const userData = { + id: 1, + email: credentials.email, + name: 'John Doe', + role: 'user', + created_at: new Date().toISOString() + }; + + const token = 'mock-jwt-token-' + Date.now(); + + // Store auth data + localStorage.setItem('user', JSON.stringify(userData)); + localStorage.setItem('authToken', token); + + setUser(userData); + return { success: true, user: userData }; + + } catch (err) { + const errorMessage = err.response?.data?.message || 'Login failed. Please try again.'; + setError(errorMessage); + return { success: false, error: errorMessage }; + } finally { + setLoading(false); + } + }; + + const register = async (userData) => { + setLoading(true); + setError(null); + + try { + // For now, we'll use mock registration + // In production, this would be: const response = await authAPI.register(userData); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Mock successful registration + const newUser = { + id: Date.now(), + email: userData.email, + name: userData.name, + role: 'user', + created_at: new Date().toISOString() + }; + + const token = 'mock-jwt-token-' + Date.now(); + + // Store auth data + localStorage.setItem('user', JSON.stringify(newUser)); + localStorage.setItem('authToken', token); + + setUser(newUser); + return { success: true, user: newUser }; + + } catch (err) { + const errorMessage = err.response?.data?.message || 'Registration failed. Please try again.'; + setError(errorMessage); + return { success: false, error: errorMessage }; + } finally { + setLoading(false); + } + }; + + const logout = async () => { + setLoading(true); + + try { + // In production, you might want to call the logout API + // await authAPI.logout(); + + clearAuthData(); + setUser(null); + return { success: true }; + + } catch (err) { + console.error('Logout error:', err); + // Even if API call fails, clear local data + clearAuthData(); + setUser(null); + return { success: true }; + } finally { + setLoading(false); + } + }; + + const clearAuthData = () => { + localStorage.removeItem('user'); + localStorage.removeItem('authToken'); + setUser(null); + setError(null); + }; + + const updateUser = (updatedUser) => { + const newUserData = { ...user, ...updatedUser }; + localStorage.setItem('user', JSON.stringify(newUserData)); + setUser(newUserData); + }; + + const clearError = () => { + setError(null); + }; + + const value = { + user, + loading, + error, + login, + register, + logout, + updateUser, + clearError, + isAuthenticated: !!user + }; + + return ( + + {children} + + ); +}; diff --git a/client/src/index.css b/client/src/index.css index 50fabf6..6dced73 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -3,20 +3,60 @@ @tailwind utilities; .theme-transition * { - transition: background-color 0.4s ease, color 0.4s ease; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; } +/* Light Theme */ [data-theme="light"] { - --background: #fdf6e3; - --text-color: #000; - --toggle-color: #282c34; + --background: #ffffff; + --sidebar-bg: #f8fafc; + --header-bg: #ffffff; + --card-bg: #ffffff; + --input-bg: #ffffff; + --text-color: #1a202c; + --text-muted: #718096; + --border-color: #e2e8f0; + --hover-bg: #f7fafc; + --active-bg: #edf2f7; + --primary-color: #3182ce; + --primary-hover: #2c5aa0; + --primary-bg: #ebf8ff; + --secondary-color: #f7fafc; + --success-color: #38a169; + --success-bg: #f0fff4; + --danger-color: #e53e3e; + --danger-bg: #fed7d7; + --warning-color: #d69e2e; + --warning-bg: #fefcbf; + --progress-bg: #e2e8f0; + --toggle-color: #4a5568; transition: all 1s ease; } +/* Dark Theme */ [data-theme="dark"] { - --background: #282c34; - --text-color: #fff; - --toggle-color: #fdf6e3; + --background: #1a202c; + --sidebar-bg: #2d3748; + --header-bg: #2d3748; + --card-bg: #2d3748; + --input-bg: #4a5568; + --text-color: #f7fafc; + --text-muted: #a0aec0; + --border-color: #4a5568; + --hover-bg: #4a5568; + --active-bg: #2d3748; + --primary-color: #63b3ed; + --primary-hover: #4299e1; + --primary-bg: #2b6cb0; + --secondary-color: #4a5568; + --success-color: #68d391; + --success-bg: #22543d; + --danger-color: #fc8181; + --danger-bg: #742a2a; + --warning-color: #f6e05e; + --warning-bg: #744210; + --progress-bg: #4a5568; + --toggle-color: #f7fafc; transition: all 1s ease; } @@ -27,6 +67,8 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + background-color: var(--background); + color: var(--text-color); } code { @@ -34,17 +76,735 @@ code { monospace; } +/* Theme Toggle */ .toggleBtn { background-color: transparent; border: none; + padding: 0.5rem; + border-radius: 0.5rem; + transition: background-color 0.2s ease; } .toggleBtn:hover { cursor: pointer; + background-color: var(--hover-bg); } .moon, .sun { color: var(--toggle-color); fill: var(--toggle-color); transition: color 0.3s ease, fill 0.3s ease; +} + +/* Authentication Styles */ +.auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--background) 0%, var(--sidebar-bg) 100%); + padding: 2rem; +} + +.auth-card { + width: 100%; + max-width: 450px; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 1rem; + padding: 2.5rem; + box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.auth-card.signup-card { + max-width: 500px; +} + +.auth-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-color), var(--success-color)); +} + +.auth-header { + text-align: center; + margin-bottom: 2rem; +} + +.auth-header .logo { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.logo-icon { + font-size: 2rem; +} + +.auth-header h2 { + font-size: 1.5rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.auth-header p { + color: var(--text-muted); + margin: 0; +} + +.auth-form { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.input-group { + position: relative; + display: flex; + align-items: center; +} + +.input-group svg { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + z-index: 1; +} + +.input-group input { + width: 100%; + padding: 1rem 3rem 1rem 3rem; /* extra right padding for toggle */ + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--input-bg); + color: var(--text-color); + font-size: 1rem; + transition: all 0.2s ease; + display: block; + visibility: visible; + opacity: 1; + box-sizing: border-box; + height: 3.5rem; +} + +.input-group input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.password-toggle { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 0.5rem; + border-radius: 0.25rem; + transition: color 0.2s ease; +} + +.password-toggle:hover { + color: var(--text-color); +} + +.form-options { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.875rem; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + color: var(--text-color); +} + +.checkbox-label input[type="checkbox"] { + width: auto; + margin: 0; +} + +.forgot-password { + color: var(--primary-color); + text-decoration: none; + transition: color 0.2s ease; +} + +.forgot-password:hover { + color: var(--primary-hover); +} + +.error-message { + background-color: var(--danger-bg); + color: var(--danger-color); + padding: 0.75rem; + border-radius: 0.5rem; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.auth-footer { + text-align: center; + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-color); +} + +.auth-footer p { + color: var(--text-muted); + margin: 0; +} + +.auth-link { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease; +} + +.auth-link:hover { + color: var(--primary-hover); +} + +/* Enhanced Authentication Styles */ +.auth-divider { + position: relative; + text-align: center; + margin: 1.5rem 0; +} + +.auth-divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background-color: var(--border-color); +} + +.auth-divider span { + background-color: var(--card-bg); + padding: 0 1rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.auth-features { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.auth-features h4 { + font-size: 1rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 1rem 0; +} + +.auth-features ul { + list-style: none; + padding: 0; + margin: 0; +} + +.auth-features li { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.auth-features li svg { + color: var(--success-color); + flex-shrink: 0; +} + +.password-strength { + margin-top: 0.5rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.strength-bar { + flex: 1; + height: 4px; + background-color: var(--progress-bg); + border-radius: 2px; + overflow: hidden; +} + +.strength-fill { + height: 100%; + transition: width 0.3s ease, background-color 0.3s ease; +} + +.strength-text { + font-size: 0.75rem; + font-weight: 500; + min-width: 60px; +} + +.terms-label { + font-size: 0.875rem; + line-height: 1.5; +} + +.terms-link { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; +} + +.terms-link:hover { + text-decoration: underline; +} + +.success-message { + text-align: center; + padding: 2rem 0; +} + +.success-message svg { + color: var(--success-color); + margin-bottom: 1rem; +} + +.success-message h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.success-message p { + color: var(--text-muted); + margin: 0; + line-height: 1.5; +} + +.help-text { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid var(--border-color); +} + +.help-text h4 { + font-size: 1rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.help-text p { + font-size: 0.875rem; + color: var(--text-muted); + margin: 0; + line-height: 1.5; +} + +/* Enhanced User Menu */ +.user-menu { + position: relative; +} + +.user-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1rem; + min-width: 220px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); + z-index: 1000; +} + +.user-info { + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color); + margin-bottom: 0.75rem; +} + +.user-name { + font-weight: 600; + color: var(--text-color); + display: block; + font-size: 0.875rem; +} + +.user-email { + font-size: 0.75rem; + color: var(--text-muted); + display: block; + margin-top: 0.25rem; +} + +.user-menu-items { + display: flex; + flex-direction: column; +} + +.user-menu-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem; + background: none; + border: none; + color: var(--text-color); + font-size: 0.875rem; + cursor: pointer; + border-radius: 0.375rem; + transition: background-color 0.2s ease; + width: 100%; + text-align: left; +} + +.user-menu-item:hover { + background-color: var(--hover-bg); +} + +.user-menu-item.logout-btn { + color: var(--danger-color); +} + +.user-menu-item.logout-btn:hover { + background-color: var(--danger-bg); +} + +.user-menu-divider { + border: none; + height: 1px; + background-color: var(--border-color); + margin: 0.5rem 0; +} + +/* Loading Spinner Small */ +.loading-spinner.small { + width: 16px; + height: 16px; + border-width: 2px; +} + +/* Dashboard Specific Styles */ +.dashboard-header { + margin-bottom: 2rem; +} + +.dashboard-header h1 { + font-size: 2rem; + font-weight: 700; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.dashboard-header p { + color: var(--text-muted); + margin: 0; +} + +.budgets-section, +.recent-transactions { + margin-bottom: 2rem; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; +} + +.section-header h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + margin: 0; +} + +/* Budget Form Styles */ +.budget-form-container, +.transaction-form-container, +.category-form-container { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 2rem; + margin-bottom: 2rem; +} + +.budget-form, +.transaction-form, +.category-form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Categories Specific Styles */ +.categories-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.summary-icon { + width: 48px; + height: 48px; + border-radius: 0.75rem; + background-color: var(--primary-bg); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary-color); +} + +.summary-content h3 { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-color); + margin: 0 0 0.25rem 0; +} + +.summary-content p { + font-size: 0.875rem; + color: var(--text-muted); + margin: 0; +} + +.category-icon { + width: 40px; + height: 40px; + border-radius: 0.5rem; + background-color: var(--primary-bg); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary-color); +} + +.category-stats { + margin-top: 0.5rem; +} + +.transaction-count { + font-size: 0.875rem; + color: var(--text-muted); +} + +.category-warning { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: var(--warning-color); + margin-top: 0.5rem; + padding: 0.5rem; + background-color: var(--warning-bg); + border-radius: 0.25rem; +} + +.usage-tips { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 0.75rem; + padding: 1.5rem; + margin-top: 2rem; +} + +.usage-tips h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 1rem 0; +} + +.usage-tips ul { + margin: 0; + padding-left: 1.5rem; +} + +.usage-tips li { + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +/* Analytics Specific Styles */ +.analytics-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.time-range-select { + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--input-bg); + color: var(--text-color); + font-size: 0.875rem; +} + +.budget-performance-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.budget-performance-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background-color: var(--hover-bg); + border-radius: 0.5rem; +} + +.budget-info h4 { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-color); + margin: 0 0 0.25rem 0; +} + +.budget-info span { + font-size: 0.75rem; + color: var(--text-muted); +} + +.budget-progress { + display: flex; + align-items: center; + gap: 1rem; + min-width: 200px; +} + +.insights-section { + margin-top: 2rem; +} + +.insights-section h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 1.5rem 0; +} + +.insights-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.insight-card { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1.5rem; + border-radius: 0.75rem; + border: 1px solid var(--border-color); +} + +.insight-card.positive { + background-color: var(--success-bg); + border-color: var(--success-color); +} + +.insight-card.warning { + background-color: var(--warning-bg); + border-color: var(--warning-color); +} + +.insight-card svg { + color: var(--text-color); + margin-top: 0.25rem; +} + +.insight-content h4 { + font-size: 1rem; + font-weight: 600; + color: var(--text-color); + margin: 0 0 0.5rem 0; +} + +.insight-content p { + font-size: 0.875rem; + color: var(--text-muted); + margin: 0; + line-height: 1.5; +} + +/* Responsive Design */ +@media (max-width: 640px) { + .auth-container { + padding: 1rem; + } + + .auth-card { + padding: 1.5rem; + } + + .page-content { + padding: 1rem; + } + + .budget-stats { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .transaction-main { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .transaction-actions { + margin-left: 0; + margin-top: 0.5rem; + } + + .insights-grid { + grid-template-columns: 1fr; + } + + .budget-performance-item { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .budget-progress { + width: 100%; + min-width: auto; + } } \ No newline at end of file diff --git a/client/src/services/api.js b/client/src/services/api.js index 45e12be..6a35e5e 100644 --- a/client/src/services/api.js +++ b/client/src/services/api.js @@ -1,11 +1,83 @@ import axios from 'axios'; +// Create axios instance with base configuration const api = axios.create({ - baseURL: process.env.REACT_APP_API_URL, + baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api/v1', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - withCredentials: true }); +// Add request interceptor to include auth token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('authToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Add response interceptor for error handling +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // Handle unauthorized access + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +// Auth API +export const authAPI = { + login: (credentials) => api.post('/auth/login', credentials), + register: (userData) => api.post('/auth/register', userData), + logout: () => api.post('/auth/logout'), + refreshToken: () => api.post('/auth/refresh'), +}; + +// Budgets API +export const budgetsAPI = { + getAll: () => api.get('/budgets'), + getById: (id) => api.get(`/budgets/${id}`), + create: (budgetData) => api.post('/budgets', budgetData), + update: (id, budgetData) => api.put(`/budgets/${id}`, budgetData), + delete: (id) => api.delete(`/budgets/${id}`), +}; + +// Categories API +export const categoriesAPI = { + getAll: (params = {}) => api.get('/categories', { params }), + getById: (id) => api.get(`/categories/${id}`), + create: (categoryData) => api.post('/categories', categoryData), + update: (id, categoryData) => api.put(`/categories/${id}`, categoryData), + delete: (id) => api.delete(`/categories/${id}`), +}; + +// Transactions API +export const transactionsAPI = { + getAll: (budgetId, params = {}) => api.get(`/budgets/${budgetId}/transactions`, { params }), + getById: (budgetId, transactionId) => api.get(`/budgets/${budgetId}/transactions/${transactionId}`), + create: (budgetId, transactionData) => api.post(`/budgets/${budgetId}/transactions`, transactionData), + update: (budgetId, transactionId, transactionData) => + api.put(`/budgets/${budgetId}/transactions/${transactionId}`, transactionData), + delete: (budgetId, transactionId) => api.delete(`/budgets/${budgetId}/transactions/${transactionId}`), +}; + +// Analytics API +export const analyticsAPI = { + getSpendingTrend: (params = {}) => api.get('/analytics/spending-trend', { params }), + getCategoryBreakdown: (params = {}) => api.get('/analytics/category-breakdown', { params }), + getBudgetPerformance: (params = {}) => api.get('/analytics/budget-performance', { params }), + getMonthlyComparison: (params = {}) => api.get('/analytics/monthly-comparison', { params }), +}; + export default api;