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 ( + SunnyIcon + ); + case 'Cloudy': + return ( + CloudyIcon + ); + case 'Mostly Cloudy': + return ( + CloudyIcon + ); + case 'Rain': + return ( + RainIcon + ); + case 'Showers': + return ( + RainIcon + ); + case 'Drizzle': + return ( + RainIcon + ); + case 'Snow': + return ( + SnowIcon + ); + case 'Thunderstorm': + return ( + ThunderstormIcon + ); + case 'Mist': + return ( + HazeIcon + ); + case 'Haze': + return ( + HazeIcon + ); + case 'Dust': + return ( + HazeIcon + ); + case 'Fog': + return ( + HazeIcon + ); + case 'Smoke': + return ( + HazeIcon + ); + case 'Sand': + return ( + HazeIcon + ); + case 'Ash': + return ( + HazeIcon + ); + case 'Squall': + return ( + HazeIcon + ); + case 'Tornado': + return ( + HazeIcon + ); + case 'Freezing Rain': + return ( + HazeIcon + ); + case 'Shower Rain': + return ( + HazeIcon + ); + case 'Heavy Rain': + return ( + HeavyRainIcon + ); + case 'Scattered Clouds': + return ( + PartlyCloudyIcon + ); + case 'Broken Clouds': + return ( + PartlyCloudyIcon + ); + case 'Overcast Clouds': + return ( + PartlyCloudyIcon + ); + case 'Moderate Rain': + return ( + PartlyCloudyIcon + ); + case 'Light Rain': + return ( + PartlyCloudyIcon + ); + case 'Heavy Snow': + return ( + SnowIcon + ); + case 'Sleet': + return ( + SleetIcon + ); + case 'Partly Sunny': + return ( + PartlyCloudyIcon + ); + default: + return ( + SunnyIcon + ); + } + }; + + 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; +}