From 74ca58a91a4bac579fb5c62ea9eeb77f025f76bd Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Mon, 22 Dec 2025 20:20:03 -0600 Subject: [PATCH] Metrics Working --- bun.lock | 81 ++++++++ package.json | 3 +- src/components/Metrics.css | 305 ++++++++++++++++++++++++++- src/components/Metrics.jsx | 407 ++++++++++++++++++++++++++++++++++++- 4 files changed, 780 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index 806a6c4..b84d087 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "lucide-react": "^0.562.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "recharts": "^3.6.0", }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -150,6 +151,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "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" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], @@ -196,6 +199,10 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -204,6 +211,24 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -212,6 +237,8 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -238,6 +265,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -250,12 +279,38 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "es-toolkit": ["es-toolkit@1.43.0", "", {}, "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -282,6 +337,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -316,10 +373,14 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -388,8 +449,20 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], + + "react-redux": ["react-redux@9.2.0", "", { "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" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "recharts": ["recharts@3.6.0", "", { "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" }, "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" } }, "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], @@ -408,6 +481,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -416,6 +491,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "victory-vendor": ["victory-vendor@37.3.6", "", { "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" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -433,5 +512,7 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@reduxjs/toolkit/immer": ["immer@11.1.0", "", {}, "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g=="], } } diff --git a/package.json b/package.json index cbc3c1c..0194e28 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "i": "^0.3.7", "lucide-react": "^0.562.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "recharts": "^3.6.0" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/src/components/Metrics.css b/src/components/Metrics.css index b42e267..36fdc15 100644 --- a/src/components/Metrics.css +++ b/src/components/Metrics.css @@ -1,10 +1,14 @@ .metrics-container { - max-width: 1200px; + max-width: 1400px; margin: 0 auto; } .metrics-header { - margin-bottom: 2rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: start; + gap: 2rem; } .metrics-header h2 { @@ -22,20 +26,305 @@ font-size: 0.875rem; } -.metrics-placeholder { +.filter-toggle-btn { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: #7c3aed; + color: white; + padding: 0.625rem 1.25rem; + border: none; + border-radius: 0.375rem; + font-family: "JetBrains Mono", monospace; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.filter-toggle-btn:hover { + background-color: #6d28d9; +} + +.filters-panel { + background-color: #2b2b2b; + border: 1px solid #374151; + border-radius: 0.5rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.filter-section { + margin-bottom: 1.5rem; +} + +.filter-section:last-child { + margin-bottom: 0; +} + +.filter-section h3 { + color: #d8b4fe; + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.75rem; +} + +.filter-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.filter-actions { + display: flex; + gap: 0.5rem; +} + +.filter-action-btn { + background: none; + border: 1px solid #374151; + color: #9ca3af; + padding: 0.25rem 0.75rem; + border-radius: 0.25rem; + font-family: "JetBrains Mono", monospace; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.filter-action-btn:hover { + border-color: #7c3aed; + color: #d8b4fe; +} + +.filter-options { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.filter-options-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.5rem; +} + +.filter-checkbox { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.filter-checkbox:hover { + background-color: rgba(124, 58, 237, 0.1); +} + +.filter-checkbox input[type="checkbox"] { + cursor: pointer; + width: 16px; + height: 16px; +} + +.filter-checkbox span { + color: #e0e0e0; + font-size: 0.875rem; +} + +.metrics-loading, +.metrics-error, +.metrics-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 6rem 2rem; - background-color: #2b2b2b; - border: 2px dashed #374151; - border-radius: 0.5rem; + padding: 4rem 2rem; color: #9ca3af; gap: 1rem; } +.metrics-hint { + color: #6b7280; + font-size: 0.875rem; +} + +.spinner { + width: 48px; + height: 48px; + border: 4px solid #374151; + border-top-color: #a78bfa; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.retry-btn { + background-color: #7c3aed; + color: white; + padding: 0.5rem 1.5rem; + border: none; + border-radius: 0.375rem; + font-family: "JetBrains Mono", monospace; + cursor: pointer; + transition: background-color 0.2s; +} + +.retry-btn:hover { + background-color: #6d28d9; +} + .placeholder-icon { font-size: 4rem; opacity: 0.5; -} \ No newline at end of file +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(550px, 1fr)); + gap: 1.5rem; +} + +.metric-card { + background-color: #2b2b2b; + border: 1px solid #374151; + border-radius: 0.5rem; + padding: 1.25rem; + transition: all 0.2s; +} + +.metric-card:hover { + border-color: #7c3aed; + box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2); +} + +.metric-card-header { + margin-bottom: 1rem; +} + +.metric-class-name { + color: #d8b4fe; + font-weight: 600; + font-size: 1.125rem; + margin-bottom: 0.25rem; +} + +.metric-class-info { + color: #9ca3af; + font-size: 0.75rem; +} + +.chart-container { + margin: 1rem 0; + background-color: #1e1e1e; + border-radius: 0.375rem; + padding: 1rem 0.5rem; +} + +.nw-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid #374151; +} + +.nw-stat { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.nw-stat-header { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.category-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.nw-name { + color: #9ca3af; + font-size: 0.75rem; + font-weight: 600; +} + +.nw-stat-data { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.nw-grade { + color: #4ade80; + font-size: 1.125rem; + font-weight: 700; +} + +.nw-trend { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + font-weight: 600; +} + +.trend-up { + color: #4ade80; +} + +.trend-down { + color: #f87171; +} + +.trend-none { + color: #9ca3af; +} + +.nw-updates { + color: #6b7280; + font-size: 0.7rem; +} + +@media (max-width: 1200px) { + .metrics-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .metrics-header { + flex-direction: column; + gap: 1rem; + } + + .filter-toggle-btn { + width: 100%; + justify-content: center; + } + + .filter-options-grid { + grid-template-columns: 1fr; + } + + .nw-summary { + grid-template-columns: 1fr; + } + + .chart-container { + padding: 0.5rem 0.25rem; + } +} diff --git a/src/components/Metrics.jsx b/src/components/Metrics.jsx index f517398..cba5863 100644 --- a/src/components/Metrics.jsx +++ b/src/components/Metrics.jsx @@ -1,17 +1,410 @@ +import { useState, useEffect } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { TrendingUp, TrendingDown, Minus, Filter } from 'lucide-react'; +import { api } from '../services/api'; +import { useAuth } from '../contexts/AuthContext'; import './Metrics.css'; export const Metrics = () => { + const { user } = useAuth(); + const [finalGrades, setFinalGrades] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedPeriods, setSelectedPeriods] = useState(['NW1', 'NW2', 'NW3', 'NW4']); + const [selectedClasses, setSelectedClasses] = useState([]); + const [showFilters, setShowFilters] = useState(false); + + useEffect(() => { + fetchFinalGrades(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const fetchFinalGrades = async () => { + setLoading(true); + setError(null); + try { + const response = await api.getFinalGrades(user.username); + const grades = Array.isArray(response) ? response : response.finalGrades || []; + setFinalGrades(grades); + + // Auto-select all classes initially + const uniqueClasses = [...new Set(grades.map(g => `${g.class.className}-${g.class.period}`))]; + setSelectedClasses(uniqueClasses); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + // Group grades by class and organize by nine-weeks periods + const groupGradesByClass = () => { + const grouped = {}; + + // Only consider NW periods, ignore SE/S1/S2/FIN + const validCategories = ['NW1', 'NW2', 'NW3', 'NW4']; + + finalGrades.forEach(grade => { + const category = grade.class.category; + + // Skip non-NW categories + if (!validCategories.includes(category)) return; + + const classKey = `${grade.class.className}-${grade.class.period}`; + + if (!grouped[classKey]) { + grouped[classKey] = { + className: grade.class.className, + period: grade.class.period, + teacher: grade.class.teacher, + classId: grade.classId, + nineWeeks: { + NW1: [], + NW2: [], + NW3: [], + NW4: [] + } + }; + } + + grouped[classKey].nineWeeks[category].push({ + grade: parseFloat(grade.grade.replace('%', '')), + date: new Date(grade.updatedAt), + timestamp: new Date(grade.updatedAt).getTime() + }); + }); + + // Sort each nine-weeks period by timestamp (chronological) + Object.values(grouped).forEach(classData => { + Object.keys(classData.nineWeeks).forEach(nw => { + classData.nineWeeks[nw].sort((a, b) => a.timestamp - b.timestamp); + }); + }); + + return grouped; + }; + + // Filter classes based on selection + const filterClasses = (grouped) => { + const filtered = {}; + + Object.entries(grouped).forEach(([key, classData]) => { + if (!selectedClasses.includes(key)) return; + + // Filter nine-weeks periods + const filteredNW = {}; + selectedPeriods.forEach(period => { + if (classData.nineWeeks[period] && classData.nineWeeks[period].length > 0) { + filteredNW[period] = classData.nineWeeks[period]; + } + }); + + if (Object.keys(filteredNW).length > 0) { + filtered[key] = { + ...classData, + nineWeeks: filteredNW + }; + } + }); + + return filtered; + }; + + // Prepare chart data + const prepareChartData = (nineWeeks) => { + const maxLength = Math.max( + ...Object.values(nineWeeks).map(arr => arr.length) + ); + + const chartData = []; + for (let i = 0; i < maxLength; i++) { + const dataPoint = { updateNumber: i + 1 }; + + Object.keys(nineWeeks).forEach(nw => { + dataPoint[nw] = nineWeeks[nw][i]?.grade || null; + }); + + chartData.push(dataPoint); + } + + return chartData; + }; + + // Calculate Y-axis domain based on actual data + const calculateYDomain = (nineWeeks) => { + const allGrades = Object.values(nineWeeks) + .flat() + .map(g => g.grade) + .filter(g => g !== null && g !== undefined); + + if (allGrades.length === 0) return [0, 100]; + + const min = Math.min(...allGrades); + const max = Math.max(...allGrades); + const padding = (max - min) * 0.1 || 5; + + return [ + Math.max(0, Math.floor(min - padding)), + Math.min(100, Math.ceil(max + padding)) + ]; + }; + + const calculateTrend = (gradeData) => { + if (gradeData.length < 2) return { trend: 'none', change: 0 }; + + const first = gradeData[0].grade; + const last = gradeData[gradeData.length - 1].grade; + const change = last - first; + + return { + trend: change > 0 ? 'up' : change < 0 ? 'down' : 'none', + change: change + }; + }; + + const getTrendIcon = (trend) => { + if (trend === 'up') return ; + if (trend === 'down') return ; + return ; + }; + + const getLatestGrade = (gradeData) => { + if (gradeData.length === 0) return null; + return gradeData[gradeData.length - 1].grade.toFixed(2); + }; + + const COLORS = { + NW1: '#8b5cf6', + NW2: '#ec4899', + NW3: '#3b82f6', + NW4: '#f43f5e' + }; + + const PERIOD_NAMES = { + NW1: '1st Nine Weeks', + NW2: '2nd Nine Weeks', + NW3: '3rd Nine Weeks', + NW4: '4th Nine Weeks' + }; + + const togglePeriod = (period) => { + setSelectedPeriods(prev => + prev.includes(period) + ? prev.filter(p => p !== period) + : [...prev, period] + ); + }; + + const toggleClass = (classKey) => { + setSelectedClasses(prev => + prev.includes(classKey) + ? prev.filter(c => c !== classKey) + : [...prev, classKey] + ); + }; + + const selectAllClasses = () => { + const allClasses = Object.keys(groupGradesByClass()); + setSelectedClasses(allClasses); + }; + + const deselectAllClasses = () => { + setSelectedClasses([]); + }; + + if (loading) { + return ( +
+
+

Loading metrics...

+
+ ); + } + + if (error) { + return ( +
+

Error loading metrics: {error}

+ +
+ ); + } + + const groupedGrades = groupGradesByClass(); + const filteredGrades = filterClasses(groupedGrades); + const allClasses = Object.keys(groupedGrades); + return (
-

Metrics

-

Coming soon...

-
- -
-
📊
-

Grade analytics and insights will appear here

+
+

Grade Metrics

+

Track your nine-weeks grade trends over time

+
+ +
+ + {showFilters && ( +
+
+

Nine-Weeks Periods

+
+ {['NW1', 'NW2', 'NW3', 'NW4'].map(period => ( + + ))} +
+
+ +
+
+

Classes ({selectedClasses.length}/{allClasses.length})

+
+ + +
+
+
+ {allClasses.map(classKey => { + const classData = groupedGrades[classKey]; + return ( + + ); + })} +
+
+
+ )} + + {Object.keys(filteredGrades).length === 0 ? ( +
+
📊
+

No grade data available for the selected filters

+

Try selecting different periods or classes

+
+ ) : ( +
+ {Object.entries(filteredGrades).map(([key, classData]) => { + const chartData = prepareChartData(classData.nineWeeks); + const yDomain = calculateYDomain(classData.nineWeeks); + + return ( +
+
+
+

{classData.className}

+

+ {classData.period} • {classData.teacher} +

+
+
+ +
+ + + + + + [ + value ? `${value.toFixed(2)}%` : 'N/A', + PERIOD_NAMES[name] + ]} + labelFormatter={(label) => `Update #${label}`} + /> + PERIOD_NAMES[value]} + /> + {Object.keys(classData.nineWeeks).map(nw => ( + + ))} + + +
+ +
+ {Object.entries(classData.nineWeeks).map(([nw, grades]) => { + const latest = getLatestGrade(grades); + const trend = calculateTrend(grades); + return ( +
+
+ + {PERIOD_NAMES[nw]} +
+
+ {latest}% +
+ {getTrendIcon(trend.trend)} + + {Math.abs(trend.change).toFixed(2)}% + +
+ ({grades.length} updates) +
+
+ ); + })} +
+
+ ); + })} +
+ )}
); }; \ No newline at end of file