diff --git a/package-lock.json b/package-lock.json index d6a33bc..d2cd516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-joyride": "^3.1.0", "react-leaflet": "^5.0.0", "react-router-dom": "^7.14.0", + "recharts": "^2.15.3", "tailwindcss": "^4.2.2" }, "devDependencies": { @@ -3040,6 +3041,69 @@ "tslib": "^2.4.0" } }, + "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.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "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.9", "dev": true, @@ -3710,6 +3774,15 @@ "node": ">=12" } }, + "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", "dev": true, @@ -3859,6 +3932,127 @@ "version": "3.2.3", "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.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "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-view-buffer": { "version": "1.0.2", "dev": true, @@ -3923,6 +4117,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-is": { "version": "0.1.4", "dev": true, @@ -3975,6 +4175,16 @@ "node": ">=8" } }, + "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/dompurify": { "version": "3.4.6", "license": "(MPL-2.0 OR Apache-2.0)", @@ -4353,11 +4563,26 @@ "url": "https://github.com/bgub/eta?sponsor=1" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, "license": "MIT" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, @@ -4972,6 +5197,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "dev": true, @@ -5401,7 +5635,6 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5762,6 +5995,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "dev": true, @@ -5781,6 +6020,18 @@ "version": "5.3.2", "license": "Apache-2.0" }, + "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/lru-cache": { "version": "5.1.1", "dev": true, @@ -5872,6 +6123,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/object-inspect": { "version": "1.13.4", "dev": true, @@ -6144,6 +6404,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/protobufjs": { "version": "7.6.1", "hasInstallScript": true, @@ -6245,6 +6522,12 @@ "react": ">=0.0.0 <=99" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/react-joyride": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-3.1.0.tgz", @@ -6313,6 +6596,70 @@ "react-dom": ">=18" } }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "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/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/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "dev": true, @@ -7038,6 +7385,12 @@ "node": ">=10" } }, + "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/tinyglobby": { "version": "0.2.15", "license": "MIT", @@ -7338,6 +7691,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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": "8.0.3", "license": "MIT", diff --git a/package.json b/package.json index 6e9ef00..a14fa01 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react-joyride": "^3.1.0", "react-leaflet": "^5.0.0", "react-router-dom": "^7.14.0", + "recharts": "^2.15.3", "tailwindcss": "^4.2.2" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 893926b..06f3c5c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ import PostHogPageView from './components/PostHogPageView'; import NotFound from './pages/NotFound'; import InstallPrompt from './components/InstallPrompt'; import PublicReport from "./pages/PublicReport"; +import AnalyticsPage from "./pages/AnalyticsPage"; export default function App() { const [runTour, setRunTour] = useState(false); @@ -48,6 +49,7 @@ useEffect(() => { } /> } /> } /> + } /> } /> } /> diff --git a/src/i18n/locales/bn.json b/src/i18n/locales/bn.json index b0cc658..7ab4630 100644 --- a/src/i18n/locales/bn.json +++ b/src/i18n/locales/bn.json @@ -368,5 +368,23 @@ }, "unknown": "একটি অপ্রত্যাশিত ত্রুটি ঘটেছে। অনুগ্রহ করে আবার চেষ্টা করুন।", "serverError": "সার্ভার ত্রুটি। অনুগ্রহ করে পরে আবার চেষ্টা করুন।" + }, + "analytics": { + "totalScans": "মোট স্ক্যান", + "avgFreshness": "গড় তাজা", + "freshRate": "তাজা হার", + "gradeDistribution": "গ্রেড বিতরণ", + "scanVolume": "স্ক্যান পরিমাণ", + "freshnessTrend": "তাজা প্রবণতা", + "backToResults": "ফলাফলে ফিরে যান", + "runAScan": "স্ক্যান চালান", + "loadingAnalytics": "বিশ্লেষণ লোড হচ্ছে...", + "crunchingData": "স্ক্যান ডেটা প্রক্রিয়াকরণ হচ্ছে...", + "failedToLoad": "বিশ্লেষণ লোড করতে ব্যর্থ", + "noScansInRange": "এই সময়ের মধ্যে কোনো স্ক্যান নেই", + "tryDifferentFilter": "ভিন্ন ফিল্টার চেষ্টা করুন", + "days7": "৭ দিন", + "days30": "৩০ দিন", + "allTime": "সব সময়" } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 514762a..952bbb0 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -374,5 +374,23 @@ }, "unknown": "An unexpected error occurred. Please try again.", "serverError": "Server error. Please try again later." + }, + "analytics": { + "totalScans": "Total Scans", + "avgFreshness": "Avg Freshness", + "freshRate": "Fresh Rate", + "gradeDistribution": "Grade Distribution", + "scanVolume": "Scan Volume", + "freshnessTrend": "Freshness Trend", + "backToResults": "Back to Results", + "runAScan": "Run a Scan", + "loadingAnalytics": "Loading analytics...", + "crunchingData": "Crunching scan data...", + "failedToLoad": "Failed to load analytics", + "noScansInRange": "No scans in this time range", + "tryDifferentFilter": "Try a different filter", + "days7": "7 Days", + "days30": "30 Days", + "allTime": "All Time" } } diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index f34a7ea..8acce50 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -368,5 +368,23 @@ }, "unknown": "एक अप्रत्याशित त्रुटि हुई। कृपया पुनः प्रयास करें।", "serverError": "सर्वर त्रुटि। कृपया बाद में पुनः प्रयास करें।" + }, + "analytics": { + "totalScans": "कुल स्कैन", + "avgFreshness": "औसत ताजगी", + "freshRate": "ताजगी दर", + "gradeDistribution": "ग्रेड वितरण", + "scanVolume": "स्कैन मात्रा", + "freshnessTrend": "ताजगी रुझान", + "backToResults": "परिणामों पर वापस", + "runAScan": "स्कैन चलाएं", + "loadingAnalytics": "विश्लेषण लोड हो रहा है...", + "crunchingData": "स्कैन डेटा प्रोसेस हो रहा है...", + "failedToLoad": "विश्लेषण लोड करने में विफल", + "noScansInRange": "इस समय सीमा में कोई स्कैन नहीं", + "tryDifferentFilter": "अलग फ़िल्टर आज़माएं", + "days7": "7 दिन", + "days30": "30 दिन", + "allTime": "सभी समय" } } diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx new file mode 100644 index 0000000..c2284c6 --- /dev/null +++ b/src/pages/AnalyticsPage.tsx @@ -0,0 +1,351 @@ +import { useEffect, useState, useMemo } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ArrowLeft, BarChart3, TrendingUp, PieChart as PieIcon } from 'lucide-react'; +import { + PieChart, Pie, Cell, LineChart, Line, BarChart, Bar, + XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend +} from 'recharts'; +import GlassCard from '../components/GlassCard'; +import StatusTerminal from '../components/StatusTerminal'; +import { api } from '../lib/api'; +import type { HistoryScan } from '../lib/types'; + +type TimeRange = '7d' | '30d' | 'all'; + +const GRADE_COLORS: Record = { + 'A+': '#22c55e', + A: '#22c55e', + B: '#00e5ff', + C: '#f59e0b', + D: '#ef4444', + F: '#ef4444', +}; + +const PIE_COLORS = ['#22c55e', '#00e5ff', '#f59e0b', '#ef4444']; + +const tooltipStyle = { + background: 'var(--color-surface-mid)', + border: '1px solid var(--ghost-border-color)', + borderRadius: 0, + fontFamily: 'var(--font-mono)', + fontSize: '0.625rem', + color: 'var(--color-on-surface)', +}; + +const axisTickStyle = { + fill: 'var(--color-on-surface-variant)', + fontFamily: 'var(--font-mono)', + fontSize: 10, +}; + +const axisLineStyle = { + stroke: 'var(--ghost-border-color)', +}; + +function formatDate(ts: string) { + const d = new Date(ts); + return `${d.getDate()}/${d.getMonth() + 1}`; +} + +export default function AnalyticsPage() { + const { t } = useTranslation(); + const [scans, setScans] = useState([]); + const [loading, setLoading] = useState(true); + const [errorKey, setErrorKey] = useState(''); + const [timeRange, setTimeRange] = useState('all'); + + useEffect(() => { + async function load() { + setLoading(true); + try { + const res = await api.getScanHistory(100, 0); + setScans(res.scans); + } catch (err) { + if (err instanceof Error && err.message.startsWith('error.')) { + setErrorKey(err.message); + } else { + setErrorKey('results.failedToLoadHistory'); + } + } finally { + setLoading(false); + } + } + load(); + }, []); + + const filteredScans = useMemo(() => { + if (timeRange === 'all') return scans; + const now = Date.now(); + const days = timeRange === '7d' ? 7 : 30; + const cutoff = now - days * 24 * 60 * 60 * 1000; + return scans.filter(s => s.timestamp && new Date(s.timestamp).getTime() >= cutoff); + }, [scans, timeRange]); + + // Grade distribution for pie chart + const gradeData = useMemo(() => { + const counts: Record = {}; + filteredScans.forEach(s => { + const g = s.grade || 'C'; + counts[g] = (counts[g] || 0) + 1; + }); + return Object.entries(counts) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); + }, [filteredScans]); + + // Freshness over time for line chart + const freshnessData = useMemo(() => { + const byDate: Record = {}; + filteredScans.forEach(s => { + if (!s.timestamp) return; + const key = formatDate(s.timestamp); + if (!byDate[key]) byDate[key] = { sum: 0, count: 0 }; + byDate[key].sum += s.freshness_index; + byDate[key].count += 1; + }); + return Object.entries(byDate) + .map(([date, { sum, count }]) => ({ + date, + avgFreshness: Math.round(sum / count), + })) + .sort((a, b) => a[0].localeCompare(b[0])) + .reverse(); + }, [filteredScans]); + + // Scan volume per day for bar chart + const volumeData = useMemo(() => { + const counts: Record = {}; + filteredScans.forEach(s => { + if (!s.timestamp) return; + const key = formatDate(s.timestamp); + counts[key] = (counts[key] || 0) + 1; + }); + return Object.entries(counts) + .map(([date, scans]) => ({ date, scans })) + .sort((a, b) => a[0].localeCompare(b[0])) + .reverse(); + }, [filteredScans]); + + const totalScans = filteredScans.length; + const avgScore = totalScans + ? Math.round(filteredScans.reduce((a, s) => a + s.freshness_index, 0) / totalScans) + : 0; + const freshRate = totalScans + ? Math.round((filteredScans.filter(s => s.is_fresh).length / totalScans) * 100) + : 0; + + if (loading) { + return ( +
+ +
+ ); + } + + if (errorKey) { + return ( +
+ +

+ {t(errorKey)} +

+ + {t('analytics.backToResults')} + +
+ ); + } + + return ( +
+
+ {/* Back */} + + + {t('analytics.backToResults')} + + + {/* Header */} +
+
+

+ Scan Analytics +

+

+ Freshness trends and grade distribution across your scans +

+
+ + {/* Time range filter */} +
+ {([ + { key: '7d', label: t('analytics.days7') }, + { key: '30d', label: t('analytics.days30') }, + { key: 'all', label: t('analytics.allTime') }, + ] as const).map(opt => ( + + ))} +
+
+ + {/* Summary stats */} +
+ + + {t('analytics.totalScans')} + + + {totalScans} + + + + + {t('analytics.avgFreshness')} + + + {avgScore} + + + + + {t('analytics.freshRate')} + + + {freshRate}% + + +
+ + {totalScans === 0 ? ( +
+ + + {t('analytics.runAScan')} + +
+ ) : ( +
+ {/* Grade Distribution Pie Chart */} + +
+ +

+ {t('analytics.gradeDistribution')} +

+
+ + + `${name} ${(percent * 100).toFixed(0)}%`} + > + {gradeData.map((entry, idx) => ( + + ))} + + + + + +
+ + {/* Scan Volume Bar Chart */} + +
+ +

+ {t('analytics.scanVolume')} +

+
+ + + + + + + + + +
+ + {/* Freshness Over Time — full width */} + +
+ +

+ {t('analytics.freshnessTrend')} +

+
+ + + + + + + + + +
+
+ )} +
+
+ ); +} diff --git a/src/pages/ResultsPage.tsx b/src/pages/ResultsPage.tsx index cdcfff6..2940b42 100644 --- a/src/pages/ResultsPage.tsx +++ b/src/pages/ResultsPage.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowRight } from 'lucide-react'; +import { BarChart3 } from 'lucide-react'; import GlassCard from '../components/GlassCard'; import StatusTerminal from '../components/StatusTerminal'; import { api } from '../lib/api'; @@ -115,6 +116,17 @@ export default function ResultsPage() { + {/* Analytics link */} + {totalScans > 0 && ( + + + View Analytics Dashboard + + )} + {/* History list */} {scans.length === 0 ? (