diff --git a/package-lock.json b/package-lock.json
index 7c8672c..c2dbc95 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,8 @@
"react-icons": "^5.4.0",
"react-query": "^3.39.3",
"react-router-dom": "^7.1.2",
- "react-toastify": "^11.0.3"
+ "react-toastify": "^11.0.3",
+ "recharts": "^2.15.1"
},
"devDependencies": {
"@types/node": "^22.10.7",
@@ -2141,6 +2142,60 @@
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="
+ },
+ "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=="
+ },
+ "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=="
+ },
+ "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==",
+ "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=="
+ },
+ "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==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "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=="
+ },
+ "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=="
+ },
"node_modules/@types/eslint": {
"version": "8.56.12",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
@@ -3022,6 +3077,116 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "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==",
+ "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==",
+ "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==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -3049,6 +3214,11 @@
}
}
},
+ "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=="
+ },
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -3074,6 +3244,15 @@
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
},
+ "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==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -3469,6 +3648,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
+ },
"node_modules/expect-type": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz",
@@ -3483,6 +3667,14 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
+ "node_modules/fast-equals": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
+ "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -3887,6 +4079,14 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "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==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -4206,8 +4406,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -4215,6 +4414,17 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "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==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/loupe": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
@@ -4422,6 +4632,14 @@
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true
},
+ "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==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/oblivious-set": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz",
@@ -4663,6 +4881,16 @@
"node": ">= 0.8.0"
}
},
+ "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==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4850,6 +5078,20 @@
"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==",
+ "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-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -4899,6 +5141,56 @@
"react-dom": "^18 || ^19"
}
},
+ "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==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
+ "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==",
+ "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==",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
+ "node_modules/recharts/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=="
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -5321,6 +5613,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "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=="
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -5636,6 +5933,27 @@
}
}
},
+ "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==",
+ "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": "6.0.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz",
diff --git a/package.json b/package.json
index 539c2ff..12bd9e7 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,8 @@
"react-icons": "^5.4.0",
"react-query": "^3.39.3",
"react-router-dom": "^7.1.2",
- "react-toastify": "^11.0.3"
+ "react-toastify": "^11.0.3",
+ "recharts": "^2.15.1"
},
"devDependencies": {
"@types/node": "^22.10.7",
@@ -41,6 +42,8 @@
"@typescript-eslint/eslint-plugin": "^8.20.0",
"@typescript-eslint/parser": "^8.20.0",
"@vitejs/plugin-react": "^4.3.4",
+ "@vitest/coverage-v8": "^3.0.2",
+ "@vitest/ui": "^3.0.2",
"eslint": "^9.18.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.18",
@@ -49,8 +52,6 @@
"vite-plugin-dts": "~4.5.0",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^5.1.4",
- "vitest": "^3.0.2",
- "@vitest/coverage-v8": "^3.0.2",
- "@vitest/ui": "^3.0.2"
+ "vitest": "^3.0.2"
}
}
diff --git a/src/assets/weather/cloudy.svg b/src/assets/weather/cloudy.svg
new file mode 100644
index 0000000..8a9f3c6
--- /dev/null
+++ b/src/assets/weather/cloudy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/haze.svg b/src/assets/weather/haze.svg
new file mode 100644
index 0000000..567e45c
--- /dev/null
+++ b/src/assets/weather/haze.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/heavy-rain.svg b/src/assets/weather/heavy-rain.svg
new file mode 100644
index 0000000..cdd44e4
--- /dev/null
+++ b/src/assets/weather/heavy-rain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/partly-cloudy.svg b/src/assets/weather/partly-cloudy.svg
new file mode 100644
index 0000000..daff138
--- /dev/null
+++ b/src/assets/weather/partly-cloudy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/rain.svg b/src/assets/weather/rain.svg
new file mode 100644
index 0000000..604247d
--- /dev/null
+++ b/src/assets/weather/rain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/sleet.svg b/src/assets/weather/sleet.svg
new file mode 100644
index 0000000..eceaf37
--- /dev/null
+++ b/src/assets/weather/sleet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/snow.svg b/src/assets/weather/snow.svg
new file mode 100644
index 0000000..466d748
--- /dev/null
+++ b/src/assets/weather/snow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/sunny.svg b/src/assets/weather/sunny.svg
new file mode 100644
index 0000000..2d5d629
--- /dev/null
+++ b/src/assets/weather/sunny.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/thunderstorm.svg b/src/assets/weather/thunderstorm.svg
new file mode 100644
index 0000000..8bf643e
--- /dev/null
+++ b/src/assets/weather/thunderstorm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/weather/windy.svg b/src/assets/weather/windy.svg
new file mode 100644
index 0000000..e97edaf
--- /dev/null
+++ b/src/assets/weather/windy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/WeatherModal/CurrentWeather.tsx b/src/components/WeatherModal/CurrentWeather.tsx
new file mode 100644
index 0000000..58d14fb
--- /dev/null
+++ b/src/components/WeatherModal/CurrentWeather.tsx
@@ -0,0 +1,145 @@
+import { Grid, Group, Stack, Text, ThemeIcon } from '@mantine/core';
+import { FaLongArrowAltDown, FaLongArrowAltUp, FaWind } from 'react-icons/fa';
+import { WiBarometer, WiHumidity } from 'react-icons/wi';
+import WeatherIcon from './WeatherIcon';
+
+export default function CurrentWeather({ weather }: any) {
+ return (
+
+
+
+ Current Weather
+
+
+
+
+
+
+
+ {weather.temperature}°F
+
+
+
+ {weather.textDescription}
+
+
+
+
+ Feels like {weather.heatIndex ?? weather.windChill ?? weather.temperature}°
+ F
+
+
+
+
+
+
+
+
+
+ {weather.highTemp}°F
+
+
+
+
+
+
+
+
+
+ {weather.lowTemp}°F
+
+
+
+
+
+
+
+
+
+
+ Humidity
+
+
+ {weather.humidity}%
+
+
+
+
+
+
+
+
+
+ Wind
+
+
+ {weather.windSpeed}mph
+
+
+
+
+
+
+
+
+
+ Pressure
+
+
+ {weather.pressure} in
+
+
+
+
+
+ );
+}
diff --git a/src/components/WeatherModal/ExtendedForecast.tsx b/src/components/WeatherModal/ExtendedForecast.tsx
new file mode 100644
index 0000000..30bd8b1
--- /dev/null
+++ b/src/components/WeatherModal/ExtendedForecast.tsx
@@ -0,0 +1,52 @@
+import { Group, Stack, Text } from '@mantine/core';
+import WeatherIcon from './WeatherIcon';
+
+export default function ExtendedForecast({ forecast }: any) {
+ const day = forecast.slice(2, 14).filter((item: any) => item.isDaytime);
+ const night = forecast.slice(2, 14).filter((item: any) => !item.isDaytime);
+ return (
+
+
+
+ Extended Forecast
+
+
+
+ {day.map((item: any, index: number) => {
+ const short = item.shortForecast.includes('And')
+ ? item.shortForecast.substring(0, item.shortForecast.indexOf('And'))
+ : item.shortForecast.includes('then')
+ ? item.shortForecast.substring(0, item.shortForecast.indexOf('then'))
+ : item.shortForecast;
+ return (
+
+ {item.name}
+
+ {short}
+
+ {item.temperature}°{item.temperatureUnit}/{night[index].temperature}°
+ {item.temperatureUnit}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/WeatherModal/HourlyCard.tsx b/src/components/WeatherModal/HourlyCard.tsx
index 4338c9b..3219d38 100644
--- a/src/components/WeatherModal/HourlyCard.tsx
+++ b/src/components/WeatherModal/HourlyCard.tsx
@@ -94,7 +94,7 @@ export default function HourlyCard({ weather }: any) {
color: 'white',
}}
>
- {weather.temperature}°{weather.temperatureUnit}/{weather.relativeHumidity.value}%
+ {weather.temperature}°{weather.temperatureUnit}/{weather.relativeHumidity?.value}%
- {`${weather.probabilityOfPrecipitation.value}`}%
+ {`${weather.probabilityOfPrecipitation?.value}`}%
diff --git a/src/components/WeatherModal/WeatherIcon.tsx b/src/components/WeatherModal/WeatherIcon.tsx
new file mode 100644
index 0000000..2b4df8d
--- /dev/null
+++ b/src/components/WeatherModal/WeatherIcon.tsx
@@ -0,0 +1,251 @@
+import { Image } from '@mantine/core';
+import CloudyIcon from '../../assets/weather/cloudy.svg';
+import HazeIcon from '../../assets/weather/haze.svg';
+import HeavyRainIcon from '../../assets/weather/heavy-rain.svg';
+import PartlyCloudyIcon from '../../assets/weather/partly-cloudy.svg';
+import RainIcon from '../../assets/weather/rain.svg';
+import SleetIcon from '../../assets/weather/sleet.svg';
+import SnowIcon from '../../assets/weather/snow.svg';
+import SunnyIcon from '../../assets/weather/sunny.svg';
+import ThunderstormIcon from '../../assets/weather/thunderstorm.svg';
+
+export default function WeatherIcon({ textDescription }: { textDescription: string }) {
+ const Icon = () => {
+ switch (textDescription.trim()) {
+ case 'Clear':
+ return (
+
+ );
+ case 'Cloudy':
+ return (
+
+ );
+ case 'Mostly Cloudy':
+ return (
+
+ );
+ case 'Rain':
+ return (
+
+ );
+ case 'Showers':
+ return (
+
+ );
+ case 'Drizzle':
+ return (
+
+ );
+ case 'Snow':
+ return (
+
+ );
+ case 'Thunderstorm':
+ return (
+
+ );
+ case 'Mist':
+ return (
+
+ );
+ case 'Haze':
+ return (
+
+ );
+ case 'Dust':
+ return (
+
+ );
+ case 'Fog':
+ return (
+
+ );
+ case 'Smoke':
+ return (
+
+ );
+ case 'Sand':
+ return (
+
+ );
+ case 'Ash':
+ return (
+
+ );
+ case 'Squall':
+ return (
+
+ );
+ case 'Tornado':
+ return (
+
+ );
+ case 'Freezing Rain':
+ return (
+
+ );
+ case 'Shower Rain':
+ return (
+
+ );
+ case 'Heavy Rain':
+ return (
+
+ );
+ case 'Scattered Clouds':
+ return (
+
+ );
+ case 'Broken Clouds':
+ return (
+
+ );
+ case 'Overcast Clouds':
+ return (
+
+ );
+ case 'Moderate Rain':
+ return (
+
+ );
+ case 'Light Rain':
+ return (
+
+ );
+ case 'Heavy Snow':
+ return (
+
+ );
+ case 'Sleet':
+ return (
+
+ );
+ case 'Partly Sunny':
+ return (
+
+ );
+ default:
+ return (
+
+ );
+ }
+ };
+
+ return ;
+}
diff --git a/src/components/WeatherModal/WeatherModal.tsx b/src/components/WeatherModal/WeatherModal.tsx
index 3741fa8..51a1cec 100644
--- a/src/components/WeatherModal/WeatherModal.tsx
+++ b/src/components/WeatherModal/WeatherModal.tsx
@@ -1,15 +1,26 @@
-import { Box, Center, Divider, Group, Modal, Stack, Text } from '@mantine/core';
+import { Center, Group, Modal, Stack, Text } from '@mantine/core';
import { modalClasses } from 'styles';
-import SmallOutlookCard from './SmallOutlookCard';
-import SmallWeatherCard from './SmallWeatherCard';
import { useCommonHooks, useWeatherAPI } from 'hooks';
import { useEffect, useState } from 'react';
-import HourlyCard from './HourlyCard';
+import {
+ Area,
+ CartesianGrid,
+ ComposedChart,
+ Legend,
+ Line,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+import CurrentWeather from './CurrentWeather';
+import ExtendedForecast from './ExtendedForecast';
export function WeatherModal({
current,
outlook,
+ extendedForecast,
closeWeatherModal,
weatherModalOpened,
}: any) {
@@ -31,9 +42,32 @@ export function WeatherModal({
const [hourlyForecast, setHourlyForecast] = useState([]);
const [sunset, setSunset] = useState(100);
+ function CustomTooltip({ payload, active, unit }: any) {
+ if (active) {
+ return (
+
+ {payload.map((item: any) => (
+ {`${item.name} - ${item.value}${item.unit}`}
+ ))}
+
+ );
+ }
+
+ return null;
+ }
+
useEffect(() => {
if (hourly) {
- setHourlyForecast(hourly.properties.periods.slice(1, 13));
+ setHourlyForecast(hourly.properties.periods.slice(1, 25));
}
}, [hourly]);
@@ -49,7 +83,6 @@ export function WeatherModal({
);
}
}, [hourlyForecast]);
-
return (
-
+
-
-
- Currently
-
-
-
-
-
-
- {outlook.map((o: any) => (
-
- {o.name}
-
-
-
-
- ))}
-
-
-
-
+ 0 && hourlyForecast[0].isDaytime
- ? `linear-gradient(90deg, rgba(245, 225, 171, 0.85) ${sunset - 5}%, rgba(35, 28, 42, 0.85) ${sunset + 5}%)`
- : `linear-gradient(90deg, rgba(35, 28, 42, 0.85)${sunset - 5}%, rgba(245, 225, 171, 0.85)${sunset + 5}%)`,
+ background: 'var(--color-search-background)',
padding: '.5rem',
borderRadius: '.5rem',
}}
>
-
- {hourlyForecast.length > 0 &&
- hourlyForecast.map((h: any) => {
- return ;
- })}
-
-
+
+
+ {
+ return new Date(data.startTime).toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+ }}
+ />
+
+
+ } />
+
+ {
+ return (data.dewpoint.value * 9) / 5 + 32;
+ }}
+ stroke={theme.accentColor}
+ dot={{ stroke: theme.primaryColor, strokeWidth: 1 }}
+ />
+
+
+
+
+
+ {
+ return new Date(data.startTime).toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+ }}
+ />
+
+
+ } />
+ {
+ return data.probabilityOfPrecipitation.value;
+ }}
+ stroke={theme.primaryColor}
+ fill={theme.accentColor}
+ dot={{ stroke: theme.primaryColor, strokeWidth: 1 }}
+ />
+ {
+ return data.relativeHumidity.value;
+ }}
+ stroke={theme.accentColor}
+ fill={theme.accentColor}
+ dot={{ stroke: theme.primaryColor, strokeWidth: 1 }}
+ />
+
+
+
+
+
diff --git a/src/pages/homepage/Homepage.tsx b/src/pages/homepage/Homepage.tsx
index 040fbc0..a92ddad 100644
--- a/src/pages/homepage/Homepage.tsx
+++ b/src/pages/homepage/Homepage.tsx
@@ -4,6 +4,7 @@ import { useCommonHooks, useOSMAPI, useQueryAPI, useWeatherAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { stackClasses } from 'styles';
import type { CurrentWeather, OutlookWeather } from 'types';
+import type { ExtendedForecast } from 'types/weather';
import Applications from './components/Applications/Applications';
import Bookmarks from './components/Bookmarks/Bookmarks';
import Header from './components/Header/Header';
@@ -106,14 +107,17 @@ export function Homepage() {
});
const [current, setCurrent] = useState({
icon: '',
+ textDescription: '',
temperature: 0,
highTemp: 0,
lowTemp: 0,
precipitation: 'N/A',
humidity: 0,
- heatIndex: 0,
+ heatIndex: null,
+ windChill: null,
windSpeed: 0,
windDirection: 0,
+ pressure: 0,
});
const [outlook, setOutlook] = useState([
{
@@ -126,11 +130,22 @@ export function Homepage() {
windDirection: 'N',
},
]);
+ const [extendedForecast, setExtendedForecast] = useState([
+ {
+ detailedForecast: '',
+ isDaytime: true,
+ name: '',
+ shortForecast: '',
+ temperature: 0,
+ temperatureUnit: 'F',
+ },
+ ]);
useEffect(() => {
if (weather !== undefined && basicWeather !== undefined) {
setCurrent({
icon: weather.features[0]?.properties.icon?.replace('medium', 'large') || '',
+ textDescription: weather.features[0]?.properties.textDescription,
temperature: Math.round(
(weather.features[0]?.properties.temperature.value * 9) / 5 + 32,
),
@@ -143,13 +158,23 @@ export function Homepage() {
precipitation:
weather.features[0]?.properties.precipitationLastHour.value || 'N/A',
humidity: Math.round(weather.features[0]?.properties.relativeHumidity.value),
- heatIndex: Math.round(
- weather.features[0]?.properties.heatIndex.value !== null
- ? (weather.features[0]?.properties.heatIndex.value * 9) / 5 + 32
- : (weather.features[0]?.properties.temperature.value * 9) / 5 + 32,
- ),
+ heatIndex:
+ basicWeather.properties.heatIndex?.values[0].value === null
+ ? null
+ : Math.round(
+ (basicWeather.properties.heatIndex.values[0].value * 9) / 5 + 32,
+ ),
+ windChill:
+ basicWeather.properties.windChill?.values[0].value === null
+ ? null
+ : Math.round(
+ (basicWeather.properties.windChill.values[0].value * 9) / 5 + 32,
+ ),
windSpeed: Math.round(weather.features[0]?.properties.windSpeed.value / 1.60934),
windDirection: weather.features[0]?.properties.windDirection.value,
+ pressure: Math.round(
+ weather.features[0]?.properties.barometricPressure.value / 3386.38,
+ ),
});
}
}, [weather, basicWeather]);
@@ -179,6 +204,16 @@ export function Homepage() {
windDirection: forecast.properties.periods[1].windDirection,
},
]);
+ setExtendedForecast(
+ forecast.properties.periods.map((item: any) => ({
+ detailedForecast: item.detailedForecast,
+ isDaytime: item.isDaytime,
+ name: item.name,
+ shortForecast: item.shortForecast,
+ temperature: item.temperature,
+ temperatureUnit: item.temperatureUnit,
+ })),
+ );
setCurrentLocation({
office: points?.properties.gridId,
station: stations?.features[0].properties.stationIdentifier,
@@ -189,11 +224,13 @@ export function Homepage() {
});
}
}, [forecast]);
+
return (
diff --git a/src/pages/homepage/components/Search/Search.tsx b/src/pages/homepage/components/Search/Search.tsx
index a48d348..e695b38 100644
--- a/src/pages/homepage/components/Search/Search.tsx
+++ b/src/pages/homepage/components/Search/Search.tsx
@@ -8,6 +8,7 @@ import { textInputClasses } from 'styles';
export default function Search({
current,
outlook,
+ extendedForecast,
weatherModalOpened,
closeWeatherModal,
openWeatherModal,
@@ -161,6 +162,7 @@ export default function Search({
diff --git a/src/types/weather.ts b/src/types/weather.ts
index afc01b5..d230f76 100644
--- a/src/types/weather.ts
+++ b/src/types/weather.ts
@@ -1,13 +1,16 @@
export interface CurrentWeather {
icon: string;
+ textDescription: string;
temperature: number;
highTemp: number;
lowTemp: number;
precipitation: string;
humidity: number;
- heatIndex: number;
+ heatIndex: number | null;
+ windChill: number | null;
windSpeed: number;
windDirection: number;
+ pressure: number;
}
export interface OutlookWeather {
@@ -19,3 +22,12 @@ export interface OutlookWeather {
windSpeed: string;
windDirection: string;
}
+
+export interface ExtendedForecast {
+ detailedForecast: string;
+ isDaytime: boolean;
+ name: string;
+ shortForecast: string;
+ temperature: number;
+ temperatureUnit: string;
+}