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
+
+
+ setTimeRange(e.target.value)}
+ className="time-range-select"
+ >
+ Last 3 Months
+ Last 6 Months
+ Last Year
+ All Time
+
+
+
+ Export Report
+
+
+
+
+ {/* 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
+
+
+
+
+
+
+
+ );
+};
+
+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
+
+
+
+
+
+
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 (
+
+ );
+ }
+
+ return (
+
+
+
+
Budgets
+
Manage your financial goals and track your spending
+
+
setShowForm(true)}
+ >
+
+ New Budget
+
+
+
+ {/* Budget Form */}
+ {showForm && (
+
+
+
{editingBudget ? 'Edit Budget' : 'Create New Budget'}
+
+
+
+
+ )}
+
+ {/* Budgets Grid */}
+
+ {budgets.length === 0 ? (
+
+
+
No budgets yet
+
Create your first budget to start tracking your spending
+
setShowForm(true)}
+ >
+
+ Create Budget
+
+
+ ) : (
+ budgets.map((budget) => (
+
+
+
+
{budget.name}
+ Created {budget.created_at}
+
+
+ handleEdit(budget)}
+ title="Edit Budget"
+ >
+
+
+ handleDelete(budget.id)}
+ title="Delete Budget"
+ >
+
+
+
+
+
+
+
+ Goal
+ ${budget.financial_goal.toLocaleString()}
+
+
+ Spent
+ ${budget.spent.toLocaleString()}
+
+
+ Remaining
+ ${budget.remaining.toLocaleString()}
+
+
+
+
+
+
+ {((budget.spent / budget.financial_goal) * 100).toFixed(1)}% used
+
+
+
+
+
+
+ View Details
+
+
+
+ ))
+ )}
+
+
+ );
+};
+
+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
+
+
setShowForm(true)}
+ >
+
+ New Category
+
+
+
+ {/* Category Form */}
+ {showForm && (
+
+
+
{editingCategory ? 'Edit Category' : 'Create New Category'}
+
+
+
+
+ )}
+
+ {/* 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
+
setShowForm(true)}
+ >
+
+ Create Category
+
+
+ ) : (
+ categories.map((category) => (
+
+
+
+
+
+
+ handleEdit(category)}
+ title="Edit Category"
+ >
+
+
+ handleDelete(category.id)}
+ title="Delete Category"
+ disabled={category.transaction_count > 0}
+ >
+
+
+
+
+
+
+
{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
+
+
+ New Budget
+
+
+
+
+ {budgets.map((budget) => (
+
+
+
{budget.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${budget.spent.toLocaleString()} / ${budget.financial_goal.toLocaleString()}
+ ${budget.remaining.toLocaleString()} left
+
+
+
+ ))}
+
+
+
+ {/* Recent Transactions */}
+
+
+
Recent Transactions
+ View All
+
+
+
+ {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
+
+
setSidebarOpen(false)}
+ >
+
+
+
+
+
+ {navigation.map((item) => {
+ const Icon = item.icon;
+ return (
+ setSidebarOpen(false)}
+ >
+
+ {item.name}
+
+ );
+ })}
+
+
+
+
+
+
+
+ {/* Main content */}
+
+ {/* Top bar */}
+
+ setSidebarOpen(true)}
+ >
+
+
+
+
+
+
setShowUserMenu(!showUserMenu)}
+ >
+ {user?.name?.charAt(0) || 'U'}
+
+ {showUserMenu && (
+
+
+ {user?.name || 'User'}
+ {user?.email || 'user@example.com'}
+
+
+
+
+ Profile Settings
+
+
+
+ Preferences
+
+
+
+
+ Sign Out
+
+
+
+ )}
+
+
+
+
+ {/* 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
+
+
setShowForm(true)}
+ >
+
+ New Transaction
+
+
+
+ {/* Transaction Form */}
+ {showForm && (
+
+
+
{editingTransaction ? 'Edit Transaction' : 'Add New Transaction'}
+
+
+
+
+ )}
+
+ {/* Filters */}
+
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
+
+ setSelectedBudget(e.target.value)}
+ >
+ All Budgets
+ {budgets.map(budget => (
+
+ {budget.name}
+
+ ))}
+
+
+
+
+ setSelectedCategory(e.target.value)}
+ >
+ All Categories
+ {categories.map(category => (
+
+ {category.name}
+
+ ))}
+
+
+
+
+
+ {filteredTransactions.length} transactions
+ Total: ${totalAmount.toFixed(2)}
+
+
+
+ {/* Transactions List */}
+
+ {filteredTransactions.length === 0 ? (
+
+
+
No transactions found
+
Add your first transaction to start tracking your spending
+
setShowForm(true)}
+ >
+
+ Add Transaction
+
+
+ ) : (
+ filteredTransactions.map((transaction) => (
+
+
+
+
{transaction.description}
+
+ {transaction.category_name}
+ {transaction.budget_name}
+ {transaction.date}
+
+
+
+ -${transaction.amount.toFixed(2)}
+
+
+
+
+ handleEdit(transaction)}
+ title="Edit Transaction"
+ >
+
+
+ handleDelete(transaction.id)}
+ title="Delete Transaction"
+ >
+
+
+
+
+ ))
+ )}
+
+
+ );
+};
+
+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 (
+
+
+
+
+
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?
+ setIsSubmitted(false)}
+ >
+ Try again
+
+
+
+
+
+ Back to Sign In
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
Forgot Password?
+
No worries! Enter your email and we'll send you reset instructions
+
+
+
+
+
+
+ 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 (
+
+
+
+
+
+
+
+
+ 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 (
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
Create Your Account
+
+
+
+
+
+
+ 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;