diff --git a/package-lock.json b/package-lock.json index dbb69cf..670555e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,8 @@ "framer-motion": "^12.23.24", "highcharts": "^12.4.0", "highcharts-react-official": "^3.2.2", - "html2canvas": "^1.4.1", "i": "^0.3.7", - "jspdf": "^3.0.3", + "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.2", "lucide-react": "^0.515.0", "npm": "^11.5.2", @@ -104,6 +103,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -420,6 +420,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -463,6 +464,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1334,6 +1336,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz", "integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.3", "@mui/core-downloads-tracker": "^7.3.2", @@ -1444,6 +1447,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.2.tgz", "integrity": "sha512-9d8JEvZW+H6cVkaZ+FK56R53vkJe3HsTpcjMUtH8v1xK6Y1TjzHdZ7Jck02mGXJsE6MQGWVs3ogRHTQmS9Q/rA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.3", "@mui/private-theming": "^7.3.2", @@ -3061,6 +3065,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3071,6 +3076,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3437,6 +3443,7 @@ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", + "optional": true, "engines": { "node": ">= 0.6.0" } @@ -3481,6 +3488,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -3646,6 +3654,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -3751,12 +3760,16 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/core-js": { @@ -3827,6 +3840,7 @@ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -4094,6 +4108,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -4103,7 +4118,8 @@ "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -4228,9 +4244,9 @@ } }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -4558,6 +4574,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5270,7 +5287,8 @@ "version": "12.4.0", "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.4.0.tgz", "integrity": "sha512-o6UxxfChSUrvrZUbWrAuqL1HO/+exhAUPcZY6nnqLsadZQlnP16d082sg7DnXKZCk1gtfkyfkp6g3qkIZ9miZg==", - "license": "https://www.highcharts.com/license" + "license": "https://www.highcharts.com/license", + "peer": true }, "node_modules/highcharts-react-official": { "version": "3.2.2", @@ -5302,6 +5320,7 @@ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "license": "MIT", + "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -5910,12 +5929,13 @@ } }, "node_modules/jspdf": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", - "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.26.9", + "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, @@ -5927,12 +5947,12 @@ } }, "node_modules/jspdf-autotable": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", - "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.7.tgz", + "integrity": "sha512-2wr7H6liNDBYNwt25hMQwXkEWFOEopgKIvR1Eukuw6Zmprm/ZcnmLTQEjW7Xx3FCbD3v7pflLcnMAv/h1jFDQw==", "license": "MIT", "peerDependencies": { - "jspdf": "^2 || ^3" + "jspdf": "^2 || ^3 || ^4" } }, "node_modules/jsx-ast-utils": { @@ -6243,9 +6263,9 @@ } }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -7410,9 +7430,9 @@ "license": "MIT" }, "node_modules/npm": { - "version": "11.6.4", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.6.4.tgz", - "integrity": "sha512-ERjKtGoFpQrua/9bG0+h3xiv/4nVdGViCjUYA1AmlV24fFvfnSB7B7dIfZnySQ1FDLd0ZVrWPsLLp78dCtJdRQ==", + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.8.0.tgz", + "integrity": "sha512-n19sJeW+RGKdkHo8SCc5xhSwkKhQUFfZaFzSc+EsYXLjSqIV0tl72aDYQVuzVvfrbysGwdaQsNLNy58J10EBSQ==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -7491,8 +7511,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.8", - "@npmcli/config": "^10.4.4", + "@npmcli/arborist": "^9.1.10", + "@npmcli/config": "^10.5.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -7500,7 +7520,7 @@ "@npmcli/promise-spawn": "^9.0.1", "@npmcli/redact": "^4.0.0", "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.0", + "@sigstore/tuf": "^4.0.1", "abbrev": "^4.0.0", "archy": "~1.0.0", "cacache": "^20.0.3", @@ -7517,11 +7537,11 @@ "is-cidr": "^6.0.1", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.11", - "libnpmexec": "^10.1.10", - "libnpmfund": "^7.0.11", + "libnpmdiff": "^8.0.13", + "libnpmexec": "^10.1.12", + "libnpmfund": "^7.0.13", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.11", + "libnpmpack": "^9.0.13", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", @@ -7550,11 +7570,11 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.0", "supports-color": "^10.2.2", - "tar": "^7.5.2", + "tar": "^7.5.4", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", - "validate-npm-package-name": "^7.0.0", + "validate-npm-package-name": "^7.0.2", "which": "^6.0.0" }, "bin": { @@ -7616,7 +7636,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.8", + "version": "9.1.10", "inBundle": true, "license": "ISC", "dependencies": { @@ -7633,7 +7653,7 @@ "@npmcli/run-script": "^10.0.0", "bin-links": "^6.0.0", "cacache": "^20.0.1", - "common-ancestor-path": "^1.0.1", + "common-ancestor-path": "^2.0.0", "hosted-git-info": "^9.0.0", "json-stringify-nice": "^1.1.4", "lru-cache": "^11.2.1", @@ -7662,7 +7682,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.4", + "version": "10.5.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -7843,7 +7863,7 @@ } }, "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "engines": { @@ -7859,48 +7879,40 @@ } }, "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", + "version": "4.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", - "proc-log": "^5.0.0", + "make-fetch-happen": "^15.0.3", + "proc-log": "^6.1.0", "promise-retry": "^2.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", + "version": "4.0.1", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" + "tuf-js": "^4.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", + "version": "3.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { @@ -7916,31 +7928,17 @@ } }, "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" + "minimatch": "^10.1.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm/node_modules/abbrev": { "version": "4.0.0", "inBundle": true, @@ -7975,11 +7973,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/bin-links": { "version": "6.0.0", "inBundle": true, @@ -8006,14 +7999,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm/node_modules/cacache": { "version": "20.0.3", "inBundle": true, @@ -8100,9 +8085,12 @@ } }, "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", + "version": "2.0.0", "inBundle": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } }, "node_modules/npm/node_modules/cssesc": { "version": "3.0.0", @@ -8132,7 +8120,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.2", + "version": "8.0.3", "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -8308,7 +8296,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", + "version": "10.1.0", "inBundle": true, "license": "MIT", "engines": { @@ -8400,11 +8388,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.11", + "version": "8.0.13", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.1.10", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -8418,11 +8406,11 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.10", + "version": "10.1.12", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.1.10", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -8440,11 +8428,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.11", + "version": "7.0.13", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8" + "@npmcli/arborist": "^9.1.10" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -8463,11 +8451,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.11", + "version": "9.0.13", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.1.8", + "@npmcli/arborist": "^9.1.10", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -8533,9 +8521,9 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", + "version": "11.2.4", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -8916,7 +8904,7 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8931,7 +8919,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -9060,16 +9048,16 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", + "@sigstore/core": "^3.1.0", "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.0.0", - "@sigstore/tuf": "^4.0.0", - "@sigstore/verify": "^3.0.0" + "@sigstore/sign": "^4.1.0", + "@sigstore/tuf": "^4.0.1", + "@sigstore/verify": "^3.1.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -9194,7 +9182,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.2", + "version": "7.5.4", "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9261,6 +9249,7 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9277,13 +9266,13 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", + "version": "4.1.0", "inBundle": true, "license": "MIT", "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -9335,7 +9324,7 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "7.0.0", + "version": "7.0.2", "inBundle": true, "license": "ISC", "engines": { @@ -9678,9 +9667,9 @@ } }, "node_modules/preact": { - "version": "10.27.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz", - "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", "funding": { "type": "opencollective", @@ -9753,6 +9742,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9787,6 +9777,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9836,7 +9827,8 @@ "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-kapsule": { "version": "2.5.7", @@ -9895,6 +9887,7 @@ "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", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9971,9 +9964,9 @@ } }, "node_modules/react-router": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", - "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -9993,12 +9986,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", - "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", "license": "MIT", "dependencies": { - "react-router": "7.9.1" + "react-router": "7.13.0" }, "engines": { "node": ">=20.0.0" @@ -10134,7 +10127,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -11056,9 +11050,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/set-function-length": { @@ -11428,6 +11422,7 @@ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -11456,7 +11451,8 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -11481,10 +11477,10 @@ } }, "node_modules/tar": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.4.tgz", - "integrity": "sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==", - "license": "ISC", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -11510,6 +11506,7 @@ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", "license": "MIT", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -11518,7 +11515,8 @@ "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/three-forcegraph": { "version": "1.43.0", @@ -11625,6 +11623,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12011,6 +12010,7 @@ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", "license": "MIT", + "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -12116,6 +12116,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -12207,6 +12208,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index cd871a9..d21f1b3 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ "highcharts": "^12.4.0", "highcharts-react-official": "^3.2.2", "i": "^0.3.7", - "lucide-react": "^0.515.0", - "jspdf": "^3.0.3", + "jspdf": "^4.0.0", "jspdf-autotable": "^5.0.2", + "lucide-react": "^0.515.0", "npm": "^11.5.2", "react": "^19.2.1", "react-chartjs-2": "^5.3.0", @@ -74,4 +74,4 @@ "tw-animate-css": "^1.4.0", "vite": "^6.3.5" } -} \ No newline at end of file +} diff --git a/src/components/version.jsx b/src/components/version.jsx index 139b8de..eb45143 100644 --- a/src/components/version.jsx +++ b/src/components/version.jsx @@ -1,18 +1,11 @@ -import { useEffect, useState } from "react"; - export default function Version() { - const [version, setVersion] = useState(""); + const version = window?.configs?.version || ""; - useEffect(() => { - const version = window?.configs?.version - ? window.configs.version - : "rc-1-1"; - setVersion(version); - }, []); + if (!version) return null; return ( -
- {version} -
+ + {version} + ); } diff --git a/src/hooks/useGetDatasetsByYears.js b/src/hooks/useGetDatasetsByYears.js index 2645f17..5ff1896 100644 --- a/src/hooks/useGetDatasetsByYears.js +++ b/src/hooks/useGetDatasetsByYears.js @@ -34,8 +34,10 @@ export const useGetDatasetsByYears = (selectedYears = [], yearToDatasetId = {}) .filter(Boolean); }, [selectedYears, queries]); - // Check if any query is loading + // Check if any query is loading or has error const isAnyLoading = queries.some((query) => query.isLoading); + const isError = queries.some((query) => query.isError); + const error = queries.find((query) => query.error)?.error; // Get the current loading year (first loading query) const loadingYear = useMemo(() => { @@ -46,6 +48,8 @@ export const useGetDatasetsByYears = (selectedYears = [], yearToDatasetId = {}) return { fetchedDatasets, loadingYear, - isAnyLoading + isAnyLoading, + isError, + error }; }; diff --git a/src/index.css b/src/index.css index c2296a6..edf6f92 100644 --- a/src/index.css +++ b/src/index.css @@ -17,22 +17,25 @@ --primary: oklch(0.15 0 0); --primary-foreground: oklch(0.98 0 0); - --secondary: oklch(0.65 0.2 240); /* soft blue */ + --secondary: oklch(0.65 0.2 240); + /* soft blue */ --secondary-foreground: oklch(1 0 0); --muted: oklch(0.92 0 0); --muted-foreground: oklch(0.5 0 0); - --accent: oklch(0.65 0.2 240); /* same as secondary for accent */ + --accent: oklch(0.65 0.2 240); + /* same as secondary for accent */ --accent-foreground: oklch(1 0 0); - --destructive: oklch(0.577 0.245 27.325); /* red for destructive stays */ + --destructive: oklch(0.577 0.245 27.325); + /* red for destructive stays */ --destructive-foreground: oklch(0.577 0.245 27.325); --border: oklch(89.143% 0.0001 271.152); --input: oklch(0.92 0 0); --ring: oklch(0.65 0.2 240); - + --active-green: oklch(72.3% 0.219 149.579); --purple-light: oklch(71.4% 0.203 305.504); @@ -116,7 +119,7 @@ body { .react-datepicker { display: flex !important; - gap: 1rem; + gap: 1rem; } .react-datepicker__month-container { @@ -124,10 +127,11 @@ body { } .scroll-wrapper { - scrollbar-width: thin; + scrollbar-width: thin; scrollbar-color: transparent transparent; transition: scrollbar-color 0.3s; } + .react-datepicker__month-container { width: 100% !important; } @@ -136,6 +140,7 @@ body { width: 100% !important; } + .react-datepicker__day-names { width: 100% !important; display: flex; @@ -170,13 +175,15 @@ body { } .custom-scrollbar::-webkit-scrollbar-thumb { - background-color: rgba(100, 100, 100, 0.4); /* Thumb color */ + background-color: rgba(100, 100, 100, 0.4); + /* Thumb color */ border-radius: 4px; } /* Scrollbar Styling for Firefox */ .custom-scrollbar { - scrollbar-width: thin; /* Makes it thin */ + scrollbar-width: thin; + /* Makes it thin */ scrollbar-color: rgba(100, 100, 100, 0.4) transparent; } @@ -184,6 +191,7 @@ body { * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } @@ -195,33 +203,28 @@ body { /* Hide scrollbar but allow scroll */ .no-scrollbar { - -ms-overflow-style: none; - scrollbar-width: none; + -ms-overflow-style: none; + scrollbar-width: none; } + .no-scrollbar::-webkit-scrollbar { - display: none; + display: none; } ::-webkit-scrollbar { - width: 7px; - height: 5px; + width: 5px; + height: 3px; } ::-webkit-scrollbar-track { - background: #1e293b; + background: #d6dcdf; } ::-webkit-scrollbar-thumb { - background-color: #043077; + background-color: #0099ee; border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { - background-color: #06359c; /* darker on hover */ -} - - - - - - + background-color: #036fa9; +} \ No newline at end of file diff --git a/src/pages/DataPage/components/chart-visualization.jsx b/src/pages/DataPage/components/chart-visualization.jsx index 333b21b..5140ecd 100644 --- a/src/pages/DataPage/components/chart-visualization.jsx +++ b/src/pages/DataPage/components/chart-visualization.jsx @@ -17,7 +17,7 @@ import { useThemeContext } from "../../../context/themeContext"; const COLORS = [ - "#00bcd4", "#8bc34a", "#ffc107", "#ff9800", "#e91e63", "#9c27b0", + "#00bcd4", "#8bc34a", "#ffc107", "#ff9800", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#009688", "#4caf50", "#ff5722", "#795548", "#9e9e9e", "#607d8b" ]; export function ChartVisualization({ columns, rows, yearlyData }) { @@ -29,6 +29,7 @@ export function ChartVisualization({ columns, rows, yearlyData }) { const [detectedXTicks, setDetectedXTicks] = useState([]); const [tickRevision, setTickRevision] = useState(0); const [chartType, setChartType] = useState("bar"); + const [tiltLabels, setTiltLabels] = useState(false); const chartRef = useRef(null); const chartContainerRef = useRef(null); @@ -104,6 +105,26 @@ export function ChartVisualization({ columns, rows, yearlyData }) { .replace(/\s+/g, ""); }; + const splitTextIntoLines = (text, maxChars) => { + const str = String(text || "").trim(); + const words = str.split(/\s+/); + const lines = []; + let currentLine = ""; + + words.forEach((word) => { + if (!currentLine) { + currentLine = word; + } else if ((currentLine + " " + word).length <= maxChars) { + currentLine += " " + word; + } else { + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) lines.push(currentLine); + return lines; + }; + const isXString = useMemo(() => stringColumns.includes(xAxis), [xAxis, stringColumns]); const yStringColumn = useMemo(() => selectedYColumns.find(col => stringColumns.includes(col)), [selectedYColumns, stringColumns]); const isAnyYString = !!yStringColumn; @@ -324,13 +345,15 @@ export function ChartVisualization({ columns, rows, yearlyData }) { }, 250); return () => clearTimeout(timer); - }, [chartData, selectedYColumns, chartLayout, chartType, tickRevision]); + }, [chartData, selectedYColumns, chartLayout, chartType, tickRevision, tiltLabels]); const isMultiYear = normalizedYearlyData.length > 1; // Layout constants const yAxisGutterWidth = chartLayout === 'horizontal' ? 150 : 100; - const yAxisLabelSectionWidth = yAxisGutterWidth - 25; // Reserve space for title + const yAxisLabelSectionWidth = chartLayout === 'horizontal' + ? yAxisGutterWidth - 2 // Horizontal: minimal gap, title is in fixed overlay + : yAxisGutterWidth - 25; // Vertical: reserve space for title const barsPerCategory = normalizedYearlyData.length * valueColumns.length; @@ -348,6 +371,28 @@ export function ChartVisualization({ columns, rows, yearlyData }) { ? Math.max(chartData.length * widthPerCategory, 450) : 450; + // Format large numbers with abbreviations + const abbreviateNumber = (value) => { + if (typeof value !== 'number') return value; + + const absValue = Math.abs(value); + const sign = value < 0 ? '-' : ''; + + if (absValue >= 1e12) { + return sign + (absValue / 1e12).toFixed(1).replace(/\.0$/, '') + 'T'; + } + if (absValue >= 1e9) { + return sign + (absValue / 1e9).toFixed(1).replace(/\.0$/, '') + 'B'; + } + if (absValue >= 1e6) { + return sign + (absValue / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'; + } + if (absValue >= 1e3) { + return sign + (absValue / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'; + } + return value.toString(); + }; + const renderCustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return ( @@ -494,6 +539,18 @@ export function ChartVisualization({ columns, rows, yearlyData }) { + + {/* Tilt Labels Toggle */} + {chartLayout === 'vertical' && ( +
+ +
+ )} {/* Chart */} @@ -590,7 +647,7 @@ export function ChartVisualization({ columns, rows, yearlyData }) {
{detectedTicks.map((tick, i) => { const text = (tick.label || "").trim(); - const words = text.split(/\s+/); - const maxChars = 13; - const lines = []; - let currentLine = ""; - - words.forEach((word) => { - if (!currentLine) { - currentLine = word; - } else if ((currentLine + " " + word).length <= maxChars) { - currentLine += " " + word; - } else { - lines.push(currentLine); - currentLine = word; + const lines = splitTextIntoLines(text, 13); + // Calculate dynamic translateY for horizontal layout + let translateY = "-50%"; + if (chartLayout === 'horizontal') { + const isTopLabel = i === 0; + const isBottomLabel = i === detectedTicks.length - 1; + + if (isTopLabel && lines.length > 1) { + // Bar chart: less adjustment (stays higher) + // Line chart: more adjustment (shifts down more) + const adjustment = chartType === 'bar' + ? Math.max(60, 50 - (lines.length - 1) * 5) + : Math.max(40, 50 - (lines.length - 1) * 10); + translateY = `-${adjustment}%`; + } else if (isBottomLabel && lines.length > 1) { + translateY = `-${Math.min(70, 50 + (lines.length - 1) * 10)}%`; } - }); - if (currentLine) lines.push(currentLine); + } return (
{lines.map((line, idx) => ( -
{line}
+
{line}
))}
); @@ -704,50 +764,86 @@ export function ChartVisualization({ columns, rows, yearlyData }) { nice={chartLayout === "horizontal"} stroke="white" interval={0} - height={chartLayout === "horizontal" ? 30 : 86} + height={chartLayout === "horizontal" ? 30 : (tiltLabels ? 128 : 86)} tickLine={false} axisLine={{ stroke: "#374151" }} + tickFormatter={chartLayout === 'horizontal' ? abbreviateNumber : undefined} tick={chartLayout === "horizontal" ? { fill: "transparent" } : ({ x, y, payload }) => { - const maxCharsPerLine = 8; - const text = payload.value; - let line1 = text; - let line2 = ""; - if (text.length > maxCharsPerLine) { - const splitIndex = text.lastIndexOf(" ", maxCharsPerLine); - if (splitIndex > 0) { - line1 = text.slice(0, splitIndex); - line2 = text.slice(splitIndex + 1); - } else { - line1 = text.slice(0, maxCharsPerLine); - line2 = text.slice(maxCharsPerLine); + const text = String(payload.value || ""); + + if (tiltLabels) { + // Tilted labels - multi-line with rotation + const lines = splitTextIntoLines(text, 12); + + // Calculate x-offset based on number of lines + const xOffset = lines.length > 1 ? -4 * (lines.length - 1) : 0; + + // Dynamic font size based on number of lines + const fontSize = lines.length === 1 ? 8 : lines.length === 2 ? 8 : 7; + + return ( + + {lines.map((line, idx) => ( + + {line} + + ))} + + ); + } else { + // Multi-line labels without tilt + const maxCharsPerLine = 14; + const str = String(text || ""); + let line1 = str; + let line2 = ""; + if (str.length > maxCharsPerLine) { + const splitIndex = str.lastIndexOf(" ", maxCharsPerLine); + if (splitIndex > 0) { + line1 = str.slice(0, splitIndex); + line2 = str.slice(splitIndex + 1); + } else { + line1 = str.slice(0, maxCharsPerLine); + line2 = str.slice(maxCharsPerLine); + } } - } - return ( - - - {line1} - - {line2 && ( + const fontSize = line2 ? 10.5 : 11.5; + + return ( + - {line2} + {line1} - )} - - ); + {line2 && ( + + {line2} + + )} + + ); + } }} /> {xAxis === "" ? ( @@ -805,12 +902,14 @@ export function ChartVisualization({ columns, rows, yearlyData }) { { - const maxCharsPerLine = 13; - const text = payload.value; - let line1 = text; - let line2 = ""; - if (text.length > maxCharsPerLine) { - const splitIndex = text.lastIndexOf(" ", maxCharsPerLine); - if (splitIndex > 0) { - line1 = text.slice(0, splitIndex); - line2 = text.slice(splitIndex + 1); - } else { - line1 = text.slice(0, maxCharsPerLine); - line2 = text.slice(maxCharsPerLine); + const text = String(payload.value || ""); + + if (tiltLabels) { + // Tilted labels - multi-line with rotation + const lines = splitTextIntoLines(text, 12); + + // Calculate x-offset based on number of lines + const xOffset = lines.length > 1 ? -2 * (lines.length - 1) : 0; + + // Dynamic font size based on number of lines + const fontSize = lines.length === 1 ? 8 : lines.length === 2 ? 8 : 7; + + return ( + + {lines.map((line, idx) => ( + + {line} + + ))} + + ); + } else { + // Multi-line labels without tilt + const maxCharsPerLine = 14; + const str = String(text || ""); + let line1 = str; + let line2 = ""; + if (str.length > maxCharsPerLine) { + const splitIndex = str.lastIndexOf(" ", maxCharsPerLine); + if (splitIndex > 0) { + line1 = str.slice(0, splitIndex); + line2 = str.slice(splitIndex + 1); + } else { + line1 = str.slice(0, maxCharsPerLine); + line2 = str.slice(maxCharsPerLine); + } } - } - return ( - - - {line1} - - {line2 && ( + + + const fontSize = line2 ? 10.5 : 11.5; + + return ( + - {line2} + {line1} - )} - - ); + {line2 && ( + + {line2} + + )} + + ); + } }} /> @@ -993,4 +1134,4 @@ export function ChartVisualization({ columns, rows, yearlyData }) { )} ); -} \ No newline at end of file +} diff --git a/src/pages/DataPage/components/dataset-view.jsx b/src/pages/DataPage/components/dataset-view.jsx index 3a95c93..daf1e89 100644 --- a/src/pages/DataPage/components/dataset-view.jsx +++ b/src/pages/DataPage/components/dataset-view.jsx @@ -2,7 +2,7 @@ import { DataTable } from "./table-view"; import { useEffect, useState, useMemo } from "react"; import { ClipLoader } from "react-spinners"; import { ChartVisualization } from "./chart-visualization"; -import { Eye } from "lucide-react"; +import { AlertCircle } from "lucide-react"; import { Link, useLocation } from "react-router-dom"; import { useThemeContext } from "../../../context/themeContext"; import { useAvailableYearsForDataset } from "../../../hooks/useAvailableYearsForDataset"; @@ -17,7 +17,7 @@ export function DatasetView({ data, setExternalDateRange }) { const [filteredYears, setFilteredYears] = useState([]); // fecth available years - const { data: availableYearsData, isLoading: yearsLoading } = + const { data: availableYearsData, isLoading: yearsLoading, isError: isYearsError, error: yearsError } = useAvailableYearsForDataset(data?.datasetIds ?? []); //map year to dataset id @@ -34,7 +34,8 @@ export function DatasetView({ data, setExternalDateRange }) { }, [selectedYears, yearToDatasetId]); // Fetch organization data for all selected datasets in parallel - const { data: organizationsData } = useRootOrganizations(selectedDatasetIds); + const { data: organizationsData, isError: isOrgErrorReal } = useRootOrganizations(selectedDatasetIds); + const isOrgError = true; // Forced for testing // Map organizations to years and check if all names are the same const organizationsByYear = useMemo(() => { @@ -110,7 +111,7 @@ export function DatasetView({ data, setExternalDateRange }) { }, [filteredYears]); // fetch datasets per year - const { fetchedDatasets, loadingYear, isAnyLoading } = useGetDatasetsByYears( + const { fetchedDatasets, loadingYear, isAnyLoading, isError: isContentError, error: contentError } = useGetDatasetsByYears( selectedYears, yearToDatasetId ); @@ -174,7 +175,20 @@ export function DatasetView({ data, setExternalDateRange }) { if (yearsLoading) { return (
- + +
+ ); + } + + if (isYearsError) { + return ( +
+
+ +

+ {yearsError?.message || "Failed to load available years for this dataset."} +

+
); } @@ -182,11 +196,11 @@ export function DatasetView({ data, setExternalDateRange }) { const firstSelectedYear = selectedYears[0]; return ( -
+
{/* dataset info */} {filteredYears.length > 0 && (
-

+

{data?.name ?? "Dataset"}

@@ -194,7 +208,7 @@ export function DatasetView({ data, setExternalDateRange }) { {/* year selector */} {multiYearMode ? ( -
+
{!isPlottable && fetchedDatasets.length > 0 && (

This dataset cannot be visualized. Showing table view only for one year. @@ -254,6 +268,15 @@ export function DatasetView({ data, setExternalDateRange }) {

)} + {isContentError && ( +
+ +

+ {contentError?.message || "Failed to load dataset content."} +

+
+ )} +
{fetchedDatasets.length > 0 ? ( <> @@ -271,67 +294,13 @@ export function DatasetView({ data, setExternalDateRange }) { )} - {displayOrganizations && ( -
-

- Published by: - {displayOrganizations.type === 'single' ? ( - // Single organization display - displayOrganizations.data.type === "department" ? ( - - {displayOrganizations.data.name} - - ) : ( - {displayOrganizations.data.name} - ) - ) : ( - // Multiple organizations display - - {displayOrganizations.data.map((org, idx) => ( - - {org.year} - {" "} - {org.type === "department" ? ( - - {org.name} - - ) : ( - {org.name} - )} - - ))} - - )} -

- - See sources - -
- )} ) : ( filteredYears.length === 0 && - allAvailableYears.length > 0 && ( + allAvailableYears.length > 0 && !loadingYear && (

- No available data yet! But you have data for + No available data yet! You can find data for the following years:

{allAvailableYears.map((year) => ( @@ -345,13 +314,74 @@ export function DatasetView({ data, setExternalDateRange }) { className="flex text-accent/90 gap-2 cursor-pointer mt-2" onClick={handleAvailableDatasetView} > - Show me
) )} + + {(displayOrganizations || isOrgError) && ( +
+
+
+ Published by: + {isOrgError && !displayOrganizations && ( + + No publisher found + + )} + {displayOrganizations && displayOrganizations.type === 'single' && ( + displayOrganizations.data.type === "department" ? ( + + {displayOrganizations.data.name} + + ) : ( + {displayOrganizations.data.name} + ) + )} +
+ + {displayOrganizations && displayOrganizations.type === 'multiple' && ( +
+ {displayOrganizations.data.map((org) => ( +
+ {org.year}: + {org.type === "department" ? ( + + {org.name} + + ) : ( + {org.name} + )} +
+ ))} +
+ )} +
+ + See sources + +
+ )}
diff --git a/src/pages/DataPage/screens/DataPage.jsx b/src/pages/DataPage/screens/DataPage.jsx index 59648fd..d333f25 100644 --- a/src/pages/DataPage/screens/DataPage.jsx +++ b/src/pages/DataPage/screens/DataPage.jsx @@ -1,7 +1,7 @@ -import { Folder, FileText } from "lucide-react"; +import { AlertCircle, Folder, Loader2, TableProperties } from "lucide-react"; import React, { useEffect, useState } from "react"; import { motion } from "framer-motion"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams, useLocation } from "react-router-dom"; import { Breadcrumb } from "../components/breadcrumb"; import formatText from "../../../utils/common_functions"; import { DatasetView } from "../components/dataset-view"; @@ -10,99 +10,133 @@ import { useDataCatalog } from "../../../hooks/useDataCatalog"; export default function DataPage({ setExternalDateRange }) { const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const location = useLocation(); const [breadcrumbTrail, setBreadcrumbTrail] = useState([]); const [selectedDataset, setSelectedDataset] = useState(null); + const [loadingCardId, setLoadingCardId] = useState(null); // Decode categoryIds from URL as an array const categoryIdsParam = searchParams.get("categoryIds") || ""; let categoryIds = []; if (categoryIdsParam) { try { - categoryIds = JSON.parse(decodeURIComponent(categoryIdsParam)); + // searchParams.get already decodes once. No need for decodeURIComponent. + categoryIds = JSON.parse(categoryIdsParam); if (!Array.isArray(categoryIds)) categoryIds = [categoryIds]; } catch { categoryIds = [categoryIdsParam]; } } - const { data, isLoading } = useDataCatalog(categoryIds); + const { data, isLoading, isError, error } = useDataCatalog(categoryIds); - // Initialize breadcrumb from URL or from first category + // Keep reference to previous data while loading + const [displayData, setDisplayData] = useState(null); + + // 1. Initialize breadcrumb from URL immediately useEffect(() => { const breadcrumbParam = searchParams.get("breadcrumb"); if (breadcrumbParam) { try { - const decoded = JSON.parse(decodeURIComponent(breadcrumbParam)); - if (Array.isArray(decoded)) setBreadcrumbTrail(decoded); + // searchParams.get already decodes once. No need for decodeURIComponent. + const decoded = JSON.parse(breadcrumbParam); + if (Array.isArray(decoded)) { + setBreadcrumbTrail(decoded); + return; + } } catch { console.warn("Invalid breadcrumb in URL"); } - } else if (data?.categories?.length && categoryIds.length) { - const newBreadcrumb = [ + } + }, [searchParams]); + + // 2. Fallback initialization if no breadcrumb exists but data/categoryIds are present + useEffect(() => { + const breadcrumbParam = searchParams.get("breadcrumb"); + if (!breadcrumbParam && data?.categories?.length && categoryIds.length) { + const initialParams = new URLSearchParams(); + initialParams.set("categoryIds", JSON.stringify(categoryIds)); + const initialBreadcrumb = [ { - label: formatText({ name: data.categories[0].name }) || "Category", - path: `/datasets?categoryIds=${encodeURIComponent( - JSON.stringify(categoryIds) - )}`, + label: "Data Catalog", + path: `/data?${initialParams.toString()}`, }, ]; - const encodedTrail = encodeURIComponent(JSON.stringify(newBreadcrumb)); - setBreadcrumbTrail(newBreadcrumb); + setBreadcrumbTrail(initialBreadcrumb); + } + }, [data, categoryIds, searchParams]); - const params = new URLSearchParams(location.search); - params.set("categoryIds", encodeURIComponent(JSON.stringify(categoryIds))); - params.set("breadcrumb", encodedTrail); - navigate(`${location.pathname}?${params.toString()}`); + // Update display data only when new data is fully loaded + useEffect(() => { + if (!isLoading && data) { + setDisplayData(data); + setLoadingCardId(null); } - }, [data, categoryIdsParam]); + }, [isLoading, data]); const handleCategoryClick = (category) => { + // Prevent clicking while loading + if (isLoading) return; + const categoryIdsArray = category.categoryIds; // full array const categoryName = category.name; + const cardId = category.categoryIds.join("-"); if (!breadcrumbTrail.find((b) => b.label === categoryName)) { + setLoadingCardId(cardId); + + const breadcrumbParams = new URLSearchParams(); + breadcrumbParams.set("categoryIds", JSON.stringify(categoryIdsArray)); + const newBreadcrumb = [ ...breadcrumbTrail, { label: formatText({ name: categoryName }) || "Category", - path: `/datasets?categoryIds=${encodeURIComponent( - JSON.stringify(categoryIdsArray) - )}`, + path: `/data?${breadcrumbParams.toString()}`, }, ]; - const encodedTrail = encodeURIComponent(JSON.stringify(newBreadcrumb)); + const jsonTrail = JSON.stringify(newBreadcrumb); setBreadcrumbTrail(newBreadcrumb); + setLoadingCardId(cardId); - const params = new URLSearchParams(location.search); - params.set("categoryIds", encodeURIComponent(JSON.stringify(categoryIdsArray))); - params.set("breadcrumb", encodedTrail); + const params = new URLSearchParams(window.location.search); + params.set("categoryIds", JSON.stringify(categoryIdsArray)); + params.set("breadcrumb", jsonTrail); navigate(`${location.pathname}?${params.toString()}`); } }; const handleDatasetClick = (dataset) => { + // Prevent clicking while loading + if (isLoading) return; + const datasetName = dataset.name || formatText({ name: dataset.name }); if (!breadcrumbTrail.find((b) => b.label === datasetName)) { + setLoadingCardId(datasetName); + + const breadcrumbParams = new URLSearchParams(); + breadcrumbParams.set("datasetName", datasetName); + breadcrumbParams.set("categoryIds", JSON.stringify(categoryIds)); + const newBreadcrumb = [ ...breadcrumbTrail, { label: datasetName, - path: `/datasets?datasetName=${datasetName}&categoryIds=${encodeURIComponent( - JSON.stringify(categoryIds) - )}`, + path: `/data?${breadcrumbParams.toString()}`, }, ]; - const encodedTrail = encodeURIComponent(JSON.stringify(newBreadcrumb)); + const jsonTrail = JSON.stringify(newBreadcrumb); setBreadcrumbTrail(newBreadcrumb); + setLoadingCardId(datasetName); - const params = new URLSearchParams(location.search); + const params = new URLSearchParams(window.location.search); params.set("datasetName", datasetName); - params.set("categoryIds", encodeURIComponent(JSON.stringify(categoryIds))); - params.set("breadcrumb", encodedTrail); + params.set("categoryIds", JSON.stringify(categoryIds)); + params.set("breadcrumb", jsonTrail); navigate(`${location.pathname}?${params.toString()}`); } @@ -112,17 +146,21 @@ export default function DataPage({ setExternalDateRange }) { const handleBreadcrumbClick = (index, item) => { const url = new URL(item.path, window.location.origin); const newTrail = breadcrumbTrail.slice(0, index + 1); - const encodedTrail = encodeURIComponent(JSON.stringify(newTrail)); setBreadcrumbTrail(newTrail); - const params = new URLSearchParams(location.search); + // Clear states when navigating back + setSelectedDataset(null); + setLoadingCardId(null); + + const params = new URLSearchParams(window.location.search); const pidParam = url.searchParams.get("categoryIds"); const datasetName = url.searchParams.get("datasetName"); if (pidParam) { try { - const decodedPid = JSON.parse(decodeURIComponent(pidParam)); - params.set("categoryIds", encodeURIComponent(JSON.stringify(decodedPid))); + // pidParam is already decoded by URLSearchParams.get + const decodedPid = JSON.parse(pidParam); + params.set("categoryIds", JSON.stringify(decodedPid)); } catch { params.set("categoryIds", pidParam); } @@ -133,7 +171,7 @@ export default function DataPage({ setExternalDateRange }) { if (datasetName) params.set("datasetName", datasetName); else params.delete("datasetName"); - params.set("breadcrumb", encodedTrail); + params.set("breadcrumb", JSON.stringify(newTrail)); navigate(`${location.pathname}?${params.toString()}`); }; @@ -146,29 +184,48 @@ export default function DataPage({ setExternalDateRange }) { setSelectedDatasets={() => setSelectedDataset(null)} /> )} + {isError && ( +
+ +

+ {error?.message || "An unexpected error occurred while fetching the data."} +

+
- {isLoading ? ( -
Loading...
- ) : selectedDataset ? ( + )} + + {isLoading && !displayData && ( +
+ +

+ Loading Data... +

+
+ )} + + {selectedDataset ? ( ) : ( <> - {data?.categories?.length > 0 && ( + {!isError && displayData?.categories?.length > 0 && ( <>

Categories

- {data.categories.map((item) => ( + {displayData.categories.map((item) => ( handleCategoryClick(item)} - className="cursor-pointer bg-background shadow-2xs relative w-full h-[100px] border border-border rounded-2xl p-4 flex items-center justify-between hover:bg-border/10 transition" + className={`bg-background shadow-2xs relative w-full h-[100px] border border-border rounded-2xl p-4 flex items-center justify-between transition ${isLoading + ? "opacity-60 cursor-not-allowed" + : "cursor-pointer hover:bg-border/10" + }`} >
@@ -176,29 +233,35 @@ export default function DataPage({ setExternalDateRange }) { {formatText({ name: item.name })}

+ {loadingCardId === item.categoryIds.join("-") && ( + + )}
))}
)} - {data?.datasets?.length > 0 && ( + {displayData?.datasets?.length > 0 && (
-

+

Datasets

- {data.datasets.map((dataset) => ( + {displayData.datasets.map((dataset) => ( handleDatasetClick(dataset)} - className="cursor-pointer shadow-2xs w-full h-[90px] border border-border rounded-xl p-4 flex items-center bg-dataset-card bg-background transition" + className={`shadow-2xs w-full h-[90px] border border-border rounded-2xl p-4 flex items-center justify-between bg-dataset-card bg-background transition ${isLoading + ? "opacity-60 cursor-not-allowed" + : "cursor-pointer" + }`} > - -
-

+

+ +

{dataset.name}

@@ -208,7 +271,7 @@ export default function DataPage({ setExternalDateRange }) {
)} - {data?.categories?.length === 0 && data?.datasets?.length === 0 && ( + {displayData?.categories?.length === 0 && displayData?.datasets?.length === 0 && (

No categories or datasets found for this level.

diff --git a/src/pages/HomePage/screens/HomePage.jsx b/src/pages/HomePage/screens/HomePage.jsx index e8b4c2b..ce28c85 100644 --- a/src/pages/HomePage/screens/HomePage.jsx +++ b/src/pages/HomePage/screens/HomePage.jsx @@ -195,7 +195,7 @@ export default function HomePage() { {isExpanded ? : } -
+
{isExpanded && Share feedback}
diff --git a/src/pages/LandingPage/components/textLogo.jsx b/src/pages/LandingPage/components/textLogo.jsx index 3f09923..963b3b3 100644 --- a/src/pages/LandingPage/components/textLogo.jsx +++ b/src/pages/LandingPage/components/textLogo.jsx @@ -1,6 +1,7 @@ import React from "react"; import { Link } from "react-router-dom"; import { useThemeContext } from "../../../context/themeContext"; +import Version from "../../../components/version"; export default function TextLogo({ isExpanded }) { const { isDark } = useThemeContext(); @@ -10,7 +11,7 @@ export default function TextLogo({ isExpanded }) {

{isExpanded ? ( - OpenGINXplore + OpenGINXplore ) : isDark ? ( diff --git a/src/pages/OrganizationPage/components/DepartmentTab.jsx b/src/pages/OrganizationPage/components/DepartmentTab.jsx index 41a6d91..c5f6840 100644 --- a/src/pages/OrganizationPage/components/DepartmentTab.jsx +++ b/src/pages/OrganizationPage/components/DepartmentTab.jsx @@ -295,15 +295,29 @@ const DepartmentTab = ({ selectedDate, ministryId }) => { History - {dep.hasData && ( - - Data - - ) } + {dep.hasData && (() => { + const pathParams = new URLSearchParams(); + pathParams.set("categoryIds", JSON.stringify([dep.id])); + + const outerParams = new URLSearchParams(); + outerParams.set("categoryIds", JSON.stringify([dep.id])); + outerParams.set("breadcrumb", JSON.stringify([ + { + label: dep.name, + path: `/data?${pathParams.toString()}`, + }, + ])); + + return ( + + Data + + ); + })()}

); diff --git a/src/services/xploredataServices.jsx b/src/services/xploredataServices.jsx index 8b64448..88f2a01 100644 --- a/src/services/xploredataServices.jsx +++ b/src/services/xploredataServices.jsx @@ -3,35 +3,56 @@ import axios from "../lib/axios"; const GI_SERVICE_URL = "/v1/data"; export const getDataCatalog = async ({ categoryIds = [], signal }) => { - const { data } = await axios.post( - `${GI_SERVICE_URL}/data-catalog`, - { categoryIds }, - { signal } - ); - return data; + try { + const { data } = await axios.post( + `${GI_SERVICE_URL}/data-catalog`, + { categoryIds }, + { signal } + ); + return data; + } catch (error) { + if (error.name === 'CanceledError') throw error; + throw new Error("An unexpected error occurred while fetching the data."); + } }; + export const getAvailableYearsForDataset = async ({ datasetIds = [], signal }) => { - const { data } = await axios.post( - `${GI_SERVICE_URL}/datasets/years`, - { datasetIds }, - { signal } - ); - return data; + try { + const { data } = await axios.post( + `${GI_SERVICE_URL}/datasets/years`, + { datasetIds }, + { signal } + ); + return data; + } catch (error) { + if (error.name === 'CanceledError') throw error; + throw new Error("Failed to load available years for the selected dataset."); + } }; export const getDatasetById = async ({ datasetId, signal }) => { - const { data } = await axios.get( - `${GI_SERVICE_URL}/datasets/${datasetId}/data`, - { signal } - ); - return data; + try { + const { data } = await axios.get( + `${GI_SERVICE_URL}/datasets/${datasetId}/data`, + { signal } + ); + return data; + } catch (error) { + if (error.name === 'CanceledError') throw error; + throw new Error("Failed to load dataset content. Please try again later."); + } }; export const getRootOrganization = async ({ datasetId, signal }) => { - const { data } = await axios.get( - `${GI_SERVICE_URL}/datasets/${datasetId}/root`, - { signal } - ); - return data; + try { + const { data } = await axios.get( + `${GI_SERVICE_URL}/datasets/${datasetId}/root`, + { signal } + ); + return data; + } catch (error) { + if (error.name === 'CanceledError') throw error; + throw new Error("Failed to load publisher information."); + } };