diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2dc83df..fd0d83d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,6 +27,7 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.26.2", "react-transition-group": "^4.4.5", + "recharts": "^3.2.1", "satcheljs": "^4.3.1" }, "devDependencies": { @@ -3684,6 +3685,32 @@ "graphql": ">=15.1.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "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/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -4046,6 +4073,18 @@ "node": ">=8" } }, + "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/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -4215,6 +4254,69 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4321,6 +4423,12 @@ "@types/react": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -6854,6 +6962,127 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "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/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -6923,6 +7152,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -7233,6 +7468,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -8656,6 +8901,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", @@ -8753,6 +9008,15 @@ "node": ">=12.0.0" } }, + "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/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10836,6 +11100,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "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", @@ -10931,6 +11218,39 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10945,6 +11265,21 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -10998,6 +11333,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "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", @@ -11997,6 +12338,12 @@ "node": ">=16" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -12485,6 +12832,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", diff --git a/frontend/package.json b/frontend/package.json index 279125f..d6340e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,6 @@ "core-js": "^3.38.1", "framer-motion": "^11.16.0", "fuse": "^0.12.1", - "fuse.js": "^7.1.0", "mobx": "^6.13.2", "mobx-react-lite": "^4.1.0", @@ -31,6 +30,7 @@ "react-icons": "^5.4.0", "react-router-dom": "^6.26.2", "react-transition-group": "^4.4.5", + "recharts": "^3.2.1", "satcheljs": "^4.3.1" }, "devDependencies": { diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index ab63fe3..4924989 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -49,6 +49,10 @@ export const updateEndDateFilter = action ( 'updateEndDateFilter', (endDateFilter: Date | null) => ({endDateFilter}) ) +export const updateYearFilter = action ( + 'updateYearFilter', + (yearFilter: number[] | null) => ({yearFilter}) +) /** * Append a new grant to the current list of grants. diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index 561a796..761dc05 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -5,7 +5,7 @@ import { logoutUser, fetchAllGrants, updateFilter, - updateStartDateFilter, updateEndDateFilter + updateStartDateFilter, updateEndDateFilter, updateYearFilter } from './actions'; import { getAppStore } from './store'; @@ -68,3 +68,8 @@ mutator(updateEndDateFilter, (actionMessage) => { const store = getAppStore(); store.endDateFilter = actionMessage.endDateFilter; }) + +mutator(updateYearFilter, (actionMessage) => { + const store = getAppStore(); + store.yearFilter = actionMessage.yearFilter; +}) diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index d873e0e..8b1eaa6 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -12,7 +12,7 @@ export interface AppState { // TODO: should this be the ISODate type? startDateFilter: Date | null; endDateFilter: Date | null; -} + yearFilter:number[] | null;} // Define initial state const initialState: AppState = { @@ -23,6 +23,7 @@ const initialState: AppState = { filterStatus: null, startDateFilter: null, endDateFilter: null, + yearFilter: null, }; const store = createStore('appStore', initialState); diff --git a/frontend/src/main-page/MainPage.tsx b/frontend/src/main-page/MainPage.tsx index 3796071..424deca 100644 --- a/frontend/src/main-page/MainPage.tsx +++ b/frontend/src/main-page/MainPage.tsx @@ -5,6 +5,8 @@ import Header from "./header/Header"; import Users from "./users/Users"; function MainPage() { + + return (
diff --git a/frontend/src/main-page/dashboard/Charts/SampleChart.tsx b/frontend/src/main-page/dashboard/Charts/SampleChart.tsx new file mode 100644 index 0000000..43f0ee5 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/SampleChart.tsx @@ -0,0 +1,52 @@ +import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip } from "recharts"; +import { observer } from "mobx-react-lite"; +import { useProcessGrantData } from "../../../main-page/grants/filter-bar/processGrantData"; +import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; + +const SampleChart: React.FC = observer(() => { + const { grants } = useProcessGrantData(); + const data = aggregateMoneyGrantsByYear(grants, "status").map( + (grant: YearAmount) => ({ + name: grant.year.toString(), + active: grant.Active, + inactive: grant.Inactive, + }) + ); + + return ( +
+ + + + + + + + +
+ ); +}); + +export default SampleChart; diff --git a/frontend/src/main-page/dashboard/Dashboard.tsx b/frontend/src/main-page/dashboard/Dashboard.tsx index 4f7c407..aece437 100644 --- a/frontend/src/main-page/dashboard/Dashboard.tsx +++ b/frontend/src/main-page/dashboard/Dashboard.tsx @@ -1,11 +1,27 @@ + +import DateFilter from "./DateFilter"; import "./styles/Dashboard.css"; +import { observer } from "mobx-react-lite"; +import SampleChart from "./Charts/SampleChart"; +import { useEffect } from "react"; +import { updateYearFilter, updateFilter, updateEndDateFilter, updateStartDateFilter } from "../../external/bcanSatchel/actions"; + +const Dashboard = observer(() => { -function Dashboard() { + // reset filters on initial render + useEffect(() => { + updateYearFilter(null); + updateFilter(null); + updateEndDateFilter(null); + updateStartDateFilter(null); + }, []); return ( -
+
+ +
); -} +}) export default Dashboard; diff --git a/frontend/src/main-page/dashboard/DateFilter.tsx b/frontend/src/main-page/dashboard/DateFilter.tsx new file mode 100644 index 0000000..8ceb9fa --- /dev/null +++ b/frontend/src/main-page/dashboard/DateFilter.tsx @@ -0,0 +1,60 @@ +import { useState, useEffect } from "react"; +import { updateYearFilter } from "../../external/bcanSatchel/actions"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import { observer } from "mobx-react-lite"; + +const DateFilter: React.FC = observer(() => { + const { allGrants, yearFilter } = getAppStore(); + + // Generate unique years dynamically from grants + const uniqueYears = Array.from( + new Set( + allGrants.map((g) => new Date(g.application_deadline).getFullYear()) + ) + ).sort((a, b) => a - b); + + // Initialize selection from store or fallback to all years + const [selectedYears, setSelectedYears] = useState(uniqueYears); + + // Keep local selection in sync if store changes + useEffect(() => { + (yearFilter && yearFilter.length) === 0 + ? setSelectedYears(uniqueYears) + : setSelectedYears(yearFilter ?? uniqueYears); + }, [yearFilter, uniqueYears]); + + // Update local store and state on checkbox change + const handleCheckboxChange = (event: React.ChangeEvent) => { + const year = Number(event.target.value); + const checked = event.target.checked; + + let updatedYears; + if (checked) { + updatedYears = [...selectedYears, year]; + } else { + updatedYears = selectedYears.filter((y) => y !== year); + } + + setSelectedYears(updatedYears); + updateYearFilter(updatedYears); + }; + + return ( +
+ {uniqueYears.map((year) => ( + + ))} +
+ ); +}); + +export default DateFilter; diff --git a/frontend/src/main-page/dashboard/grantCalculations.ts b/frontend/src/main-page/dashboard/grantCalculations.ts new file mode 100644 index 0000000..64cee7d --- /dev/null +++ b/frontend/src/main-page/dashboard/grantCalculations.ts @@ -0,0 +1,68 @@ +import { Grant } from "../../../../middle-layer/types/Grant"; + +export type YearAmount = { + year: number; + [key: string]: number; +}; + +/** + * Aggregates total grant amounts by year, optionally grouped by a secondary key. + * + * @param grants - Array of grants. + * @param groupBy - Optional secondary grouping key (e.g., "status" or "organization"). + * @returns Array of { year, [groupValue]: amount }. + */ +export function aggregateMoneyGrantsByYear( + grants: Grant[], + groupBy?: keyof Grant +): YearAmount[] { + const grouped: Record> = {}; + + for (const grant of grants) { + const year = new Date(grant.application_deadline).getUTCFullYear(); + const groupValue = groupBy ? String(grant[groupBy] ?? "Unknown") : "All"; + + grouped[year] ??= {}; + grouped[year][groupValue] = (grouped[year][groupValue] ?? 0) + grant.amount; + } + + return Object.entries(grouped) + .map(([year, groups]) => ({ + year: Number(year), + ...groups, + })) + .sort((a, b) => a.year - b.year); +} + +/** + * Aggregates distinct grant counts by year, optionally grouped by a secondary key. + * + * @param grants - Array of grants. + * @param groupBy - Optional secondary grouping key (e.g., "status" or "organization"). + * @returns Array of { year, [groupValue]: count }. + */ +export function aggregateCountGrantsByYear( + grants: Grant[], + groupBy?: keyof Grant +): YearAmount[] { + const grouped: Record>> = {}; + + for (const grant of grants) { + const year = new Date(grant.application_deadline).getUTCFullYear(); + const groupValue = groupBy ? String(grant[groupBy] ?? "Unknown") : "All"; + + grouped[year] ??= {}; + grouped[year][groupValue] ??= new Set(); + grouped[year][groupValue].add(grant.grantId); + } + + return Object.entries(grouped) + .map(([year, groups]) => { + const counts: Record = {}; + for (const [key, ids] of Object.entries(groups)) { + counts[key] = ids.size; + } + return { year: Number(year), ...counts }; + }) + .sort((a, b) => a.year - b.year); +} diff --git a/frontend/src/main-page/grants/GrantPage.tsx b/frontend/src/main-page/grants/GrantPage.tsx index 047f8da..64f24a5 100644 --- a/frontend/src/main-page/grants/GrantPage.tsx +++ b/frontend/src/main-page/grants/GrantPage.tsx @@ -4,14 +4,23 @@ import GrantList from "./grant-list/index.tsx"; import AddGrantButton from "./new-grant/AddGrant.tsx"; import GrantSearch from "./filter-bar/GrantSearch.tsx"; import NewGrantModal from "./new-grant/NewGrantModal.tsx"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Grant } from "../../../../middle-layer/types/Grant.ts"; import FilterBar from "./filter-bar/FilterBar.tsx"; +import { updateEndDateFilter, updateFilter, updateStartDateFilter, updateYearFilter } from "../../external/bcanSatchel/actions.ts"; function GrantPage() { const [showNewGrantModal, setShowNewGrantModal] = useState(false); const [selectedGrant, setSelectedGrant] = useState(null); + // reset filters on initial render + useEffect(() => { + updateYearFilter(null); + updateFilter(null); + updateEndDateFilter(null); + updateStartDateFilter(null); + }, []); + return (
diff --git a/frontend/src/main-page/grants/filter-bar/CalendarDropdown.tsx b/frontend/src/main-page/grants/filter-bar/CalendarDropdown.tsx index 5cdff0e..ce2b555 100644 --- a/frontend/src/main-page/grants/filter-bar/CalendarDropdown.tsx +++ b/frontend/src/main-page/grants/filter-bar/CalendarDropdown.tsx @@ -14,7 +14,7 @@ const CalendarDropdown = observer(() => { // ex: Apr 14th, 2025 const formatDate = (date: Date) => - date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); + date.toLocaleDateString("en-US", { month: "numeric", day: "numeric", year: "numeric" }); // state variable not needed since will always rerender with satchel changes let displayText = "Select Date Range"; @@ -26,10 +26,10 @@ const CalendarDropdown = observer(() => { return (
- {isOpen && ( diff --git a/frontend/src/main-page/grants/filter-bar/FilterBar.tsx b/frontend/src/main-page/grants/filter-bar/FilterBar.tsx index 267e93e..423eef8 100644 --- a/frontend/src/main-page/grants/filter-bar/FilterBar.tsx +++ b/frontend/src/main-page/grants/filter-bar/FilterBar.tsx @@ -46,7 +46,7 @@ const FilterBar: React.FC = observer(() => { } return ( -
+
{"Filter by Date"}
diff --git a/frontend/src/main-page/grants/filter-bar/grantFilters.ts b/frontend/src/main-page/grants/filter-bar/grantFilters.ts index 0786a72..d3127a8 100644 --- a/frontend/src/main-page/grants/filter-bar/grantFilters.ts +++ b/frontend/src/main-page/grants/filter-bar/grantFilters.ts @@ -15,4 +15,10 @@ export const dateRangeFilter = (start: Date | null, end: Date | null) => (grant: if (start && date < start) return false; if (end && date > end) return false; return true; -}; \ No newline at end of file +}; + +export const yearFilterer = (years: number[] | null) => (grant: Grant) => { + if (!years) return true; + const grantYear = new Date(grant.application_deadline).getFullYear(); + return years.includes(grantYear); +} \ No newline at end of file diff --git a/frontend/src/main-page/grants/filter-bar/processGrantData.ts b/frontend/src/main-page/grants/filter-bar/processGrantData.ts index 956f6b3..d345103 100644 --- a/frontend/src/main-page/grants/filter-bar/processGrantData.ts +++ b/frontend/src/main-page/grants/filter-bar/processGrantData.ts @@ -1,50 +1,43 @@ -import { useEffect, useState } from "react"; import { getAppStore } from "../../../external/bcanSatchel/store.ts"; import { fetchAllGrants } from "../../../external/bcanSatchel/actions.ts"; import { Grant } from "../../../../../middle-layer/types/Grant.ts"; -import {dateRangeFilter, filterGrants, statusFilter} from "./grantFilters"; +import { dateRangeFilter, filterGrants, statusFilter, yearFilterer } from "./grantFilters"; import { sortGrants } from "./grantSorter.ts"; import { api } from "../../../api.ts"; +import { useEffect } from "react"; -// GET request for all grants +// fetch grants const fetchGrants = async () => { - try { - const response = await api("/grant"); - if (!response.ok) { - throw new Error(`HTTP Error, Status: ${response.status}`); - } - const updatedGrants: Grant[] = await response.json(); - fetchAllGrants(updatedGrants); - } catch (error) { - console.error("Error fetching grants:", error); - } + try { + const response = await api("/grant"); + if (!response.ok) throw new Error(`HTTP Error ${response.status}`); + const updatedGrants: Grant[] = await response.json(); + fetchAllGrants(updatedGrants); + } catch (err) { + console.error(err); + } }; -// contains callbacks for sorting and filtering grants -// stores state for list of grants/filter -export const ProcessGrantData = () => { - const { allGrants, filterStatus, startDateFilter, endDateFilter } = getAppStore(); - const [grants, setGrants] = useState([]); +// Hook to expose filtered/sorted grants +export const useProcessGrantData = () => { + const { allGrants, filterStatus, startDateFilter, endDateFilter, yearFilter } = getAppStore(); - // init grant list - useEffect(() => { - fetchGrants(); - }, []); + // fetch grants on mount if empty + useEffect(() => { + if (allGrants.length === 0) fetchGrants(); + }, [allGrants.length]); - // when filter changes, update grant list state - useEffect(() => { - const filters = [statusFilter(filterStatus), dateRangeFilter(startDateFilter, endDateFilter)] - const filtered = filterGrants(allGrants, filters); - setGrants(filtered); - // current brute force update everything when an attribute changes - }, [allGrants, filterStatus, startDateFilter, endDateFilter]); + // compute filtered grants dynamically — no useState needed + const filteredGrants = filterGrants(allGrants, [ + statusFilter(filterStatus), + dateRangeFilter(startDateFilter, endDateFilter), + yearFilterer(yearFilter), + ]); - // sorts grants based on attribute given, updates grant list state - const onSort = (header: keyof Grant, asc: boolean) => { - const sorted = sortGrants(grants, header, asc); - setGrants(sorted); - }; + // sorting callback + const onSort = (header: keyof Grant, asc: boolean) => { + return sortGrants(filteredGrants, header, asc); + }; - // calculates total # of pages for pagination - return { grants, onSort }; + return { grants: filteredGrants, onSort }; }; diff --git a/frontend/src/main-page/grants/grant-list/index.tsx b/frontend/src/main-page/grants/grant-list/index.tsx index cc48a11..6ec9af6 100644 --- a/frontend/src/main-page/grants/grant-list/index.tsx +++ b/frontend/src/main-page/grants/grant-list/index.tsx @@ -5,7 +5,7 @@ import GrantItem from "./GrantItem.tsx"; import GrantLabels from "./GrantLabels.tsx"; import { ButtonGroup, IconButton, Pagination } from "@chakra-ui/react"; import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; -import { ProcessGrantData } from "../filter-bar/processGrantData.ts"; +import { useProcessGrantData } from "../filter-bar/processGrantData.ts"; import NewGrantModal from "../new-grant/NewGrantModal.tsx"; const ITEMS_PER_PAGE = 6; @@ -16,7 +16,7 @@ interface GrantListProps { } const GrantList: React.FC = observer(({ selectedGrantId, onClearSelectedGrant }) => { - const { grants, onSort } = ProcessGrantData(); + const { grants, onSort } = useProcessGrantData(); const [currentPage, setPage] = useState(1); const [showNewGrantModal, setShowNewGrantModal] = useState(false); diff --git a/frontend/src/main-page/grants/styles/CalendarDropdown.css b/frontend/src/main-page/grants/styles/CalendarDropdown.css index 136c88b..236ff6d 100644 --- a/frontend/src/main-page/grants/styles/CalendarDropdown.css +++ b/frontend/src/main-page/grants/styles/CalendarDropdown.css @@ -7,7 +7,7 @@ align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; - border: 1px solid black; + border: 0.1rem solid black; border-radius: 15px; background-color: white; cursor: pointer; @@ -31,6 +31,6 @@ z-index: 9999; background-color: white; padding: 1rem; - border: 1px solid #ccc; + border: 0.1rem solid #ccc; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); } \ No newline at end of file diff --git a/frontend/src/main-page/grants/styles/GrantAttributes.css b/frontend/src/main-page/grants/styles/GrantAttributes.css index addfe2a..965de9d 100644 --- a/frontend/src/main-page/grants/styles/GrantAttributes.css +++ b/frontend/src/main-page/grants/styles/GrantAttributes.css @@ -22,7 +22,7 @@ .attribute-value { background-color: white; padding: 0.5rem; - border: 1px solid black; + border: 0.1rem solid black; border-radius: 15px; width: 100px; height: 20px; diff --git a/frontend/src/main-page/grants/styles/GrantButton.css b/frontend/src/main-page/grants/styles/GrantButton.css index 0a08cd1..f8ed652 100644 --- a/frontend/src/main-page/grants/styles/GrantButton.css +++ b/frontend/src/main-page/grants/styles/GrantButton.css @@ -5,16 +5,16 @@ border-radius: 15px; padding: 8px 16px; font-size: 16px; - cursor: pointer; height: 42px; - + border: 0.1rem solid; transition: opacity 0.2s ease-in-out; } .add-grant-button { width: 168.42px; font-weight: 600; + border-color: transparent; } diff --git a/frontend/src/main-page/grants/styles/GrantDetails.css b/frontend/src/main-page/grants/styles/GrantDetails.css index 9dae068..7348c54 100644 --- a/frontend/src/main-page/grants/styles/GrantDetails.css +++ b/frontend/src/main-page/grants/styles/GrantDetails.css @@ -12,7 +12,7 @@ } .grant-details p{ - border: 1px solid black; + border: 0.1rem solid black; border-radius: 15px; padding: 1rem; margin-bottom: 1rem; diff --git a/frontend/src/main-page/grants/styles/GrantSearch.css b/frontend/src/main-page/grants/styles/GrantSearch.css index 191b089..85e4f23 100644 --- a/frontend/src/main-page/grants/styles/GrantSearch.css +++ b/frontend/src/main-page/grants/styles/GrantSearch.css @@ -22,13 +22,13 @@ color: black; padding: 8px; border-radius: 15px; - border: 1px solid black; + border: 0.1rem solid black; width: 100%; } .search-input:focus { outline: none !important; - border:1px solid #F58D5C; + border:0.1rem solid #F58D5C; } @@ -42,7 +42,7 @@ left: 0; width: 100%; background: white; - border: 1px solid #ddd; + border: 0.1rem solid #ddd; border-radius: 15px; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); z-index: 10; @@ -53,7 +53,7 @@ .dropdown-item { padding: 8px; cursor: pointer; - border-bottom: 1px solid #eee; + border-bottom: 0.1rem solid #eee; transition: background 0.2s ease; color: black; } diff --git a/frontend/src/main-page/grants/styles/NewGrantModal.css b/frontend/src/main-page/grants/styles/NewGrantModal.css index d0e70a1..ec3d327 100644 --- a/frontend/src/main-page/grants/styles/NewGrantModal.css +++ b/frontend/src/main-page/grants/styles/NewGrantModal.css @@ -91,7 +91,7 @@ padding: 0.6rem; font-size: 1rem; margin-bottom: 0.25rem; - border: 1px solid #ccc; + border: 0.1rem solid #ccc; border-radius: 15px; box-sizing: border-box; background-color: #fff; diff --git a/frontend/src/main-page/header/Header.tsx b/frontend/src/main-page/header/Header.tsx index d36a72b..2478af5 100644 --- a/frontend/src/main-page/header/Header.tsx +++ b/frontend/src/main-page/header/Header.tsx @@ -1,4 +1,3 @@ -import React, { useState } from "react"; import { Link } from "react-router-dom"; import "./styles/Header.css"; import logo from "../../images/bcan_logo.svg"; @@ -14,6 +13,7 @@ import { observer } from "mobx-react-lite"; import { Menu, Button } from "@chakra-ui/react"; import { FaCog } from "react-icons/fa"; import BellButton from "./Bell.tsx"; +import { useLocation } from 'react-router-dom'; interface NavBarProps { name: string; @@ -32,14 +32,12 @@ const linkList: NavBarProps[] = [ * The cog displays a dropdown with "My Account" and "Logout" options. */ const Header: React.FC = observer(() => { - const [selected, setSelected] = useState("All Grants"); function categoryClicked( e: React.MouseEvent, category: string, linkTo?: string ) { - setSelected(category); if (!linkTo) { e.preventDefault(); updateFilter(statusToString(category)); @@ -65,7 +63,7 @@ const Header: React.FC = observer(() => { >
{item.name} diff --git a/frontend/src/main-page/header/styles/Header.css b/frontend/src/main-page/header/styles/Header.css index befa33c..3ba13e3 100644 --- a/frontend/src/main-page/header/styles/Header.css +++ b/frontend/src/main-page/header/styles/Header.css @@ -35,7 +35,7 @@ font-weight: 600; color: black; background-color: #F7A781; - border: 1px black solid; + border: 0.1rem black solid; } .grant-buttons a { diff --git a/frontend/src/styles/Footer.css b/frontend/src/styles/Footer.css index b29dec9..a8d3132 100644 --- a/frontend/src/styles/Footer.css +++ b/frontend/src/styles/Footer.css @@ -19,7 +19,7 @@ flex-grow: 1; .bcan-link { background-color: "#0b303b"; - border: "1px solid #ccc"; + border: "0.1rem solid #ccc"; border-width: 0; border-radius: "100px"; color: "#fff"; diff --git a/frontend/src/styles/button.css b/frontend/src/styles/button.css index 8114ec0..5ecbdbb 100644 --- a/frontend/src/styles/button.css +++ b/frontend/src/styles/button.css @@ -11,7 +11,7 @@ left: 0; z-index: 10; background-color: #fff; - border: 1px solid #ddd; + border: 0.1rem solid #ddd; border-radius: 15px; padding: 0.5rem; display: flex; diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index 996c1c0..b0ec10e 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -41,7 +41,7 @@ h1 { button { border-radius: 15px; - border: 1px solid transparent; + border: 0.1rem solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; diff --git a/frontend/tests/GrantCalculations.test.tsx b/frontend/tests/GrantCalculations.test.tsx new file mode 100644 index 0000000..3743518 --- /dev/null +++ b/frontend/tests/GrantCalculations.test.tsx @@ -0,0 +1,138 @@ +import { describe, it, assert } from "vitest"; +import { Grant } from "../../middle-layer/types/Grant"; +import { + aggregateCountGrantsByYear, + aggregateMoneyGrantsByYear, +} from "../src/main-page/dashboard/grantCalculations"; + +describe("Grant Calculations", () => { + enum Status { + Potential = "Potential", + Active = "Active", + Inactive = "Inactive", + Rejected = "Rejected", + Pending = "Pending", + } + + const mockGrants: Grant[] = [ + { + grantId: 1, + organization: "Test Organization", + does_bcan_qualify: true, + status: Status.Potential, + amount: 1000, + grant_start_date: "2024-01-01", + application_deadline: "2024-01-01", + report_deadlines: ["2025-01-01"], + description: "Test Description", + timeline: 1, + estimated_completion_time: 100, + grantmaker_poc: { POC_name: "name", POC_email: "test@test.com" }, + bcan_poc: { POC_name: "name", POC_email: "" }, + attachments: [], + isRestricted: false, + }, + { + grantId: 2, + organization: "Test Organization 2", + does_bcan_qualify: false, + status: Status.Active, + amount: 2000, + grant_start_date: "2025-02-15", + application_deadline: "2025-02-01", + report_deadlines: ["2025-03-01", "2025-04-01"], + description: "Test Description 2", + timeline: 2, + estimated_completion_time: 300, + bcan_poc: { POC_name: "Allie", POC_email: "allie@gmail.com" }, + grantmaker_poc: { + POC_name: "Benjamin", + POC_email: "benpetrillo@yahoo.com", + }, + attachments: [], + isRestricted: true, + }, + { + grantId: 3, + organization: "Test Organization 2", + does_bcan_qualify: false, + status: Status.Potential, + amount: 2000, + grant_start_date: "2025-02-15", + application_deadline: "2025-02-01", + report_deadlines: ["2025-03-01", "2025-04-01"], + description: "Test Description 2", + timeline: 2, + estimated_completion_time: 300, + bcan_poc: { POC_name: "Allie", POC_email: "allie@gmail.com" }, + grantmaker_poc: { + POC_name: "Benjamin", + POC_email: "benpetrillo@yahoo.com", + }, + attachments: [], + isRestricted: true, + }, + { + grantId: 4, + organization: "Test Organization 2", + does_bcan_qualify: false, + status: Status.Potential, + amount: 2000, + grant_start_date: "2025-02-15", + application_deadline: "2025-02-01", + report_deadlines: ["2025-03-01", "2025-04-01"], + description: "Test Description 2", + timeline: 2, + estimated_completion_time: 300, + bcan_poc: { POC_name: "Allie", POC_email: "allie@gmail.com" }, + grantmaker_poc: { + POC_name: "Benjamin", + POC_email: "benpetrillo@yahoo.com", + }, + attachments: [], + isRestricted: true, + }, + ]; + + it("should aggregate money by year without grouping", () => { + const result = aggregateMoneyGrantsByYear(mockGrants); + assert.deepEqual(result, [ + { year: 2024, All: 1000 }, + { year: 2025, All: 6000 }, + ]); + }); + + it("should aggregate money by year by status", () => { + const result = aggregateMoneyGrantsByYear(mockGrants, "status"); + assert.deepEqual(result, [ + { year: 2024, Potential: 1000 }, + { year: 2025, Active: 2000, Potential: 4000 }, + ]); + }); + + it("should return empty when there is nothing to sum", () => { + const result = aggregateMoneyGrantsByYear([]); + assert.deepEqual(result, []); + }); + + it("should count by year without grouping", () => { + const result = aggregateCountGrantsByYear(mockGrants); + assert.deepEqual(result, [ + { year: 2024, All: 1 }, + { year: 2025, All: 3 }, + ]); + }); + + it("should count by year by status", () => { + const result = aggregateCountGrantsByYear(mockGrants, "status"); + assert.deepEqual(result, [ + { year: 2024, Potential: 1 }, + { year: 2025, Active: 1, Potential: 2 }, + ]); + }); + + it("should return empty when there is nothing to count", () => { + const result = aggregateCountGrantsByYear([]); + assert.deepEqual(result, []); + }); +}); diff --git a/package-lock.json b/package-lock.json index f871b7b..a643330 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "recharts": "^3.2.1", "ts-morph": "^23.0.0", "typescript": "^5.7.3" } @@ -48,6 +49,44 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "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/@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/@ts-morph/common": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz", @@ -60,6 +99,75 @@ "path-browserify": "^1.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/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/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -87,12 +195,164 @@ "node": ">=8" } }, + "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/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "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/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/es-toolkit": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz", + "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -142,6 +402,25 @@ "node": ">= 6" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/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-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -262,6 +541,107 @@ ], "license": "MIT" }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT", + "peer": true + }, + "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/recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/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/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -295,6 +675,19 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -329,6 +722,37 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/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" + } } } } diff --git a/package.json b/package.json index 6bac9cc..aa7149c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "recharts": "^3.2.1", "ts-morph": "^23.0.0", "typescript": "^5.7.3" }, @@ -13,7 +14,6 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "keywords": [], "author": "", "license": "ISC"