From cb7899dd45217605993b69a1744adda5b2725a41 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 26 Aug 2025 14:42:45 +0300 Subject: [PATCH 1/9] feat: static sold fluctuation chart --- client/package-lock.json | 383 +++++++++++++++++- client/package.json | 1 + .../src/components/common/BackgroundImage.tsx | 2 +- client/src/components/dashboard/SoldChart.tsx | 98 +++++ client/src/pages/Dashboard.tsx | 14 +- client/src/ui/Breadcrumb.tsx | 24 ++ 6 files changed, 518 insertions(+), 4 deletions(-) create mode 100644 client/src/components/dashboard/SoldChart.tsx create mode 100644 client/src/ui/Breadcrumb.tsx diff --git a/client/package-lock.json b/client/package-lock.json index b60cdff..ca0f52d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,6 +18,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", + "recharts": "^3.1.2", "tailwindcss": "^4.1.11", "zustand": "^5.0.8" }, @@ -1238,6 +1239,32 @@ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "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/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1541,6 +1568,18 @@ "win32" ] }, + "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/@storybook/addon-a11y": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.1.2.tgz", @@ -2237,6 +2276,69 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "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/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2298,6 +2400,12 @@ "dev": true, "license": "MIT" }, + "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/@typescript-eslint/eslint-plugin": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", @@ -3241,6 +3349,15 @@ } } }, + "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/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3335,6 +3452,127 @@ "devOptional": true, "license": "MIT" }, + "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/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -3353,6 +3591,12 @@ } } }, + "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/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3528,6 +3772,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -3821,6 +4075,12 @@ "node": ">=0.10.0" } }, + "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/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -4317,6 +4577,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4354,6 +4624,15 @@ "node": ">=8" } }, + "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/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5586,9 +5865,31 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT" }, + "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.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5654,6 +5955,33 @@ "node": ">= 4" } }, + "node_modules/recharts": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.2.tgz", + "integrity": "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==", + "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/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -5681,6 +6009,27 @@ "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/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.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6223,7 +6572,6 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/tinybench": { @@ -6522,6 +6870,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/vite": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz", diff --git a/client/package.json b/client/package.json index 7378118..8eb38fb 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", + "recharts": "^3.1.2", "tailwindcss": "^4.1.11", "zustand": "^5.0.8" }, diff --git a/client/src/components/common/BackgroundImage.tsx b/client/src/components/common/BackgroundImage.tsx index fcf87fb..048ad73 100644 --- a/client/src/components/common/BackgroundImage.tsx +++ b/client/src/components/common/BackgroundImage.tsx @@ -2,7 +2,7 @@ import { assets } from "../../assets/images"; function BackgroundImage() { return ( -
+
Bg image
); diff --git a/client/src/components/dashboard/SoldChart.tsx b/client/src/components/dashboard/SoldChart.tsx new file mode 100644 index 0000000..f2e9f9a --- /dev/null +++ b/client/src/components/dashboard/SoldChart.tsx @@ -0,0 +1,98 @@ +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; + +type Tx = { + amount: number; + createdAt: string; +}; + +function getLastNDays(n = 10) { + const days: { key: string; label: string }[] = []; + const today = new Date(); + for (let i = n - 1; i >= 0; i--) { + const d = new Date(today); + d.setDate(today.getDate() - i); + const key = d.toISOString().slice(0, 10); + const label = d.toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + }); + days.push({ key, label }); + } + return days; +} + +const formatAr = (v: number) => `Ar ${Math.round(v).toLocaleString("fr-MG")}`; + +type Props = { + transactions: Tx[]; + title?: string; +}; + +export default function SoldChart({ + transactions, + title = "Sold variation over the last 10 days", +}: Props) { + const days = getLastNDays(10); + const sumByDay = new Map(); + for (const tx of transactions ?? []) { + const d = new Date(tx.createdAt); + const key = isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10); + if (!key) continue; + sumByDay.set(key, (sumByDay.get(key) ?? 0) + (tx.amount || 0)); + } + const data = days.map(({ key, label }) => ({ + name: label, + amount: sumByDay.get(key) ?? 0, + })); + + return ( +
+

{title}

+ +
+ + + + + + + + + + + + + [formatAr(value), "Income"]} + labelClassName="font-semibold" + /> + + + +
+
+ ); +} diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index f8120e7..302ded5 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -1,6 +1,18 @@ +import SoldChart from "../components/dashboard/SoldChart"; + function Dashboard() { return ( -
+
+
+ +
+
+
); } diff --git a/client/src/ui/Breadcrumb.tsx b/client/src/ui/Breadcrumb.tsx new file mode 100644 index 0000000..56a5847 --- /dev/null +++ b/client/src/ui/Breadcrumb.tsx @@ -0,0 +1,24 @@ +import { Link } from "react-router-dom"; + +function Breadcrumb({ path }: { path: string }) { + let splitted: string[] = []; + + if (path == "/") { + splitted.push("Dahsboard"); + } else { + splitted = path.split("/"); + splitted = splitted.slice(1); + } + + return ( +
+ {splitted.map((l) => ( + + {l[0].toUpperCase() + l.slice(1)} {">"} + + ))} +
+ ); +} + +export default Breadcrumb; From dbb06505bce5dbbcb168e9b4b1cdf7ee2226603d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 26 Aug 2025 14:46:46 +0300 Subject: [PATCH 2/9] refactor: single layout component for every type of graph (with title and children) --- client/src/components/dashboard/Layout.tsx | 21 ++++++ client/src/components/dashboard/SoldChart.tsx | 71 +++++++++---------- .../src/components/dashboard/SpendingPie.tsx | 5 ++ 3 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 client/src/components/dashboard/Layout.tsx create mode 100644 client/src/components/dashboard/SpendingPie.tsx diff --git a/client/src/components/dashboard/Layout.tsx b/client/src/components/dashboard/Layout.tsx new file mode 100644 index 0000000..90cbb92 --- /dev/null +++ b/client/src/components/dashboard/Layout.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; + +export default function Layout({ + title, + children, +}: { + title: string; + children: ReactNode; +}) { + return ( +
+ {" "} +

+ {title} +

{" "} +
+ {children} +
+
+ ); +} diff --git a/client/src/components/dashboard/SoldChart.tsx b/client/src/components/dashboard/SoldChart.tsx index f2e9f9a..1255408 100644 --- a/client/src/components/dashboard/SoldChart.tsx +++ b/client/src/components/dashboard/SoldChart.tsx @@ -7,6 +7,7 @@ import { CartesianGrid, ResponsiveContainer, } from "recharts"; +import Layout from "./Layout"; type Tx = { amount: number; @@ -54,45 +55,37 @@ export default function SoldChart({ })); return ( -
-

{title}

+ + + + + + + + + -
- - - - - - - - - - - - - [formatAr(value), "Income"]} - labelClassName="font-semibold" - /> - - - -
-
+ + + + [formatAr(value), "Income"]} + labelClassName="font-semibold" + /> + + + + ); } diff --git a/client/src/components/dashboard/SpendingPie.tsx b/client/src/components/dashboard/SpendingPie.tsx new file mode 100644 index 0000000..d170f8f --- /dev/null +++ b/client/src/components/dashboard/SpendingPie.tsx @@ -0,0 +1,5 @@ +export default function SpendingPie() { + return ( +
+ ) +} From 6e6bd964db731a22eb1a92764b1d92b9551c38cb Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 26 Aug 2025 15:03:54 +0300 Subject: [PATCH 3/9] feat: static spending pie with filter option --- .../src/components/dashboard/SpendingPie.tsx | 96 ++++++++++++++++++- client/src/pages/Dashboard.tsx | 2 + 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/client/src/components/dashboard/SpendingPie.tsx b/client/src/components/dashboard/SpendingPie.tsx index d170f8f..21c953b 100644 --- a/client/src/components/dashboard/SpendingPie.tsx +++ b/client/src/components/dashboard/SpendingPie.tsx @@ -1,5 +1,97 @@ +import { useState } from "react"; +import Layout from "./Layout"; +import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts"; + +const initialData = [ + { name: "Food", value: 45000 }, + { name: "Transport", value: 20000 }, + { name: "Shopping", value: 30000 }, + { name: "Bills", value: 25000 }, + { name: "Entertainment", value: 15000 }, +]; + +const COLORS = ["#FF8042", "#0088FE", "#00C49F", "#FFBB28", "#AF19FF"]; + +const formatAr = (v: number) => `Ar ${v.toLocaleString("fr-MG")}`; + export default function SpendingPie() { + const [activeCats, setActiveCats] = useState( + initialData.map((d) => d.name) + ); + + const toggleCategory = (name: string) => { + setActiveCats((prev) => + prev.includes(name) ? prev.filter((c) => c !== name) : [...prev, name] + ); + }; + + const filteredData = initialData.filter((d) => activeCats.includes(d.name)); + const total = filteredData.reduce((acc, cur) => acc + cur.value, 0); + return ( -
- ) +
+ {/* Chart */} + + + + + `${name} ${(percent * 100).toFixed(0)}%` + } + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {filteredData.map((_entry, index) => ( + + ))} + + formatAr(v)} /> + + + + + {/* Options */} +
+

Options

+ + {/* Total */} +
+ Total + {formatAr(total)} +
+ + {/* Filtrage */} +
+ {initialData.map((d, i) => ( + + ))} +
+
+
+ ); } diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 302ded5..bd89f0d 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -1,4 +1,5 @@ import SoldChart from "../components/dashboard/SoldChart"; +import SpendingPie from "../components/dashboard/SpendingPie"; function Dashboard() { return ( @@ -10,6 +11,7 @@ function Dashboard() { { amount: 12000, createdAt: "2025-08-22T08:03:00Z" }, ]} /> +
From a656d3e6b575593863d6e8d3b2f21a343e02b40a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 26 Aug 2025 15:19:13 +0300 Subject: [PATCH 4/9] feat: simple calendar UI with react-calendar --- client/package-lock.json | 96 ++++++++++++++++++- client/package.json | 1 + .../dashboard/CalendarComponent.tsx | 14 +++ client/src/components/dashboard/Layout.tsx | 16 +++- .../src/components/dashboard/SpendingPie.tsx | 2 +- client/src/pages/Dashboard.tsx | 4 +- 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 client/src/components/dashboard/CalendarComponent.tsx diff --git a/client/package-lock.json b/client/package-lock.json index ca0f52d..a0f59fe 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,6 +16,7 @@ "framer-motion": "^12.23.12", "lucide-react": "^0.541.0", "react": "^19.1.1", + "react-calendar": "^6.0.0", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", "recharts": "^3.1.2", @@ -2966,6 +2967,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-2.0.2.tgz", + "integrity": "sha512-Do66mSlSNifFFuo3l9gNKfRMSFi26CRuQMsDJuuKO/ekrDWuTTtE4ZQxoFCUOG+NgxnpSeBq/k5TY8ZseEzLpA==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4391,6 +4401,18 @@ "node": ">= 0.4" } }, + "node_modules/get-user-locale": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-3.0.0.tgz", + "integrity": "sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g==", + "license": "MIT", + "dependencies": { + "memoize": "^10.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4811,7 +4833,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5172,6 +5193,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/loupe": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", @@ -5267,6 +5300,21 @@ "node": ">= 0.4" } }, + "node_modules/memoize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", + "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/memoize?sponsor=1" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5312,6 +5360,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5817,6 +5877,31 @@ "node": ">=0.10.0" } }, + "node_modules/react-calendar": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-6.0.0.tgz", + "integrity": "sha512-6wqaki3Us0DNDjZDr0DYIzhSFprNoy4FdPT9Pjy5aD2hJJVjtJwmdMT9VmrTUo949nlk35BOxehThxX62RkuRQ==", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^2.0.2", + "clsx": "^2.0.0", + "get-user-locale": "^3.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-docgen": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.0.tgz", @@ -7110,6 +7195,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", diff --git a/client/package.json b/client/package.json index 8eb38fb..d28b38d 100644 --- a/client/package.json +++ b/client/package.json @@ -21,6 +21,7 @@ "framer-motion": "^12.23.12", "lucide-react": "^0.541.0", "react": "^19.1.1", + "react-calendar": "^6.0.0", "react-dom": "^19.1.1", "react-router-dom": "^7.8.2", "recharts": "^3.1.2", diff --git a/client/src/components/dashboard/CalendarComponent.tsx b/client/src/components/dashboard/CalendarComponent.tsx new file mode 100644 index 0000000..4d9f7f9 --- /dev/null +++ b/client/src/components/dashboard/CalendarComponent.tsx @@ -0,0 +1,14 @@ +import { Calendar } from "react-calendar"; +import Layout from "./Layout"; +export default function CalendarComponent() { + return ( + + + + ); +} diff --git a/client/src/components/dashboard/Layout.tsx b/client/src/components/dashboard/Layout.tsx index 90cbb92..4a01dd2 100644 --- a/client/src/components/dashboard/Layout.tsx +++ b/client/src/components/dashboard/Layout.tsx @@ -3,17 +3,23 @@ import type { ReactNode } from "react"; export default function Layout({ title, children, + graphClassName, }: { - title: string; + title?: string; children: ReactNode; + graphClassName?: string; }) { return (
{" "} -

- {title} -

{" "} -
+ {title && ( +

+ {title} +

+ )} +
{children}
diff --git a/client/src/components/dashboard/SpendingPie.tsx b/client/src/components/dashboard/SpendingPie.tsx index 21c953b..8c089eb 100644 --- a/client/src/components/dashboard/SpendingPie.tsx +++ b/client/src/components/dashboard/SpendingPie.tsx @@ -31,7 +31,7 @@ export default function SpendingPie() { return (
{/* Chart */} - + +
+
From 3a41a281dbcb8faffc7ba799d52398d79089655a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Aug 2025 12:15:12 +0300 Subject: [PATCH 5/9] feat: redesign the dashboard from scratch --- client/package-lock.json | 304 ++++++++++++++++++ client/package.json | 2 + client/src/App.tsx | 2 +- .../src/components/common/BackgroundImage.tsx | 2 +- .../src/components/common/Header/Header.tsx | 2 +- .../common/Header/components/DateDropdown.tsx | 4 +- .../common/Header/components/SearchInput.tsx | 2 +- client/src/components/common/Mascot.tsx | 2 +- .../dashboard/CalendarComponent.tsx | 14 - .../src/components/dashboard/DisplayCard.tsx | 106 ++++++ .../src/components/dashboard/IncomeGraph.tsx | 39 +++ client/src/components/dashboard/IncomePie.tsx | 160 +++++++++ client/src/components/dashboard/Layout.tsx | 14 +- .../components/dashboard/MonthlyBarchart.tsx | 134 ++++++++ client/src/components/dashboard/PieGraph.tsx | 61 ++++ client/src/components/dashboard/SoldChart.tsx | 91 ------ .../src/components/dashboard/SpendingPie.tsx | 97 ------ client/src/pages/Dashboard.tsx | 99 +++++- 18 files changed, 907 insertions(+), 228 deletions(-) delete mode 100644 client/src/components/dashboard/CalendarComponent.tsx create mode 100644 client/src/components/dashboard/DisplayCard.tsx create mode 100644 client/src/components/dashboard/IncomeGraph.tsx create mode 100644 client/src/components/dashboard/IncomePie.tsx create mode 100644 client/src/components/dashboard/MonthlyBarchart.tsx create mode 100644 client/src/components/dashboard/PieGraph.tsx delete mode 100644 client/src/components/dashboard/SoldChart.tsx delete mode 100644 client/src/components/dashboard/SpendingPie.tsx diff --git a/client/package-lock.json b/client/package-lock.json index a0f59fe..71ef937 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,8 @@ "name": "expense-tracker", "version": "0.0.0", "dependencies": { + "@nivo/core": "^0.99.0", + "@nivo/pie": "^0.99.0", "@radix-ui/react-icons": "^1.3.2", "@tailwindcss/vite": "^4.1.11", "@vercel/analytics": "^1.5.0", @@ -1175,6 +1177,189 @@ "dev": true, "license": "MIT" }, + "node_modules/@nivo/arcs": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.99.0.tgz", + "integrity": "sha512-UcvWLQPl+A3APk2Gm74N5xDfT+ATnVs2XkP73WxhYPWJk+dBzF00cndA5g/dptOwdFBvvo62VgcCsNiwUsjKTw==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/core": "9.4.5 || ^9.7.2 || ^10.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/colors": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.99.0.tgz", + "integrity": "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@types/d3-color": "^3.0.0", + "@types/d3-scale": "^4.0.8", + "@types/d3-scale-chromatic": "^3.0.0", + "d3-color": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/core": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.99.0.tgz", + "integrity": "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g==", + "license": "MIT", + "dependencies": { + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-shape": "^3.1.6", + "d3-color": "^3.1.0", + "d3-format": "^1.4.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21", + "react-virtualized-auto-sizer": "^1.0.26", + "use-debounce": "^10.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nivo/donate" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/core/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/@nivo/core/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@nivo/core/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/@nivo/core/node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/@nivo/core/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/@nivo/legends": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.99.0.tgz", + "integrity": "sha512-P16FjFqNceuTTZphINAh5p0RF0opu3cCKoWppe2aRD9IuVkvRm/wS5K1YwMCxDzKyKh5v0AuTlu9K6o3/hk8hA==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@types/d3-scale": "^4.0.8", + "d3-scale": "^4.0.2" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/pie": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.99.0.tgz", + "integrity": "sha512-zUbo8UdLndp2RMljrOqitAKKEnl7YypkJrOzjKLk8jQGU7qqUKtgFoJIPhiBsvNPs3xtX2KwgtS1+JKNTNns7A==", + "license": "MIT", + "dependencies": { + "@nivo/arcs": "0.99.0", + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@types/d3-shape": "^3.1.6", + "d3-shape": "^3.2.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/text": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz", + "integrity": "sha512-ho3oZpAZApsJNjsIL5WJSAdg/wjzTBcwo1KiHBlRGUmD+yUWO8qp7V+mnYRhJchwygtRVALlPgZ/rlcW2Xr/MQ==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/theming": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/theming/-/theming-0.99.0.tgz", + "integrity": "sha512-KvXlf0nqBzh/g2hAIV9bzscYvpq1uuO3TnFN3RDXGI72CrbbZFTGzprPju3sy/myVsauv+Bb+V4f5TZ0jkYKRg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/tooltip": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.99.0.tgz", + "integrity": "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1240,6 +1425,78 @@ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/@react-spring/animated": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", + "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", + "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", + "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", + "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", + "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", + "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~10.0.1", + "@react-spring/core": "~10.0.1", + "@react-spring/shared": "~10.0.1", + "@react-spring/types": "~10.0.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", @@ -2319,6 +2576,12 @@ "@types/d3-time": "*" } }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", @@ -3538,6 +3801,19 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -5186,6 +5462,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6023,6 +6305,16 @@ "react-dom": ">=18" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -6955,6 +7247,18 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.5.tgz", + "integrity": "sha512-Q76E3lnIV+4YT9AHcrHEHYmAd9LKwUAbPXDm7FlqVGDHiSOhX3RDjT8dm0AxbJup6WgOb1YEcKyCr11kBJR5KQ==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/client/package.json b/client/package.json index d28b38d..6ed7349 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,8 @@ "api:generate": "openapi --input \"../server/docs/Expense Tracker API.yaml\" --output src/api --client axios" }, "dependencies": { + "@nivo/core": "^0.99.0", + "@nivo/pie": "^0.99.0", "@radix-ui/react-icons": "^1.3.2", "@tailwindcss/vite": "^4.1.11", "@vercel/analytics": "^1.5.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 49f4c99..2a08d0f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -22,7 +22,7 @@ function App() { pauseOnHover={true} anchorOrigin={{ vertical: "top", horizontal: "right" }} > -
+
{location.pathname.includes("/login") || location.pathname.includes("/register") ? null : ( <> diff --git a/client/src/components/common/BackgroundImage.tsx b/client/src/components/common/BackgroundImage.tsx index 048ad73..2657594 100644 --- a/client/src/components/common/BackgroundImage.tsx +++ b/client/src/components/common/BackgroundImage.tsx @@ -2,7 +2,7 @@ import { assets } from "../../assets/images"; function BackgroundImage() { return ( -
+
Bg image
); diff --git a/client/src/components/common/Header/Header.tsx b/client/src/components/common/Header/Header.tsx index 85f3521..07355f8 100644 --- a/client/src/components/common/Header/Header.tsx +++ b/client/src/components/common/Header/Header.tsx @@ -28,7 +28,7 @@ const DashboardHeader = () => { Hi,{" "} John -

+

Track all your transactions with PennyPal

diff --git a/client/src/components/common/Header/components/DateDropdown.tsx b/client/src/components/common/Header/components/DateDropdown.tsx index ddc0d8d..9c100bc 100644 --- a/client/src/components/common/Header/components/DateDropdown.tsx +++ b/client/src/components/common/Header/components/DateDropdown.tsx @@ -30,10 +30,10 @@ const DateDropdown = () => {
- {formatTime(currentDate)} + {formatTime(currentDate)} | - {formatDate(currentDate)} + {formatDate(currentDate)}
diff --git a/client/src/components/common/Header/components/SearchInput.tsx b/client/src/components/common/Header/components/SearchInput.tsx index 4186d84..7456d37 100644 --- a/client/src/components/common/Header/components/SearchInput.tsx +++ b/client/src/components/common/Header/components/SearchInput.tsx @@ -7,7 +7,7 @@ const SearchInput = () => { return (
= ({ className = "" }) => { Bear mascot
diff --git a/client/src/components/dashboard/CalendarComponent.tsx b/client/src/components/dashboard/CalendarComponent.tsx deleted file mode 100644 index 4d9f7f9..0000000 --- a/client/src/components/dashboard/CalendarComponent.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Calendar } from "react-calendar"; -import Layout from "./Layout"; -export default function CalendarComponent() { - return ( - - - - ); -} diff --git a/client/src/components/dashboard/DisplayCard.tsx b/client/src/components/dashboard/DisplayCard.tsx new file mode 100644 index 0000000..747f5c0 --- /dev/null +++ b/client/src/components/dashboard/DisplayCard.tsx @@ -0,0 +1,106 @@ +// MiniStatCard.tsx +import React from "react"; +import { + MoreHorizontal, + TrendingUp, + TrendingDown, + type LucideProps, +} from "lucide-react"; + +export type MiniStatItem = { + label: string; + value: number; + icon: React.ComponentType<{ className?: string } & LucideProps>; + /** Variation en % (ex: +6.4 / -0.8) */ + deltaPct?: number; + /** Sous-texte (ex: "From last month") */ + hint?: string; + /** Suffixe de valeur (ex: "%") */ + valueSuffix?: string; + /** Format personnalisé de la valeur (sinon monnaie Ar abrégée) */ + formatValue?: (v: number) => string; + className?: string; +}; + +const nfMG = new Intl.NumberFormat("fr-MG"); +const fmtArShort = (n: number) => + n >= 1_000_000 + ? `Ar ${(n / 1_000_000).toFixed(1)}M` + : n >= 1_000 + ? `Ar ${(n / 1_000).toFixed(1)}k` + : `Ar ${nfMG.format(n)}`; + +export default function MiniStatCard({ + label, + value, + icon: Icon, + deltaPct, + hint = "From last month", + valueSuffix, + formatValue, + className = "", +}: MiniStatItem) { + const isUp = typeof deltaPct === "number" ? deltaPct >= 0 : undefined; + const deltaColor = + isUp === undefined + ? "bg-slate-100 text-slate-600" + : isUp + ? "bg-emerald-50 text-emerald-600" + : "bg-rose-50 text-rose-600"; + + const valueStr = formatValue + ? formatValue(value) + : valueSuffix + ? nfMG.format(value) + : fmtArShort(value); + + return ( +
+ {/* Header */} +
+
+ + + + {label} +
+ +
+ + {/* Value */} +
+ {valueStr} + {valueSuffix ? valueSuffix : ""} +
+ + {/* Delta */} +
+ {typeof deltaPct === "number" && ( + + {isUp ? ( + + ) : ( + + )} + {isUp ? "+" : ""} + {deltaPct.toFixed(1)}% + + )} + {hint} +
+
+ ); +} diff --git a/client/src/components/dashboard/IncomeGraph.tsx b/client/src/components/dashboard/IncomeGraph.tsx new file mode 100644 index 0000000..5ede1e9 --- /dev/null +++ b/client/src/components/dashboard/IncomeGraph.tsx @@ -0,0 +1,39 @@ +import { + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import Layout from "./Layout"; +const incomeData = [ + { day: "01/08", amount: 12000 }, + { day: "02/08", amount: 15000 }, + { day: "03/08", amount: 8000 }, + { day: "04/08", amount: 18000 }, + { day: "05/08", amount: 10000 }, +]; +export function IncomeGraph() { + return ( + + + + + + + `Ar ${v.toLocaleString("fr-MG")}`} + /> + + + + + ); +} diff --git a/client/src/components/dashboard/IncomePie.tsx b/client/src/components/dashboard/IncomePie.tsx new file mode 100644 index 0000000..0d2ee75 --- /dev/null +++ b/client/src/components/dashboard/IncomePie.tsx @@ -0,0 +1,160 @@ + +// import { useMemo, useState } from "react"; +// import Layout from "./Layout"; + + + +// const initialData = [ +// { name: "Food", value: 45000 }, +// { name: "Transport", value: 20000 }, +// { name: "Shopping", value: 30000 }, +// { name: "Bills", value: 25000 }, +// { name: "Entertainment", value: 15000 }, +// ]; + +// const formatAr = (v: number) => `Ar ${v.toLocaleString("fr-MG")}`; + +// export default function IncomePie() { +// const [activeCats, setActiveCats] = useState( +// initialData.map((d) => d.name) +// ); + +// const chartData = useMemo( +// () => +// initialData +// .filter((d) => activeCats.includes(d.name)) +// .map((d) => ({ id: d.name, label: d.name, value: d.value })), +// [activeCats] +// ); + +// const total = useMemo( +// () => chartData.reduce((a, c) => a + Number(c.value), 0), +// [chartData] +// ); + +// const CenterTotal = ({ centerX, centerY }: any) => ( +// +// {formatAr(total)} +// +// ); + +// return ( +//
+// {/* Graph 2/3 */} +//
+// +// {/* ⬇️ Hauteur explicite sur le parent direct */} +//
+// {chartData.length === 0 ? ( +//
+// Aucune catégorie sélectionnée +//
+// ) : ( +// formatAr(v)} +// theme={{ +// background: "transparent", +// text: { fill: "#e5e7eb" }, +// tooltip: { container: { background: "#0b0f1a" } }, +// }} +// // labels externes uniquement +// enableArcLabels={false} +// enableArcLinkLabels +// arcLinkLabel={(d) => +// `${d.id} ${ +// total ? Math.round((Number(d.value) / total) * 100) : 0 +// }%` +// } +// arcLinkLabelsSkipAngle={8} +// arcLinkLabelsTextColor="#e5e7eb" +// arcLinkLabelsColor={{ from: "color" }} +// arcLinkLabelsThickness={1} +// arcLinkLabelsOffset={6} +// arcLinkLabelsDiagonalLength={14} +// arcLinkLabelsStraightLength={12} +// tooltip={({ datum }) => ( +//
+// {datum.id} :{" "} +// {formatAr(Number(datum.value))} +//
+// )} +// layers={["arcs", "legends", CenterTotal]} +// /> +// )} +//
+//
+//
+ +// {/* Options 1/3 */} +//
+//

Options

+ +//
+// Total +// {formatAr(total)} +//
+ +//
+// +// +//
+ +//
+// {initialData.map((d) => ( +// +// ))} +//
+//
+//
+// ); +// } diff --git a/client/src/components/dashboard/Layout.tsx b/client/src/components/dashboard/Layout.tsx index 4a01dd2..11c62fe 100644 --- a/client/src/components/dashboard/Layout.tsx +++ b/client/src/components/dashboard/Layout.tsx @@ -4,22 +4,26 @@ export default function Layout({ title, children, graphClassName, + titleClassName, }: { title?: string; children: ReactNode; graphClassName?: string; + titleClassName?: string; }) { return ( -
+
{" "} {title && ( -

+

{title}

)} -
+
{children}
diff --git a/client/src/components/dashboard/MonthlyBarchart.tsx b/client/src/components/dashboard/MonthlyBarchart.tsx new file mode 100644 index 0000000..68b53e7 --- /dev/null +++ b/client/src/components/dashboard/MonthlyBarchart.tsx @@ -0,0 +1,134 @@ +import { useMemo, useState } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import Layout from "./Layout"; + +type Row = { month: string; spending: number; income: number }; + +const monthlyData: Row[] = [ + { month: "Avril", spending: 120000, income: 180000 }, + { month: "Mai", spending: 95000, income: 165000 }, + { month: "Juin", spending: 110000, income: 170000 }, + { month: "Juil", spending: 130000, income: 190000 }, +]; + +const COLORS = { + spending: "#FF8042", + income: "#00C49F", +}; + +const fmtShort = (n: number) => + n >= 1_000_000 + ? `${(n / 1_000_000).toFixed(1)}M` + : n >= 1_000 + ? `${(n / 1_000).toFixed(1)}k` + : `${n}`; +const fmtAr = (n: number) => `Ar ${n.toLocaleString("fr-MG")}`; + +export function MonthlyBarChart() { + const [show, setShow] = useState({ spending: true, income: true }); + + const totals = useMemo( + () => ({ + spending: monthlyData.reduce((a, c) => a + c.spending, 0), + income: monthlyData.reduce((a, c) => a + c.income, 0), + }), + [] + ); + + return ( + + {/* Toggles */} +
+ + +
+ + + + {/* Gradients */} + + + + + + + + + + + + + + `Ar ${fmtShort(v)}`} + allowDecimals={false} + domain={["auto", "auto"]} + /> + [ + fmtAr(Number(v)), + name === "spending" ? "Spending" : "Income", + ]} + /> + + {show.spending && ( + + )} + {show.income && ( + + )} + + +
+ ); +} diff --git a/client/src/components/dashboard/PieGraph.tsx b/client/src/components/dashboard/PieGraph.tsx new file mode 100644 index 0000000..da8c734 --- /dev/null +++ b/client/src/components/dashboard/PieGraph.tsx @@ -0,0 +1,61 @@ +import { useMemo } from "react"; +import Layout from "./Layout"; +import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts"; + +export type PieItem = { name: string; value: number; color?: string }; + +const FALLBACK_COLORS = ["#FF8042", "#0088FE", "#00C49F", "#FFBB28", "#AF19FF"]; +const formatAr = (v: number) => `Ar ${v.toLocaleString("fr-MG")}`; + +export default function PieGraph({ + title = "By category", + data, + heightClass = "h-44 sm:h-56 md:h-64", +}: { + title?: string; + data: PieItem[]; + heightClass?: string; +}) { + const total = useMemo( + () => data.reduce((acc, cur) => acc + (cur.value ?? 0), 0), + [data] + ); + + return ( + + + + + `${name} ${(percent * 100).toFixed(0)}%` + } + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {data.map((d, i) => ( + + ))} + + formatAr(v)} /> + + + +
+ Total:{" "} + {formatAr(total)} +
+
+ ); +} diff --git a/client/src/components/dashboard/SoldChart.tsx b/client/src/components/dashboard/SoldChart.tsx deleted file mode 100644 index 1255408..0000000 --- a/client/src/components/dashboard/SoldChart.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { - LineChart, - Line, - XAxis, - YAxis, - Tooltip, - CartesianGrid, - ResponsiveContainer, -} from "recharts"; -import Layout from "./Layout"; - -type Tx = { - amount: number; - createdAt: string; -}; - -function getLastNDays(n = 10) { - const days: { key: string; label: string }[] = []; - const today = new Date(); - for (let i = n - 1; i >= 0; i--) { - const d = new Date(today); - d.setDate(today.getDate() - i); - const key = d.toISOString().slice(0, 10); - const label = d.toLocaleDateString("fr-FR", { - day: "2-digit", - month: "2-digit", - }); - days.push({ key, label }); - } - return days; -} - -const formatAr = (v: number) => `Ar ${Math.round(v).toLocaleString("fr-MG")}`; - -type Props = { - transactions: Tx[]; - title?: string; -}; - -export default function SoldChart({ - transactions, - title = "Sold variation over the last 10 days", -}: Props) { - const days = getLastNDays(10); - const sumByDay = new Map(); - for (const tx of transactions ?? []) { - const d = new Date(tx.createdAt); - const key = isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10); - if (!key) continue; - sumByDay.set(key, (sumByDay.get(key) ?? 0) + (tx.amount || 0)); - } - const data = days.map(({ key, label }) => ({ - name: label, - amount: sumByDay.get(key) ?? 0, - })); - - return ( - - - - - - - - - - - - - - [formatAr(value), "Income"]} - labelClassName="font-semibold" - /> - - - - - ); -} diff --git a/client/src/components/dashboard/SpendingPie.tsx b/client/src/components/dashboard/SpendingPie.tsx deleted file mode 100644 index 8c089eb..0000000 --- a/client/src/components/dashboard/SpendingPie.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useState } from "react"; -import Layout from "./Layout"; -import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts"; - -const initialData = [ - { name: "Food", value: 45000 }, - { name: "Transport", value: 20000 }, - { name: "Shopping", value: 30000 }, - { name: "Bills", value: 25000 }, - { name: "Entertainment", value: 15000 }, -]; - -const COLORS = ["#FF8042", "#0088FE", "#00C49F", "#FFBB28", "#AF19FF"]; - -const formatAr = (v: number) => `Ar ${v.toLocaleString("fr-MG")}`; - -export default function SpendingPie() { - const [activeCats, setActiveCats] = useState( - initialData.map((d) => d.name) - ); - - const toggleCategory = (name: string) => { - setActiveCats((prev) => - prev.includes(name) ? prev.filter((c) => c !== name) : [...prev, name] - ); - }; - - const filteredData = initialData.filter((d) => activeCats.includes(d.name)); - const total = filteredData.reduce((acc, cur) => acc + cur.value, 0); - - return ( -
- {/* Chart */} - - - - - `${name} ${(percent * 100).toFixed(0)}%` - } - outerRadius={100} - fill="#8884d8" - dataKey="value" - > - {filteredData.map((_entry, index) => ( - - ))} - - formatAr(v)} /> - - - - - {/* Options */} -
-

Options

- - {/* Total */} -
- Total - {formatAr(total)} -
- - {/* Filtrage */} -
- {initialData.map((d, i) => ( - - ))} -
-
-
- ); -} diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index 07656bb..d197757 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -1,21 +1,92 @@ -import CalendarComponent from "../components/dashboard/CalendarComponent"; -import SoldChart from "../components/dashboard/SoldChart"; -import SpendingPie from "../components/dashboard/SpendingPie"; +import { useState } from "react"; +import StatCard, { + type MiniStatItem, +} from "../components/dashboard/DisplayCard"; +import SpendingPie, { type PieItem } from "../components/dashboard/PieGraph"; +import { MonthlyBarChart } from "../components/dashboard/MonthlyBarchart"; +import { Percent, ReceiptCent, Wallet } from "lucide-react"; + +const mockTotals: MiniStatItem[] = [ + { label: "Revenue", value: 82450, icon: Wallet, deltaPct: 6.4 }, + { label: "Expenses", value: 24780, icon: ReceiptCent, deltaPct: -0.11 }, + { + label: "Spending Rate", + value: 65, + icon: Percent, + deltaPct: -0.8, + valueSuffix: "%", + formatValue: (v) => v.toFixed(0), + }, +]; + +const expensesByCat: PieItem[] = [ + { name: "Food", value: 45000, color: "#FF8042" }, + { name: "Transport", value: 20000, color: "#0088FE" }, + { name: "Shopping", value: 30000, color: "#00C49F" }, + { name: "Bills", value: 25000, color: "#FFBB28" }, + { name: "Entertainment", value: 15000, color: "#AF19FF" }, +]; + +const incomeByCat: PieItem[] = [ + { name: "Salary", value: 120000, color: "#22c55e" }, + { name: "Freelance", value: 65000, color: "#06b6d4" }, + { name: "Investments", value: 30000, color: "#a78bfa" }, + { name: "Other", value: 15000, color: "#f472b6" }, +]; function Dashboard() { + const [activeGraph, setActiveGraph] = useState<"expenses" | "income">( + "expenses" + ); + + const currentPieData = + activeGraph === "expenses" ? expensesByCat : incomeByCat; + const currentTitle = + activeGraph === "expenses" + ? "Expenses — by category" + : "Income — by category"; + return ( -
-
- - - +
+
+
+ {mockTotals.map((item) => ( + + ))} +
+ +
+ + +
+
+
+ + +
+
+
-
); } From 289d2138abc2ddc1024afd387759ed377bffacdf Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Aug 2025 13:09:52 +0300 Subject: [PATCH 6/9] feat: dashboard with mock data --- client/package-lock.json | 924 +++++++++++++++++- client/package.json | 5 + .../src/components/dashboard/DisplayCard.tsx | 22 +- .../src/components/dashboard/IncomeGraph.tsx | 39 - client/src/components/dashboard/IncomePie.tsx | 160 --- .../components/dashboard/MonthlyBarchart.tsx | 40 +- client/src/components/dashboard/PieGraph.tsx | 64 +- client/src/pages/Dashboard.tsx | 23 +- client/src/utils/formatter.ts | 7 + 9 files changed, 991 insertions(+), 293 deletions(-) delete mode 100644 client/src/components/dashboard/IncomeGraph.tsx delete mode 100644 client/src/components/dashboard/IncomePie.tsx create mode 100644 client/src/utils/formatter.ts diff --git a/client/package-lock.json b/client/package-lock.json index 71ef937..b5ae4c8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,11 @@ "name": "expense-tracker", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.3.1", + "@mui/x-charts": "^8.10.2", + "@nivo/bar": "^0.99.0", "@nivo/core": "^0.99.0", "@nivo/pie": "^0.99.0", "@radix-ui/react-icons": "^1.3.2", @@ -94,7 +99,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -150,7 +154,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.0", @@ -184,7 +187,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -194,7 +196,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -236,7 +237,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -246,7 +246,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -280,7 +279,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.0" @@ -328,7 +326,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -338,7 +335,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -353,7 +349,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -372,7 +367,6 @@ "version": "7.28.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -413,6 +407,167 @@ "storybook": "^0.0.0-0 || ^9.0.0 || ^9.1.0-0 || ^9.2.0-0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", @@ -1170,6 +1325,318 @@ "react": ">=16" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.1.tgz", + "integrity": "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz", + "integrity": "sha512-Xf6Shbo03YmcBedZMwSpEFOwpYDtU7tC+rhAHTrA9FHk0FpsDqiQ9jUa1j/9s3HLs7KWb5mDcGnlwdh9Q9KAag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/core-downloads-tracker": "^7.3.1", + "@mui/system": "^7.3.1", + "@mui/types": "^7.4.5", + "@mui/utils": "^7.3.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.1.tgz", + "integrity": "sha512-WU3YLkKXii/x8ZEKnrLKsPwplCVE11yZxUvlaaZSIzCcI3x2OdFC8eMlNy74hVeUsYQvzzX1Es/k4ARPlFvpPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.1.tgz", + "integrity": "sha512-Nqo6OHjvJpXJ1+9TekTE//+8RybgPQUKwns2Lh0sq+8rJOUSUKS3KALv4InSOdHhIM9Mdi8/L7LTF1/Ky6D6TQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.1.tgz", + "integrity": "sha512-mIidecvcNVpNJMdPDmCeoSL5zshKBbYPcphjuh6ZMjhybhqhZ4mX6k9zmIWh6XOXcqRQMg5KrcjnO0QstrNj3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/private-theming": "^7.3.1", + "@mui/styled-engine": "^7.3.1", + "@mui/types": "^7.4.5", + "@mui/utils": "^7.3.1", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz", + "integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.1.tgz", + "integrity": "sha512-/31y4wZqVWa0jzMnzo6JPjxwP6xXy4P3+iLbosFg/mJQowL1KIou0LC+lquWW60FKVbKz5ZUWBg2H3jausa0pw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/types": "^7.4.5", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/@mui/x-charts": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.10.2.tgz", + "integrity": "sha512-F4SdpixbaAeNaZHylYLpaj/WD1jKn6gMD1Twbd+Y8FzZneE2mrtXwaQc6W2Hbri/VdJl2OV55nL5pFMpDSlwAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.1", + "@mui/x-charts-vendor": "8.6.0", + "@mui/x-internal-gestures": "0.2.4", + "@mui/x-internals": "8.10.2", + "bezier-easing": "^2.1.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.6.0.tgz", + "integrity": "sha512-TTtfhxXuwtoZfyno7+4y3ZhZeFqavFJecWbteLEby0lFqALWB9GGJpkc1TIHWr3GkWE5UHEbdADZ0pfrPenezA==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.7", + "@types/d3-time": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-timer": "^3.0.1", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-internal-gestures": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-0.2.4.tgz", + "integrity": "sha512-Hpc5+LQfT0TrI7O0ngBlZ1LD6Gp1h5DUNs4ABbrotY/2OsOfJOzltV/QtBjINTmiBzDPnrE8l8nEiG97qOWX3Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2" + } + }, + "node_modules/@mui/x-internals": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.2.tgz", + "integrity": "sha512-dlC0BQRRBdiWtqn1yDppaHYRUjU3OuPWTxy0UtqxDaJjJf4pfR8ALr243nbxgJAFqvQyWPWyO4A6p9x9eJMJEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@neoconfetti/react": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@neoconfetti/react/-/react-1.0.0.tgz", @@ -1177,6 +1644,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@nivo/annotations": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.99.0.tgz", + "integrity": "sha512-jCuuXPbvpaqaz4xF7k5dv0OT2ubn5Nt0gWryuTe/8oVsC/9bzSuK8bM9vBty60m9tfO+X8vUYliuaCDwGksC2g==", + "license": "MIT", + "dependencies": { + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, "node_modules/@nivo/arcs": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.99.0.tgz", @@ -1196,6 +1679,98 @@ "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/@nivo/axes": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.99.0.tgz", + "integrity": "sha512-3KschnmEL0acRoa7INSSOSEFwJLm54aZwSev7/r8XxXlkgRBriu6ReZy/FG0wfN+ljZ4GMvx+XyIIf6kxzvrZg==", + "license": "MIT", + "dependencies": { + "@nivo/core": "0.99.0", + "@nivo/scales": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-format": "^1.4.1", + "@types/d3-time-format": "^2.3.1", + "d3-format": "^1.4.4", + "d3-time-format": "^3.0.0" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/axes/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/@nivo/axes/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@nivo/axes/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "2" + } + }, + "node_modules/@nivo/axes/node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, + "node_modules/@nivo/axes/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/@nivo/bar": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.99.0.tgz", + "integrity": "sha512-9yfMn7H6UF/TqtCwVZ/vihVAXUff9wWvSaeF2Z1DCfgr5S07qs31Qb2p0LZA+YgCWpaU7zqkeb3VZ4WCpZbrDA==", + "license": "MIT", + "dependencies": { + "@nivo/annotations": "0.99.0", + "@nivo/axes": "0.99.0", + "@nivo/canvas": "0.99.0", + "@nivo/colors": "0.99.0", + "@nivo/core": "0.99.0", + "@nivo/legends": "0.99.0", + "@nivo/scales": "0.99.0", + "@nivo/text": "0.99.0", + "@nivo/theming": "0.99.0", + "@nivo/tooltip": "0.99.0", + "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" + } + }, + "node_modules/@nivo/canvas": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/canvas/-/canvas-0.99.0.tgz", + "integrity": "sha512-UxA8zb+NPwqmNm81hoyUZSMAikgjU1ukLf4KybVNyV8ejcJM+BUFXsb8DxTcLdt4nmCFHqM56GaJQv2hnAHmzg==", + "license": "MIT" + }, "node_modules/@nivo/colors": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.99.0.tgz", @@ -1320,6 +1895,50 @@ "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, + "node_modules/@nivo/scales": { + "version": "0.99.0", + "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.99.0.tgz", + "integrity": "sha512-g/2K4L6L8si6E2BWAHtFVGahtDKbUcO6xHJtlIZMwdzaJc7yB16EpWLK8AfI/A42KadLhJSJqBK3mty+c7YZ+w==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-time": "^1.1.1", + "@types/d3-time-format": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-time": "^1.0.11", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21" + } + }, + "node_modules/@nivo/scales/node_modules/@types/d3-time": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.1.4.tgz", + "integrity": "sha512-JIvy2HjRInE+TXOmIGN5LCmeO0hkFZx5f9FZ7kiN+D+YTcc8pptsiLiuHsvwxwC7VVKmJ2ExHUgNlAiV7vQM9g==", + "license": "MIT" + }, + "node_modules/@nivo/scales/node_modules/@types/d3-time-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-3.0.4.tgz", + "integrity": "sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg==", + "license": "MIT" + }, + "node_modules/@nivo/scales/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "license": "BSD-3-Clause" + }, + "node_modules/@nivo/scales/node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" + } + }, "node_modules/@nivo/text": { "version": "0.99.0", "resolved": "https://registry.npmjs.org/@nivo/text/-/text-0.99.0.tgz", @@ -1416,6 +2035,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/react-icons": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", @@ -2546,12 +3175,24 @@ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "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-format": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==", + "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", @@ -2597,6 +3238,12 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.3.4.tgz", + "integrity": "sha512-xdDXbpVO74EvadI3UDxjxTdR6QIxm1FKzEA/+F8tL4GWWUg/hgvBqf6chql64U5A9ZUGWo7pEu4eNlyLwbKdhg==", + "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", @@ -2637,11 +3284,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2657,6 +3315,15 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", @@ -3401,6 +4068,21 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3421,6 +4103,12 @@ "node": ">=12.0.0" } }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3505,7 +4193,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3696,6 +4383,31 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3722,7 +4434,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -3746,6 +4457,18 @@ "node": ">=12" } }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -3863,7 +4586,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3910,6 +4632,15 @@ "node": ">=8" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3958,6 +4689,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4006,6 +4747,15 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4136,7 +4886,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4474,6 +5223,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4858,6 +5613,21 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4889,7 +5659,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -4931,11 +5700,16 @@ "node": ">=12" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -5128,7 +5902,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5144,6 +5917,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5446,6 +6225,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5752,7 +6537,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5794,6 +6578,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -5890,7 +6683,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -5899,6 +6691,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5923,7 +6733,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -5950,6 +6759,15 @@ "dev": true, "license": "ISC" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -6113,6 +6931,23 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6305,6 +7140,22 @@ "react-dom": ">=18" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/react-virtualized-auto-sizer": { "version": "1.0.26", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", @@ -6411,7 +7262,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -6432,7 +7282,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6449,6 +7298,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", @@ -6837,6 +7692,12 @@ "dev": true, "license": "MIT" }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6854,7 +7715,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7679,6 +8539,20 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index 6ed7349..6566c3b 100644 --- a/client/package.json +++ b/client/package.json @@ -13,6 +13,11 @@ "api:generate": "openapi --input \"../server/docs/Expense Tracker API.yaml\" --output src/api --client axios" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.3.1", + "@mui/x-charts": "^8.10.2", + "@nivo/bar": "^0.99.0", "@nivo/core": "^0.99.0", "@nivo/pie": "^0.99.0", "@radix-ui/react-icons": "^1.3.2", diff --git a/client/src/components/dashboard/DisplayCard.tsx b/client/src/components/dashboard/DisplayCard.tsx index 747f5c0..b90499a 100644 --- a/client/src/components/dashboard/DisplayCard.tsx +++ b/client/src/components/dashboard/DisplayCard.tsx @@ -1,23 +1,13 @@ -// MiniStatCard.tsx import React from "react"; -import { - MoreHorizontal, - TrendingUp, - TrendingDown, - type LucideProps, -} from "lucide-react"; +import { TrendingUp, TrendingDown, type LucideProps } from "lucide-react"; export type MiniStatItem = { label: string; value: number; icon: React.ComponentType<{ className?: string } & LucideProps>; - /** Variation en % (ex: +6.4 / -0.8) */ deltaPct?: number; - /** Sous-texte (ex: "From last month") */ hint?: string; - /** Suffixe de valeur (ex: "%") */ valueSuffix?: string; - /** Format personnalisé de la valeur (sinon monnaie Ar abrégée) */ formatValue?: (v: number) => string; className?: string; }; @@ -70,12 +60,6 @@ export default function MiniStatCard({ {label}
-
{/* Value */} @@ -88,7 +72,7 @@ export default function MiniStatCard({
{typeof deltaPct === "number" && ( {isUp ? ( @@ -99,7 +83,7 @@ export default function MiniStatCard({ {deltaPct.toFixed(1)}% )} - {hint} + {hint}
); diff --git a/client/src/components/dashboard/IncomeGraph.tsx b/client/src/components/dashboard/IncomeGraph.tsx deleted file mode 100644 index 5ede1e9..0000000 --- a/client/src/components/dashboard/IncomeGraph.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - CartesianGrid, - Line, - LineChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import Layout from "./Layout"; -const incomeData = [ - { day: "01/08", amount: 12000 }, - { day: "02/08", amount: 15000 }, - { day: "03/08", amount: 8000 }, - { day: "04/08", amount: 18000 }, - { day: "05/08", amount: 10000 }, -]; -export function IncomeGraph() { - return ( - - - - - - - `Ar ${v.toLocaleString("fr-MG")}`} - /> - - - - - ); -} diff --git a/client/src/components/dashboard/IncomePie.tsx b/client/src/components/dashboard/IncomePie.tsx deleted file mode 100644 index 0d2ee75..0000000 --- a/client/src/components/dashboard/IncomePie.tsx +++ /dev/null @@ -1,160 +0,0 @@ - -// import { useMemo, useState } from "react"; -// import Layout from "./Layout"; - - - -// const initialData = [ -// { name: "Food", value: 45000 }, -// { name: "Transport", value: 20000 }, -// { name: "Shopping", value: 30000 }, -// { name: "Bills", value: 25000 }, -// { name: "Entertainment", value: 15000 }, -// ]; - -// const formatAr = (v: number) => `Ar ${v.toLocaleString("fr-MG")}`; - -// export default function IncomePie() { -// const [activeCats, setActiveCats] = useState( -// initialData.map((d) => d.name) -// ); - -// const chartData = useMemo( -// () => -// initialData -// .filter((d) => activeCats.includes(d.name)) -// .map((d) => ({ id: d.name, label: d.name, value: d.value })), -// [activeCats] -// ); - -// const total = useMemo( -// () => chartData.reduce((a, c) => a + Number(c.value), 0), -// [chartData] -// ); - -// const CenterTotal = ({ centerX, centerY }: any) => ( -// -// {formatAr(total)} -// -// ); - -// return ( -//
-// {/* Graph 2/3 */} -//
-// -// {/* ⬇️ Hauteur explicite sur le parent direct */} -//
-// {chartData.length === 0 ? ( -//
-// Aucune catégorie sélectionnée -//
-// ) : ( -// formatAr(v)} -// theme={{ -// background: "transparent", -// text: { fill: "#e5e7eb" }, -// tooltip: { container: { background: "#0b0f1a" } }, -// }} -// // labels externes uniquement -// enableArcLabels={false} -// enableArcLinkLabels -// arcLinkLabel={(d) => -// `${d.id} ${ -// total ? Math.round((Number(d.value) / total) * 100) : 0 -// }%` -// } -// arcLinkLabelsSkipAngle={8} -// arcLinkLabelsTextColor="#e5e7eb" -// arcLinkLabelsColor={{ from: "color" }} -// arcLinkLabelsThickness={1} -// arcLinkLabelsOffset={6} -// arcLinkLabelsDiagonalLength={14} -// arcLinkLabelsStraightLength={12} -// tooltip={({ datum }) => ( -//
-// {datum.id} :{" "} -// {formatAr(Number(datum.value))} -//
-// )} -// layers={["arcs", "legends", CenterTotal]} -// /> -// )} -//
-//
-//
- -// {/* Options 1/3 */} -//
-//

Options

- -//
-// Total -// {formatAr(total)} -//
- -//
-// -// -//
- -//
-// {initialData.map((d) => ( -// -// ))} -//
-//
-//
-// ); -// } diff --git a/client/src/components/dashboard/MonthlyBarchart.tsx b/client/src/components/dashboard/MonthlyBarchart.tsx index 68b53e7..89a6e80 100644 --- a/client/src/components/dashboard/MonthlyBarchart.tsx +++ b/client/src/components/dashboard/MonthlyBarchart.tsx @@ -9,38 +9,23 @@ import { YAxis, } from "recharts"; import Layout from "./Layout"; - -type Row = { month: string; spending: number; income: number }; - -const monthlyData: Row[] = [ - { month: "Avril", spending: 120000, income: 180000 }, - { month: "Mai", spending: 95000, income: 165000 }, - { month: "Juin", spending: 110000, income: 170000 }, - { month: "Juil", spending: 130000, income: 190000 }, -]; +import { fmtAr, fmtShort } from "../../utils/formatter"; +export type Row = { month: string; spending: number; income: number }; const COLORS = { spending: "#FF8042", income: "#00C49F", }; -const fmtShort = (n: number) => - n >= 1_000_000 - ? `${(n / 1_000_000).toFixed(1)}M` - : n >= 1_000 - ? `${(n / 1_000).toFixed(1)}k` - : `${n}`; -const fmtAr = (n: number) => `Ar ${n.toLocaleString("fr-MG")}`; - -export function MonthlyBarChart() { +export function MonthlyBarChart({ data }: { data: Row[] }) { const [show, setShow] = useState({ spending: true, income: true }); const totals = useMemo( () => ({ - spending: monthlyData.reduce((a, c) => a + c.spending, 0), - income: monthlyData.reduce((a, c) => a + c.income, 0), + spending: data.reduce((a, c) => a + c.spending, 0), + income: data.reduce((a, c) => a + c.income, 0), }), - [] + [data] ); return ( @@ -49,7 +34,7 @@ export function MonthlyBarChart() {
- - - {/* Gradients */} + + @@ -105,9 +89,9 @@ export function MonthlyBarChart() { borderRadius: 8, color: "#fff", }} - formatter={(v: any, name: string) => [ + formatter={(v, name: string) => [ fmtAr(Number(v)), - name === "spending" ? "Spending" : "Income", + name == "spending" ? "Spending" : "Income", ]} /> diff --git a/client/src/components/dashboard/PieGraph.tsx b/client/src/components/dashboard/PieGraph.tsx index da8c734..6960bbc 100644 --- a/client/src/components/dashboard/PieGraph.tsx +++ b/client/src/components/dashboard/PieGraph.tsx @@ -1,6 +1,14 @@ import { useMemo } from "react"; import Layout from "./Layout"; -import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts"; +import { + PieChart, + Pie, + Cell, + Tooltip, + ResponsiveContainer, + Legend, +} from "recharts"; +import { fmtAr } from "../../utils/formatter"; export type PieItem = { name: string; value: number; color?: string }; @@ -17,10 +25,9 @@ export default function PieGraph({ heightClass?: string; }) { const total = useMemo( - () => data.reduce((acc, cur) => acc + (cur.value ?? 0), 0), + () => data.reduce((acc, item) => acc + item.value, 0), [data] ); - return ( - + - `${name} ${(percent * 100).toFixed(0)}%` - } + innerRadius={50} outerRadius={80} - fill="#8884d8" + label={false} dataKey="value" > {data.map((d, i) => ( @@ -48,14 +52,46 @@ export default function PieGraph({ /> ))} + formatAr(v)} /> + + ( +
    + {data.map((d, i) => ( +
  • + + {d.name} +
  • + ))} +
+ )} + />
- -
- Total:{" "} - {formatAr(total)} -
+ Total: {fmtAr(total)}
); } diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index d197757..f2818b3 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -2,23 +2,31 @@ import { useState } from "react"; import StatCard, { type MiniStatItem, } from "../components/dashboard/DisplayCard"; -import SpendingPie, { type PieItem } from "../components/dashboard/PieGraph"; -import { MonthlyBarChart } from "../components/dashboard/MonthlyBarchart"; +import { type PieItem } from "../components/dashboard/PieGraph"; +import { + MonthlyBarChart, + type Row, +} from "../components/dashboard/MonthlyBarchart"; import { Percent, ReceiptCent, Wallet } from "lucide-react"; +import PieGraph from "../components/dashboard/PieGraph"; const mockTotals: MiniStatItem[] = [ { label: "Revenue", value: 82450, icon: Wallet, deltaPct: 6.4 }, { label: "Expenses", value: 24780, icon: ReceiptCent, deltaPct: -0.11 }, { - label: "Spending Rate", + label: "Sold", value: 65, icon: Percent, deltaPct: -0.8, - valueSuffix: "%", - formatValue: (v) => v.toFixed(0), }, ]; +const monthlyData: Row[] = [ + { month: "Avril", spending: 120000, income: 180000 }, + { month: "Mai", spending: 95000, income: 165000 }, + { month: "Juin", spending: 110000, income: 170000 }, + { month: "Juil", spending: 130000, income: 190000 }, +]; const expensesByCat: PieItem[] = [ { name: "Food", value: 45000, color: "#FF8042" }, { name: "Transport", value: 20000, color: "#0088FE" }, @@ -54,10 +62,9 @@ function Dashboard() { ))}
- +
-
@@ -85,7 +92,7 @@ function Dashboard() {
- +
); diff --git a/client/src/utils/formatter.ts b/client/src/utils/formatter.ts new file mode 100644 index 0000000..2995b36 --- /dev/null +++ b/client/src/utils/formatter.ts @@ -0,0 +1,7 @@ +export const fmtShort = (n: number) => + n >= 1_000_000 + ? `${(n / 1_000_000).toFixed(1)}M` + : n >= 1_000 + ? `${(n / 1_000).toFixed(1)}k` + : `${n}`; +export const fmtAr = (n: number) => `Ar ${n.toLocaleString("fr-MG")}`; From a1a9e5e272f09d3abea1e7b32ede6c3e16c0b222 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Aug 2025 13:11:04 +0300 Subject: [PATCH 7/9] fix: bug on the tooltip displayer --- client/src/components/dashboard/MonthlyBarchart.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/components/dashboard/MonthlyBarchart.tsx b/client/src/components/dashboard/MonthlyBarchart.tsx index 89a6e80..ee7229d 100644 --- a/client/src/components/dashboard/MonthlyBarchart.tsx +++ b/client/src/components/dashboard/MonthlyBarchart.tsx @@ -54,7 +54,11 @@ export function MonthlyBarChart({ data }: { data: Row[] }) {
- + @@ -89,10 +93,7 @@ export function MonthlyBarChart({ data }: { data: Row[] }) { borderRadius: 8, color: "#fff", }} - formatter={(v, name: string) => [ - fmtAr(Number(v)), - name == "spending" ? "Spending" : "Income", - ]} + formatter={(v, name: string) => [fmtAr(Number(v)), String(name)]} /> {show.spending && ( From 56b0946933bcab9703aee2a46e39b703ad2a3656 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Aug 2025 13:31:52 +0300 Subject: [PATCH 8/9] feat: seed file for test --- server/package.json | 3 +- .../20250824074829_dev/migration.sql | 92 +++++++ server/prisma/seed.js | 226 ++++++++++++++++++ 3 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 server/prisma/migrations/20250824074829_dev/migration.sql create mode 100644 server/prisma/seed.js diff --git a/server/package.json b/server/package.json index c5b9cdc..1bbb5fa 100644 --- a/server/package.json +++ b/server/package.json @@ -11,7 +11,8 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:deploy": "prisma migrate deploy", - "prisma:studio": "prisma studio" + "prisma:studio": "prisma studio", + "prisma:seed": "node ./prisma/seed.js" }, "keywords": [ "express", diff --git a/server/prisma/migrations/20250824074829_dev/migration.sql b/server/prisma/migrations/20250824074829_dev/migration.sql new file mode 100644 index 0000000..cdd1879 --- /dev/null +++ b/server/prisma/migrations/20250824074829_dev/migration.sql @@ -0,0 +1,92 @@ +-- CreateTable +CREATE TABLE "public"."user" ( + "user_id" SERIAL NOT NULL, + "username" VARCHAR(50) NOT NULL, + "email" VARCHAR(255) NOT NULL, + "hashed_password" VARCHAR(255) NOT NULL, + "firstname" VARCHAR(100) NOT NULL, + "lastname" VARCHAR(100) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "public"."expense_category" ( + "category_id" SERIAL NOT NULL, + "category_name" VARCHAR(50) NOT NULL, + "icon_url" VARCHAR(500), + "is_custom" BOOLEAN NOT NULL DEFAULT false, + "user_id" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "expense_category_pkey" PRIMARY KEY ("category_id") +); + +-- CreateTable +CREATE TABLE "public"."income_category" ( + "category_id" SERIAL NOT NULL, + "category_name" VARCHAR(50) NOT NULL, + "icon_url" VARCHAR(500), + "icon_emoji" VARCHAR(10), + "is_custom" BOOLEAN NOT NULL DEFAULT false, + "user_id" INTEGER, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "income_category_pkey" PRIMARY KEY ("category_id") +); + +-- CreateTable +CREATE TABLE "public"."expense" ( + "expense_id" SERIAL NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "description" VARCHAR(2000), + "type" VARCHAR(20) NOT NULL DEFAULT 'ONE_TIME', + "receipt_upload" VARCHAR(2000), + "creation_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expense_date" TIMESTAMP(3), + "start_date" TIMESTAMP(3), + "end_date" TIMESTAMP(3), + "user_id" INTEGER NOT NULL, + "category_id" INTEGER NOT NULL, + + CONSTRAINT "expense_pkey" PRIMARY KEY ("expense_id") +); + +-- CreateTable +CREATE TABLE "public"."income" ( + "income_id" SERIAL NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "source" VARCHAR(100) NOT NULL, + "description" VARCHAR(2000), + "creation_date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "user_id" INTEGER NOT NULL, + "category_id" INTEGER NOT NULL, + + CONSTRAINT "income_pkey" PRIMARY KEY ("income_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email"); + +-- AddForeignKey +ALTER TABLE "public"."expense_category" ADD CONSTRAINT "expense_category_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."income_category" ADD CONSTRAINT "income_category_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."expense" ADD CONSTRAINT "expense_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."expense" ADD CONSTRAINT "expense_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."expense_category"("category_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."income" ADD CONSTRAINT "income_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."income" ADD CONSTRAINT "income_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."income_category"("category_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/seed.js b/server/prisma/seed.js new file mode 100644 index 0000000..ba4b0a5 --- /dev/null +++ b/server/prisma/seed.js @@ -0,0 +1,226 @@ +import { PrismaClient } from "@prisma/client"; +const prisma = new PrismaClient(); + +async function ensureUser(data) { + return prisma.user.upsert({ + where: { email: data.email }, + update: {}, + create: data, + }); +} + +async function ensureExpenseCategory(category_name, opts = {}) { + const { user_id = null, is_custom = false, icon_url = null } = opts; + const existing = await prisma.expenseCategory.findFirst({ + where: { category_name, user_id: user_id ?? null }, + select: { category_id: true }, + }); + if (existing) return existing.category_id; + const created = await prisma.expenseCategory.create({ + data: { category_name, user_id, is_custom, icon_url }, + select: { category_id: true }, + }); + return created.category_id; +} + +async function ensureIncomeCategory(category_name, opts = {}) { + const { user_id = null, is_custom = false, icon_url = null } = opts; + const existing = await prisma.incomeCategory.findFirst({ + where: { category_name, user_id: user_id ?? null }, + select: { category_id: true }, + }); + if (existing) return existing.category_id; + const created = await prisma.incomeCategory.create({ + data: { category_name, user_id, is_custom, icon_url }, + select: { category_id: true }, + }); + return created.category_id; +} + +async function main() { + // --- Users --- + const jane = await ensureUser({ + username: "jane_doe", + email: "jane@example.com", + hashed_password: "bcrypt$dummyhash$jane", // remplace par un vrai hash + firstname: "Jane", + lastname: "Doe", + }); + + const john = await ensureUser({ + username: "john_smith", + email: "john@example.com", + hashed_password: "bcrypt$dummyhash$john", + firstname: "John", + lastname: "Smith", + }); + + // --- Default categories (global = user_id null) --- + const defaultExpenseCats = [ + "Food", + "Transport", + "Shopping", + "Bills", + "Entertainment", + "Rent", + "Utilities", + ]; + const defaultIncomeCats = [ + "Salary", + "Freelance", + "Investments", + "Gifts", + "Other", + ]; + + const expCatIdsGlobal = {}; + for (const name of defaultExpenseCats) { + expCatIdsGlobal[name] = await ensureExpenseCategory(name); + } + const incCatIdsGlobal = {}; + for (const name of defaultIncomeCats) { + incCatIdsGlobal[name] = await ensureIncomeCategory(name); + } + + // --- Custom categories for Jane --- + const catCoffee = await ensureExpenseCategory("Coffee", { + user_id: jane.user_id, + is_custom: true, + }); + const catSideHustle = await ensureIncomeCategory("Side Hustle", { + user_id: jane.user_id, + is_custom: true, + }); + + // --- Sample expenses for Jane --- + await prisma.expense.createMany({ + data: [ + { + amount: 45000, + description: "Supermarket", + type: "ONE_TIME", + expense_date: new Date("2025-08-02"), + user_id: jane.user_id, + category_id: expCatIdsGlobal["Food"], + }, + { + amount: 15000, + description: "Bus card", + type: "ONE_TIME", + expense_date: new Date("2025-08-03"), + user_id: jane.user_id, + category_id: expCatIdsGlobal["Transport"], + }, + { + amount: 60000, + description: "Clothes", + type: "ONE_TIME", + expense_date: new Date("2025-08-05"), + user_id: jane.user_id, + category_id: expCatIdsGlobal["Shopping"], + }, + { + amount: 250000, + description: "Monthly rent", + type: "RECURRING", + expense_date: new Date("2025-08-01"), + user_id: jane.user_id, + category_id: expCatIdsGlobal["Rent"], + }, + { + amount: 8000, + description: "Latte", + type: "ONE_TIME", + expense_date: new Date("2025-08-06"), + user_id: jane.user_id, + category_id: catCoffee, + }, + ], + skipDuplicates: true, // n'empêche pas les doublons ici (pas d'unique), mais OK si relancé sans delete + }); + + // --- Sample incomes for Jane --- + await prisma.income.createMany({ + data: [ + { + amount: 1_200_000, + date: new Date("2025-08-01"), + source: "Company X", + description: "Monthly salary", + user_id: jane.user_id, + category_id: incCatIdsGlobal["Salary"], + }, + { + amount: 300_000, + date: new Date("2025-08-10"), + source: "Freelance UI work", + description: "Landing page", + user_id: jane.user_id, + category_id: incCatIdsGlobal["Freelance"], + }, + { + amount: 120_000, + date: new Date("2025-08-12"), + source: "Etsy shop", + description: "Side hustle payout", + user_id: jane.user_id, + category_id: catSideHustle, + }, + ], + skipDuplicates: true, + }); + + // --- A couple of rows for John as well --- + await prisma.expense.createMany({ + data: [ + { + amount: 38000, + description: "Groceries", + type: "ONE_TIME", + expense_date: new Date("2025-08-04"), + user_id: john.user_id, + category_id: expCatIdsGlobal["Food"], + }, + { + amount: 120000, + description: "Electricity bill", + type: "ONE_TIME", + expense_date: new Date("2025-08-07"), + user_id: john.user_id, + category_id: expCatIdsGlobal["Utilities"], + }, + ], + }); + + await prisma.income.createMany({ + data: [ + { + amount: 900_000, + date: new Date("2025-08-01"), + source: "Company Y", + description: "Salary", + user_id: john.user_id, + category_id: incCatIdsGlobal["Salary"], + }, + { + amount: 80_000, + date: new Date("2025-08-09"), + source: "Dividends", + description: "ETF payout", + user_id: john.user_id, + category_id: incCatIdsGlobal["Investments"], + }, + ], + }); + + console.log("✅ Seed done."); +} + +main() + .catch((e) => { + console.error("❌ Seed error", e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); From d2a9d85e523013b1ed7432ade8192258d6e69942 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 27 Aug 2025 13:33:16 +0300 Subject: [PATCH 9/9] chore: remove invalid attribute in tsconfig.app.json --- client/tsconfig.app.json | 1 - 1 file changed, 1 deletion(-) diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 227a6c6..1597d8c 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -19,7 +19,6 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true },