diff options
| -rw-r--r-- | frontend/package-lock.json | 347 | ||||
| -rw-r--r-- | frontend/package.json | 5 | ||||
| -rw-r--r-- | frontend/src/api/Api.ts | 4 | ||||
| -rw-r--r-- | frontend/src/api/Stats.ts | 52 | ||||
| -rw-r--r-- | frontend/src/css/Homepage.css | 545 | ||||
| -rw-r--r-- | frontend/src/pages/Homepage.tsx | 240 |
6 files changed, 1183 insertions, 10 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cc1e3e8..7a122c1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json | |||
| @@ -17,7 +17,8 @@ | |||
| 17 | "react-dom": "^18.3.1", | 17 | "react-dom": "^18.3.1", |
| 18 | "react-helmet": "^6.1.0", | 18 | "react-helmet": "^6.1.0", |
| 19 | "react-markdown": "^9.0.1", | 19 | "react-markdown": "^9.0.1", |
| 20 | "react-router-dom": "^6.26.1" | 20 | "react-router-dom": "^6.26.1", |
| 21 | "recharts": "^3.3.0" | ||
| 21 | }, | 22 | }, |
| 22 | "devDependencies": { | 23 | "devDependencies": { |
| 23 | "@eslint/js": "^9.38.0", | 24 | "@eslint/js": "^9.38.0", |
| @@ -900,6 +901,31 @@ | |||
| 900 | "node": ">= 8" | 901 | "node": ">= 8" |
| 901 | } | 902 | } |
| 902 | }, | 903 | }, |
| 904 | "node_modules/@reduxjs/toolkit": { | ||
| 905 | "version": "2.9.2", | ||
| 906 | "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz", | ||
| 907 | "integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==", | ||
| 908 | "dependencies": { | ||
| 909 | "@standard-schema/spec": "^1.0.0", | ||
| 910 | "@standard-schema/utils": "^0.3.0", | ||
| 911 | "immer": "^10.0.3", | ||
| 912 | "redux": "^5.0.1", | ||
| 913 | "redux-thunk": "^3.1.0", | ||
| 914 | "reselect": "^5.1.0" | ||
| 915 | }, | ||
| 916 | "peerDependencies": { | ||
| 917 | "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", | ||
| 918 | "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" | ||
| 919 | }, | ||
| 920 | "peerDependenciesMeta": { | ||
| 921 | "react": { | ||
| 922 | "optional": true | ||
| 923 | }, | ||
| 924 | "react-redux": { | ||
| 925 | "optional": true | ||
| 926 | } | ||
| 927 | } | ||
| 928 | }, | ||
| 903 | "node_modules/@remix-run/router": { | 929 | "node_modules/@remix-run/router": { |
| 904 | "version": "1.23.0", | 930 | "version": "1.23.0", |
| 905 | "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", | 931 | "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", |
| @@ -1224,6 +1250,16 @@ | |||
| 1224 | "win32" | 1250 | "win32" |
| 1225 | ] | 1251 | ] |
| 1226 | }, | 1252 | }, |
| 1253 | "node_modules/@standard-schema/spec": { | ||
| 1254 | "version": "1.0.0", | ||
| 1255 | "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", | ||
| 1256 | "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" | ||
| 1257 | }, | ||
| 1258 | "node_modules/@standard-schema/utils": { | ||
| 1259 | "version": "0.3.0", | ||
| 1260 | "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", | ||
| 1261 | "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" | ||
| 1262 | }, | ||
| 1227 | "node_modules/@types/babel__core": { | 1263 | "node_modules/@types/babel__core": { |
| 1228 | "version": "7.20.5", | 1264 | "version": "7.20.5", |
| 1229 | "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", | 1265 | "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", |
| @@ -1269,6 +1305,60 @@ | |||
| 1269 | "@babel/types": "^7.28.2" | 1305 | "@babel/types": "^7.28.2" |
| 1270 | } | 1306 | } |
| 1271 | }, | 1307 | }, |
| 1308 | "node_modules/@types/d3-array": { | ||
| 1309 | "version": "3.2.2", | ||
| 1310 | "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", | ||
| 1311 | "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" | ||
| 1312 | }, | ||
| 1313 | "node_modules/@types/d3-color": { | ||
| 1314 | "version": "3.1.3", | ||
| 1315 | "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", | ||
| 1316 | "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" | ||
| 1317 | }, | ||
| 1318 | "node_modules/@types/d3-ease": { | ||
| 1319 | "version": "3.0.2", | ||
| 1320 | "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", | ||
| 1321 | "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" | ||
| 1322 | }, | ||
| 1323 | "node_modules/@types/d3-interpolate": { | ||
| 1324 | "version": "3.0.4", | ||
| 1325 | "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", | ||
| 1326 | "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", | ||
| 1327 | "dependencies": { | ||
| 1328 | "@types/d3-color": "*" | ||
| 1329 | } | ||
| 1330 | }, | ||
| 1331 | "node_modules/@types/d3-path": { | ||
| 1332 | "version": "3.1.1", | ||
| 1333 | "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", | ||
| 1334 | "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" | ||
| 1335 | }, | ||
| 1336 | "node_modules/@types/d3-scale": { | ||
| 1337 | "version": "4.0.9", | ||
| 1338 | "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", | ||
| 1339 | "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", | ||
| 1340 | "dependencies": { | ||
| 1341 | "@types/d3-time": "*" | ||
| 1342 | } | ||
| 1343 | }, | ||
| 1344 | "node_modules/@types/d3-shape": { | ||
| 1345 | "version": "3.1.7", | ||
| 1346 | "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", | ||
| 1347 | "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", | ||
| 1348 | "dependencies": { | ||
| 1349 | "@types/d3-path": "*" | ||
| 1350 | } | ||
| 1351 | }, | ||
| 1352 | "node_modules/@types/d3-time": { | ||
| 1353 | "version": "3.0.4", | ||
| 1354 | "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", | ||
| 1355 | "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" | ||
| 1356 | }, | ||
| 1357 | "node_modules/@types/d3-timer": { | ||
| 1358 | "version": "3.0.2", | ||
| 1359 | "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", | ||
| 1360 | "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" | ||
| 1361 | }, | ||
| 1272 | "node_modules/@types/debug": { | 1362 | "node_modules/@types/debug": { |
| 1273 | "version": "4.1.12", | 1363 | "version": "4.1.12", |
| 1274 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", | 1364 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", |
| @@ -1367,6 +1457,11 @@ | |||
| 1367 | "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", | 1457 | "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", |
| 1368 | "license": "MIT" | 1458 | "license": "MIT" |
| 1369 | }, | 1459 | }, |
| 1460 | "node_modules/@types/use-sync-external-store": { | ||
| 1461 | "version": "0.0.6", | ||
| 1462 | "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", | ||
| 1463 | "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" | ||
| 1464 | }, | ||
| 1370 | "node_modules/@typescript-eslint/eslint-plugin": { | 1465 | "node_modules/@typescript-eslint/eslint-plugin": { |
| 1371 | "version": "8.46.2", | 1466 | "version": "8.46.2", |
| 1372 | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", | 1467 | "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", |
| @@ -1964,6 +2059,14 @@ | |||
| 1964 | "url": "https://github.com/sponsors/wooorm" | 2059 | "url": "https://github.com/sponsors/wooorm" |
| 1965 | } | 2060 | } |
| 1966 | }, | 2061 | }, |
| 2062 | "node_modules/clsx": { | ||
| 2063 | "version": "2.1.1", | ||
| 2064 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", | ||
| 2065 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", | ||
| 2066 | "engines": { | ||
| 2067 | "node": ">=6" | ||
| 2068 | } | ||
| 2069 | }, | ||
| 1967 | "node_modules/color-convert": { | 2070 | "node_modules/color-convert": { |
| 1968 | "version": "2.0.1", | 2071 | "version": "2.0.1", |
| 1969 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", | 2072 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", |
| @@ -2041,6 +2144,116 @@ | |||
| 2041 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", | 2144 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", |
| 2042 | "license": "MIT" | 2145 | "license": "MIT" |
| 2043 | }, | 2146 | }, |
| 2147 | "node_modules/d3-array": { | ||
| 2148 | "version": "3.2.4", | ||
| 2149 | "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", | ||
| 2150 | "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", | ||
| 2151 | "dependencies": { | ||
| 2152 | "internmap": "1 - 2" | ||
| 2153 | }, | ||
| 2154 | "engines": { | ||
| 2155 | "node": ">=12" | ||
| 2156 | } | ||
| 2157 | }, | ||
| 2158 | "node_modules/d3-color": { | ||
| 2159 | "version": "3.1.0", | ||
| 2160 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", | ||
| 2161 | "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", | ||
| 2162 | "engines": { | ||
| 2163 | "node": ">=12" | ||
| 2164 | } | ||
| 2165 | }, | ||
| 2166 | "node_modules/d3-ease": { | ||
| 2167 | "version": "3.0.1", | ||
| 2168 | "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", | ||
| 2169 | "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", | ||
| 2170 | "engines": { | ||
| 2171 | "node": ">=12" | ||
| 2172 | } | ||
| 2173 | }, | ||
| 2174 | "node_modules/d3-format": { | ||
| 2175 | "version": "3.1.0", | ||
| 2176 | "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", | ||
| 2177 | "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", | ||
| 2178 | "engines": { | ||
| 2179 | "node": ">=12" | ||
| 2180 | } | ||
| 2181 | }, | ||
| 2182 | "node_modules/d3-interpolate": { | ||
| 2183 | "version": "3.0.1", | ||
| 2184 | "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", | ||
| 2185 | "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", | ||
| 2186 | "dependencies": { | ||
| 2187 | "d3-color": "1 - 3" | ||
| 2188 | }, | ||
| 2189 | "engines": { | ||
| 2190 | "node": ">=12" | ||
| 2191 | } | ||
| 2192 | }, | ||
| 2193 | "node_modules/d3-path": { | ||
| 2194 | "version": "3.1.0", | ||
| 2195 | "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", | ||
| 2196 | "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", | ||
| 2197 | "engines": { | ||
| 2198 | "node": ">=12" | ||
| 2199 | } | ||
| 2200 | }, | ||
| 2201 | "node_modules/d3-scale": { | ||
| 2202 | "version": "4.0.2", | ||
| 2203 | "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", | ||
| 2204 | "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", | ||
| 2205 | "dependencies": { | ||
| 2206 | "d3-array": "2.10.0 - 3", | ||
| 2207 | "d3-format": "1 - 3", | ||
| 2208 | "d3-interpolate": "1.2.0 - 3", | ||
| 2209 | "d3-time": "2.1.1 - 3", | ||
| 2210 | "d3-time-format": "2 - 4" | ||
| 2211 | }, | ||
| 2212 | "engines": { | ||
| 2213 | "node": ">=12" | ||
| 2214 | } | ||
| 2215 | }, | ||
| 2216 | "node_modules/d3-shape": { | ||
| 2217 | "version": "3.2.0", | ||
| 2218 | "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", | ||
| 2219 | "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", | ||
| 2220 | "dependencies": { | ||
| 2221 | "d3-path": "^3.1.0" | ||
| 2222 | }, | ||
| 2223 | "engines": { | ||
| 2224 | "node": ">=12" | ||
| 2225 | } | ||
| 2226 | }, | ||
| 2227 | "node_modules/d3-time": { | ||
| 2228 | "version": "3.1.0", | ||
| 2229 | "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", | ||
| 2230 | "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", | ||
| 2231 | "dependencies": { | ||
| 2232 | "d3-array": "2 - 3" | ||
| 2233 | }, | ||
| 2234 | "engines": { | ||
| 2235 | "node": ">=12" | ||
| 2236 | } | ||
| 2237 | }, | ||
| 2238 | "node_modules/d3-time-format": { | ||
| 2239 | "version": "4.1.0", | ||
| 2240 | "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", | ||
| 2241 | "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", | ||
| 2242 | "dependencies": { | ||
| 2243 | "d3-time": "1 - 3" | ||
| 2244 | }, | ||
| 2245 | "engines": { | ||
| 2246 | "node": ">=12" | ||
| 2247 | } | ||
| 2248 | }, | ||
| 2249 | "node_modules/d3-timer": { | ||
| 2250 | "version": "3.0.1", | ||
| 2251 | "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", | ||
| 2252 | "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", | ||
| 2253 | "engines": { | ||
| 2254 | "node": ">=12" | ||
| 2255 | } | ||
| 2256 | }, | ||
| 2044 | "node_modules/debug": { | 2257 | "node_modules/debug": { |
| 2045 | "version": "4.4.3", | 2258 | "version": "4.4.3", |
| 2046 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", | 2259 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", |
| @@ -2058,6 +2271,11 @@ | |||
| 2058 | } | 2271 | } |
| 2059 | } | 2272 | } |
| 2060 | }, | 2273 | }, |
| 2274 | "node_modules/decimal.js-light": { | ||
| 2275 | "version": "2.5.1", | ||
| 2276 | "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", | ||
| 2277 | "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" | ||
| 2278 | }, | ||
| 2061 | "node_modules/decode-named-character-reference": { | 2279 | "node_modules/decode-named-character-reference": { |
| 2062 | "version": "1.2.0", | 2280 | "version": "1.2.0", |
| 2063 | "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", | 2281 | "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", |
| @@ -2188,6 +2406,11 @@ | |||
| 2188 | "node": ">= 0.4" | 2406 | "node": ">= 0.4" |
| 2189 | } | 2407 | } |
| 2190 | }, | 2408 | }, |
| 2409 | "node_modules/es-toolkit": { | ||
| 2410 | "version": "1.41.0", | ||
| 2411 | "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", | ||
| 2412 | "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==" | ||
| 2413 | }, | ||
| 2191 | "node_modules/esbuild": { | 2414 | "node_modules/esbuild": { |
| 2192 | "version": "0.21.5", | 2415 | "version": "0.21.5", |
| 2193 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", | 2416 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", |
| @@ -2421,6 +2644,11 @@ | |||
| 2421 | "node": ">=0.10.0" | 2644 | "node": ">=0.10.0" |
| 2422 | } | 2645 | } |
| 2423 | }, | 2646 | }, |
| 2647 | "node_modules/eventemitter3": { | ||
| 2648 | "version": "5.0.1", | ||
| 2649 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", | ||
| 2650 | "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" | ||
| 2651 | }, | ||
| 2424 | "node_modules/extend": { | 2652 | "node_modules/extend": { |
| 2425 | "version": "3.0.2", | 2653 | "version": "3.0.2", |
| 2426 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", | 2654 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", |
| @@ -2846,6 +3074,15 @@ | |||
| 2846 | "node": ">= 4" | 3074 | "node": ">= 4" |
| 2847 | } | 3075 | } |
| 2848 | }, | 3076 | }, |
| 3077 | "node_modules/immer": { | ||
| 3078 | "version": "10.2.0", | ||
| 3079 | "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", | ||
| 3080 | "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", | ||
| 3081 | "funding": { | ||
| 3082 | "type": "opencollective", | ||
| 3083 | "url": "https://opencollective.com/immer" | ||
| 3084 | } | ||
| 3085 | }, | ||
| 2849 | "node_modules/import-fresh": { | 3086 | "node_modules/import-fresh": { |
| 2850 | "version": "3.3.1", | 3087 | "version": "3.3.1", |
| 2851 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", | 3088 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", |
| @@ -2898,6 +3135,14 @@ | |||
| 2898 | "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", | 3135 | "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", |
| 2899 | "license": "MIT" | 3136 | "license": "MIT" |
| 2900 | }, | 3137 | }, |
| 3138 | "node_modules/internmap": { | ||
| 3139 | "version": "2.0.3", | ||
| 3140 | "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", | ||
| 3141 | "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", | ||
| 3142 | "engines": { | ||
| 3143 | "node": ">=12" | ||
| 3144 | } | ||
| 3145 | }, | ||
| 2901 | "node_modules/is-alphabetical": { | 3146 | "node_modules/is-alphabetical": { |
| 2902 | "version": "2.0.1", | 3147 | "version": "2.0.1", |
| 2903 | "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", | 3148 | "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", |
| @@ -4183,6 +4428,28 @@ | |||
| 4183 | "react": ">=18" | 4428 | "react": ">=18" |
| 4184 | } | 4429 | } |
| 4185 | }, | 4430 | }, |
| 4431 | "node_modules/react-redux": { | ||
| 4432 | "version": "9.2.0", | ||
| 4433 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", | ||
| 4434 | "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", | ||
| 4435 | "dependencies": { | ||
| 4436 | "@types/use-sync-external-store": "^0.0.6", | ||
| 4437 | "use-sync-external-store": "^1.4.0" | ||
| 4438 | }, | ||
| 4439 | "peerDependencies": { | ||
| 4440 | "@types/react": "^18.2.25 || ^19", | ||
| 4441 | "react": "^18.0 || ^19", | ||
| 4442 | "redux": "^5.0.0" | ||
| 4443 | }, | ||
| 4444 | "peerDependenciesMeta": { | ||
| 4445 | "@types/react": { | ||
| 4446 | "optional": true | ||
| 4447 | }, | ||
| 4448 | "redux": { | ||
| 4449 | "optional": true | ||
| 4450 | } | ||
| 4451 | } | ||
| 4452 | }, | ||
| 4186 | "node_modules/react-refresh": { | 4453 | "node_modules/react-refresh": { |
| 4187 | "version": "0.17.0", | 4454 | "version": "0.17.0", |
| 4188 | "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", | 4455 | "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", |
| @@ -4234,6 +4501,45 @@ | |||
| 4234 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0" | 4501 | "react": "^16.3.0 || ^17.0.0 || ^18.0.0" |
| 4235 | } | 4502 | } |
| 4236 | }, | 4503 | }, |
| 4504 | "node_modules/recharts": { | ||
| 4505 | "version": "3.3.0", | ||
| 4506 | "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", | ||
| 4507 | "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", | ||
| 4508 | "dependencies": { | ||
| 4509 | "@reduxjs/toolkit": "1.x.x || 2.x.x", | ||
| 4510 | "clsx": "^2.1.1", | ||
| 4511 | "decimal.js-light": "^2.5.1", | ||
| 4512 | "es-toolkit": "^1.39.3", | ||
| 4513 | "eventemitter3": "^5.0.1", | ||
| 4514 | "immer": "^10.1.1", | ||
| 4515 | "react-redux": "8.x.x || 9.x.x", | ||
| 4516 | "reselect": "5.1.1", | ||
| 4517 | "tiny-invariant": "^1.3.3", | ||
| 4518 | "use-sync-external-store": "^1.2.2", | ||
| 4519 | "victory-vendor": "^37.0.2" | ||
| 4520 | }, | ||
| 4521 | "engines": { | ||
| 4522 | "node": ">=18" | ||
| 4523 | }, | ||
| 4524 | "peerDependencies": { | ||
| 4525 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", | ||
| 4526 | "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", | ||
| 4527 | "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" | ||
| 4528 | } | ||
| 4529 | }, | ||
| 4530 | "node_modules/redux": { | ||
| 4531 | "version": "5.0.1", | ||
| 4532 | "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", | ||
| 4533 | "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" | ||
| 4534 | }, | ||
| 4535 | "node_modules/redux-thunk": { | ||
| 4536 | "version": "3.1.0", | ||
| 4537 | "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", | ||
| 4538 | "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", | ||
| 4539 | "peerDependencies": { | ||
| 4540 | "redux": "^5.0.0" | ||
| 4541 | } | ||
| 4542 | }, | ||
| 4237 | "node_modules/remark-parse": { | 4543 | "node_modules/remark-parse": { |
| 4238 | "version": "11.0.0", | 4544 | "version": "11.0.0", |
| 4239 | "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", | 4545 | "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", |
| @@ -4267,6 +4573,11 @@ | |||
| 4267 | "url": "https://opencollective.com/unified" | 4573 | "url": "https://opencollective.com/unified" |
| 4268 | } | 4574 | } |
| 4269 | }, | 4575 | }, |
| 4576 | "node_modules/reselect": { | ||
| 4577 | "version": "5.1.1", | ||
| 4578 | "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", | ||
| 4579 | "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" | ||
| 4580 | }, | ||
| 4270 | "node_modules/resolve-from": { | 4581 | "node_modules/resolve-from": { |
| 4271 | "version": "4.0.0", | 4582 | "version": "4.0.0", |
| 4272 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", | 4583 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", |
| @@ -4511,6 +4822,11 @@ | |||
| 4511 | "dev": true, | 4822 | "dev": true, |
| 4512 | "license": "MIT" | 4823 | "license": "MIT" |
| 4513 | }, | 4824 | }, |
| 4825 | "node_modules/tiny-invariant": { | ||
| 4826 | "version": "1.3.3", | ||
| 4827 | "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", | ||
| 4828 | "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" | ||
| 4829 | }, | ||
| 4514 | "node_modules/to-regex-range": { | 4830 | "node_modules/to-regex-range": { |
| 4515 | "version": "5.0.1", | 4831 | "version": "5.0.1", |
| 4516 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | 4832 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", |
| @@ -4755,6 +5071,14 @@ | |||
| 4755 | "punycode": "^2.1.0" | 5071 | "punycode": "^2.1.0" |
| 4756 | } | 5072 | } |
| 4757 | }, | 5073 | }, |
| 5074 | "node_modules/use-sync-external-store": { | ||
| 5075 | "version": "1.6.0", | ||
| 5076 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", | ||
| 5077 | "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", | ||
| 5078 | "peerDependencies": { | ||
| 5079 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" | ||
| 5080 | } | ||
| 5081 | }, | ||
| 4758 | "node_modules/vfile": { | 5082 | "node_modules/vfile": { |
| 4759 | "version": "6.0.3", | 5083 | "version": "6.0.3", |
| 4760 | "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", | 5084 | "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", |
| @@ -4783,6 +5107,27 @@ | |||
| 4783 | "url": "https://opencollective.com/unified" | 5107 | "url": "https://opencollective.com/unified" |
| 4784 | } | 5108 | } |
| 4785 | }, | 5109 | }, |
| 5110 | "node_modules/victory-vendor": { | ||
| 5111 | "version": "37.3.6", | ||
| 5112 | "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", | ||
| 5113 | "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", | ||
| 5114 | "dependencies": { | ||
| 5115 | "@types/d3-array": "^3.0.3", | ||
| 5116 | "@types/d3-ease": "^3.0.0", | ||
| 5117 | "@types/d3-interpolate": "^3.0.1", | ||
| 5118 | "@types/d3-scale": "^4.0.2", | ||
| 5119 | "@types/d3-shape": "^3.1.0", | ||
| 5120 | "@types/d3-time": "^3.0.0", | ||
| 5121 | "@types/d3-timer": "^3.0.0", | ||
| 5122 | "d3-array": "^3.1.6", | ||
| 5123 | "d3-ease": "^3.0.1", | ||
| 5124 | "d3-interpolate": "^3.0.1", | ||
| 5125 | "d3-scale": "^4.0.2", | ||
| 5126 | "d3-shape": "^3.1.0", | ||
| 5127 | "d3-time": "^3.0.0", | ||
| 5128 | "d3-timer": "^3.0.1" | ||
| 5129 | } | ||
| 5130 | }, | ||
| 4786 | "node_modules/vite": { | 5131 | "node_modules/vite": { |
| 4787 | "version": "5.4.21", | 5132 | "version": "5.4.21", |
| 4788 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", | 5133 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", |
diff --git a/frontend/package.json b/frontend/package.json index 946ee40..46bb5ad 100644 --- a/frontend/package.json +++ b/frontend/package.json | |||
| @@ -13,7 +13,8 @@ | |||
| 13 | "react-dom": "^18.3.1", | 13 | "react-dom": "^18.3.1", |
| 14 | "react-helmet": "^6.1.0", | 14 | "react-helmet": "^6.1.0", |
| 15 | "react-markdown": "^9.0.1", | 15 | "react-markdown": "^9.0.1", |
| 16 | "react-router-dom": "^6.26.1" | 16 | "react-router-dom": "^6.26.1", |
| 17 | "recharts": "^3.3.0" | ||
| 17 | }, | 18 | }, |
| 18 | "scripts": { | 19 | "scripts": { |
| 19 | "dev": "vite", | 20 | "dev": "vite", |
| @@ -32,4 +33,4 @@ | |||
| 32 | "typescript-eslint": "^8.46.2", | 33 | "typescript-eslint": "^8.46.2", |
| 33 | "vite": "^5.4.2" | 34 | "vite": "^5.4.2" |
| 34 | } | 35 | } |
| 35 | } \ No newline at end of file | 36 | } |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index dd5076a..4385f2c 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -5,6 +5,7 @@ import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search | |||
| 5 | import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; | 5 | import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; |
| 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from "@api/Maps"; | 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from "@api/Maps"; |
| 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from "@api/Mod"; | 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from "@api/Mod"; |
| 8 | import { get_portal_count_history, get_recent_scores } from "@api/Stats"; | ||
| 8 | import { UploadRunContent } from "@customTypes/Content"; | 9 | import { UploadRunContent } from "@customTypes/Content"; |
| 9 | 10 | ||
| 10 | // add new api call function entries here | 11 | // add new api call function entries here |
| @@ -47,6 +48,9 @@ export const API = { | |||
| 47 | put_map_summary: (token: string, map_id: string, content: ModMenuContent) => put_map_summary(token, map_id, content), | 48 | put_map_summary: (token: string, map_id: string, content: ModMenuContent) => put_map_summary(token, map_id, content), |
| 48 | 49 | ||
| 49 | delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), | 50 | delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), |
| 51 | // Stats | ||
| 52 | get_portal_count_history: () => get_portal_count_history(), | ||
| 53 | get_recent_scores: () => get_recent_scores(), | ||
| 50 | }; | 54 | }; |
| 51 | 55 | ||
| 52 | const BASE_API_URL: string = import.meta.env.DEV | 56 | const BASE_API_URL: string = import.meta.env.DEV |
diff --git a/frontend/src/api/Stats.ts b/frontend/src/api/Stats.ts new file mode 100644 index 0000000..21654b5 --- /dev/null +++ b/frontend/src/api/Stats.ts | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | import axios from "axios"; | ||
| 2 | import { url } from "./Api"; | ||
| 3 | |||
| 4 | export interface PortalCountData { | ||
| 5 | date: string; | ||
| 6 | count: number; | ||
| 7 | } | ||
| 8 | |||
| 9 | export interface RecordsTimelineResponse { | ||
| 10 | timeline_singleplayer: PortalCountData[]; | ||
| 11 | timeline_multiplayer: PortalCountData[]; | ||
| 12 | } | ||
| 13 | |||
| 14 | export interface ScoreLog { | ||
| 15 | game: { | ||
| 16 | id: number; | ||
| 17 | name: string; | ||
| 18 | image: string; | ||
| 19 | is_coop: boolean; | ||
| 20 | category_portals: null; | ||
| 21 | }; | ||
| 22 | user: { | ||
| 23 | steam_id: string; | ||
| 24 | user_name: string; | ||
| 25 | }; | ||
| 26 | map: { | ||
| 27 | id: number; | ||
| 28 | name: string; | ||
| 29 | image: string; | ||
| 30 | is_disabled: boolean; | ||
| 31 | portal_count: number; | ||
| 32 | difficulty: number; | ||
| 33 | }; | ||
| 34 | score_count: number; | ||
| 35 | } | ||
| 36 | |||
| 37 | export async function get_portal_count_history(): Promise<RecordsTimelineResponse | undefined> { | ||
| 38 | const response = await axios.get(url("stats/timeline")); | ||
| 39 | if (!response.data.data) { | ||
| 40 | return undefined; | ||
| 41 | } | ||
| 42 | return response.data.data; | ||
| 43 | } | ||
| 44 | |||
| 45 | export async function get_recent_scores(): Promise<ScoreLog[]> { | ||
| 46 | const response = await axios.get(url("stats/scores")); | ||
| 47 | if (!response.data.data) { | ||
| 48 | return []; | ||
| 49 | } | ||
| 50 | return response.data.data.scores.slice(0, 5); | ||
| 51 | } | ||
| 52 | |||
diff --git a/frontend/src/css/Homepage.css b/frontend/src/css/Homepage.css new file mode 100644 index 0000000..b89602e --- /dev/null +++ b/frontend/src/css/Homepage.css | |||
| @@ -0,0 +1,545 @@ | |||
| 1 | .hero-section { | ||
| 2 | text-align: center; | ||
| 3 | padding: 20px 20px; | ||
| 4 | margin: 20px; | ||
| 5 | background: #202232; | ||
| 6 | border-radius: 24px; | ||
| 7 | } | ||
| 8 | |||
| 9 | .hero-content { | ||
| 10 | max-width: 800px; | ||
| 11 | margin: 0 auto; | ||
| 12 | } | ||
| 13 | |||
| 14 | .hero-title { | ||
| 15 | font-size: 56px; | ||
| 16 | font-family: BarlowCondensed-Bold; | ||
| 17 | color: #FFF; | ||
| 18 | margin-bottom: 20px; | ||
| 19 | line-height: 1.2; | ||
| 20 | } | ||
| 21 | |||
| 22 | .hero-subtitle { | ||
| 23 | font-size: 24px; | ||
| 24 | font-family: BarlowSemiCondensed-Regular; | ||
| 25 | color: #CDCFDF; | ||
| 26 | margin: 0; | ||
| 27 | } | ||
| 28 | |||
| 29 | .stats-section { | ||
| 30 | margin: 20px; | ||
| 31 | } | ||
| 32 | |||
| 33 | .stats-grid { | ||
| 34 | display: grid; | ||
| 35 | grid-template-columns: 4fr 1fr; | ||
| 36 | gap: 20px; | ||
| 37 | } | ||
| 38 | |||
| 39 | .stats-container { | ||
| 40 | background: #202232; | ||
| 41 | border-radius: 24px; | ||
| 42 | padding: 20px; | ||
| 43 | } | ||
| 44 | |||
| 45 | .stats-header { | ||
| 46 | text-align: center; | ||
| 47 | margin-bottom: 30px; | ||
| 48 | } | ||
| 49 | |||
| 50 | .stats-header h3 { | ||
| 51 | font-size: 32px; | ||
| 52 | font-family: BarlowCondensed-Bold; | ||
| 53 | color: #FFF; | ||
| 54 | margin-top: 0px; | ||
| 55 | margin-bottom: 10px; | ||
| 56 | } | ||
| 57 | |||
| 58 | .stats-header p { | ||
| 59 | color: #CDCFDF; | ||
| 60 | font-size: 20px; | ||
| 61 | font-family: BarlowSemiCondensed-Regular; | ||
| 62 | margin: 0; | ||
| 63 | } | ||
| 64 | |||
| 65 | /* Chart Wrapper */ | ||
| 66 | .chart-wrapper { | ||
| 67 | background: #2b2e46; | ||
| 68 | border-radius: 20px; | ||
| 69 | padding: 20px 10px; | ||
| 70 | margin-top: 20px; | ||
| 71 | } | ||
| 72 | |||
| 73 | .chart-loading { | ||
| 74 | display: flex; | ||
| 75 | flex-direction: column; | ||
| 76 | align-items: center; | ||
| 77 | justify-content: center; | ||
| 78 | padding: 60px 20px; | ||
| 79 | color: #CDCFDF; | ||
| 80 | } | ||
| 81 | |||
| 82 | .loading-spinner { | ||
| 83 | width: 48px; | ||
| 84 | height: 48px; | ||
| 85 | border: 5px solid #FFF; | ||
| 86 | border-bottom-color: transparent; | ||
| 87 | border-radius: 50%; | ||
| 88 | display: inline-block; | ||
| 89 | box-sizing: border-box; | ||
| 90 | animation: rotation 1s linear infinite; | ||
| 91 | margin-bottom: 20px; | ||
| 92 | } | ||
| 93 | |||
| 94 | @keyframes rotation { | ||
| 95 | 0% { | ||
| 96 | transform: rotate(0deg); | ||
| 97 | } | ||
| 98 | |||
| 99 | 100% { | ||
| 100 | transform: rotate(360deg); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | .chart-loading p { | ||
| 105 | font-size: 20px; | ||
| 106 | font-family: BarlowSemiCondensed-Regular; | ||
| 107 | margin: 0; | ||
| 108 | } | ||
| 109 | |||
| 110 | .chart-empty { | ||
| 111 | text-align: center; | ||
| 112 | padding: 60px 20px; | ||
| 113 | color: #CDCFDF; | ||
| 114 | font-size: 20px; | ||
| 115 | font-family: BarlowSemiCondensed-Regular; | ||
| 116 | } | ||
| 117 | |||
| 118 | /* Custom Tooltip */ | ||
| 119 | .custom-tooltip { | ||
| 120 | background: #2b2e46; | ||
| 121 | border-radius: 12px; | ||
| 122 | padding: 12px 16px; | ||
| 123 | } | ||
| 124 | |||
| 125 | .tooltip-date { | ||
| 126 | color: #CDCFDF; | ||
| 127 | font-family: BarlowSemiCondensed-Regular; | ||
| 128 | font-size: 16px; | ||
| 129 | margin: 0 0 4px 0; | ||
| 130 | } | ||
| 131 | |||
| 132 | .tooltip-count { | ||
| 133 | color: #FFF; | ||
| 134 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 135 | font-size: 22px; | ||
| 136 | margin: 0; | ||
| 137 | } | ||
| 138 | |||
| 139 | /* Mode Toggle Buttons */ | ||
| 140 | .mode-toggle-container { | ||
| 141 | display: flex; | ||
| 142 | justify-content: center; | ||
| 143 | margin-top: 20px; | ||
| 144 | } | ||
| 145 | |||
| 146 | .mode-toggle-button { | ||
| 147 | background-color: #2b2e46; | ||
| 148 | padding: 10px 20px; | ||
| 149 | border: 0; | ||
| 150 | color: #cdcfdf; | ||
| 151 | cursor: pointer; | ||
| 152 | font-family: BarlowSemiCondensed-Regular; | ||
| 153 | font-size: 24px; | ||
| 154 | transition: all 0.1s; | ||
| 155 | flex: 1; | ||
| 156 | max-width: 150px; | ||
| 157 | } | ||
| 158 | |||
| 159 | .mode-toggle-button:first-child { | ||
| 160 | border-radius: 5px 0 0 5px; | ||
| 161 | } | ||
| 162 | |||
| 163 | .mode-toggle-button:last-child { | ||
| 164 | border-radius: 0 5px 5px 0; | ||
| 165 | } | ||
| 166 | |||
| 167 | .mode-toggle-button:hover, | ||
| 168 | .mode-toggle-button.selected { | ||
| 169 | background-color: #202232; | ||
| 170 | } | ||
| 171 | |||
| 172 | /* Recent Scores */ | ||
| 173 | .recent-scores-container { | ||
| 174 | background: #202232; | ||
| 175 | border-radius: 24px; | ||
| 176 | padding: 20px; | ||
| 177 | display: flex; | ||
| 178 | flex-direction: column; | ||
| 179 | } | ||
| 180 | |||
| 181 | .recent-scores-header { | ||
| 182 | text-align: center; | ||
| 183 | margin-bottom: 20px; | ||
| 184 | } | ||
| 185 | |||
| 186 | .recent-scores-header h3 { | ||
| 187 | font-size: 32px; | ||
| 188 | font-family: BarlowCondensed-Bold; | ||
| 189 | color: #FFF; | ||
| 190 | margin: 0; | ||
| 191 | } | ||
| 192 | |||
| 193 | .scores-loading { | ||
| 194 | display: flex; | ||
| 195 | justify-content: center; | ||
| 196 | align-items: center; | ||
| 197 | flex: 1; | ||
| 198 | padding: 40px 20px; | ||
| 199 | } | ||
| 200 | |||
| 201 | .recent-scores-list { | ||
| 202 | display: flex; | ||
| 203 | flex-direction: column; | ||
| 204 | gap: 10px; | ||
| 205 | } | ||
| 206 | |||
| 207 | .score-item { | ||
| 208 | background: #2b2e46; | ||
| 209 | border-radius: 16px; | ||
| 210 | padding: 12px 16px; | ||
| 211 | transition: background-color 0.15s; | ||
| 212 | } | ||
| 213 | |||
| 214 | .score-item:hover { | ||
| 215 | background: #353854; | ||
| 216 | } | ||
| 217 | |||
| 218 | .score-user { | ||
| 219 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 220 | font-size: 18px; | ||
| 221 | color: #FFF; | ||
| 222 | margin-bottom: 4px; | ||
| 223 | } | ||
| 224 | |||
| 225 | .score-map { | ||
| 226 | font-family: BarlowSemiCondensed-Regular; | ||
| 227 | font-size: 16px; | ||
| 228 | color: #CDCFDF; | ||
| 229 | margin-bottom: 2px; | ||
| 230 | } | ||
| 231 | |||
| 232 | .score-portals { | ||
| 233 | font-family: BarlowSemiCondensed-Regular; | ||
| 234 | font-size: 14px; | ||
| 235 | color: #888; | ||
| 236 | } | ||
| 237 | |||
| 238 | .scores-empty { | ||
| 239 | text-align: center; | ||
| 240 | padding: 40px 20px; | ||
| 241 | color: #CDCFDF; | ||
| 242 | font-size: 16px; | ||
| 243 | font-family: BarlowSemiCondensed-Regular; | ||
| 244 | } | ||
| 245 | |||
| 246 | /* Info Section */ | ||
| 247 | .info-section { | ||
| 248 | margin: 20px; | ||
| 249 | } | ||
| 250 | |||
| 251 | .info-cards { | ||
| 252 | display: grid; | ||
| 253 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | ||
| 254 | gap: 20px; | ||
| 255 | } | ||
| 256 | |||
| 257 | .info-card { | ||
| 258 | background: #202232; | ||
| 259 | border-radius: 24px; | ||
| 260 | padding: 30px; | ||
| 261 | text-align: center; | ||
| 262 | transition: background-color 0.15s; | ||
| 263 | } | ||
| 264 | |||
| 265 | .info-card:hover { | ||
| 266 | background: #2b2e46; | ||
| 267 | } | ||
| 268 | |||
| 269 | .info-icon { | ||
| 270 | font-size: 50px; | ||
| 271 | margin-bottom: 20px; | ||
| 272 | } | ||
| 273 | |||
| 274 | .info-card h3 { | ||
| 275 | font-size: 32px; | ||
| 276 | font-family: BarlowCondensed-Bold; | ||
| 277 | color: #FFF; | ||
| 278 | margin-bottom: 15px; | ||
| 279 | } | ||
| 280 | |||
| 281 | .info-card p { | ||
| 282 | color: #CDCFDF; | ||
| 283 | font-size: 18px; | ||
| 284 | font-family: BarlowSemiCondensed-Regular; | ||
| 285 | line-height: 1.6; | ||
| 286 | margin: 0; | ||
| 287 | } | ||
| 288 | |||
| 289 | /* Notice Section */ | ||
| 290 | .notice-section { | ||
| 291 | background: #202232; | ||
| 292 | border-radius: 24px; | ||
| 293 | padding: 30px; | ||
| 294 | margin: 20px; | ||
| 295 | } | ||
| 296 | |||
| 297 | .notice-content h3 { | ||
| 298 | color: #FFF; | ||
| 299 | font-size: 40px; | ||
| 300 | font-family: BarlowCondensed-Bold; | ||
| 301 | margin-top: 0; | ||
| 302 | margin-bottom: 20px; | ||
| 303 | } | ||
| 304 | |||
| 305 | .notice-content p { | ||
| 306 | color: #CDCFDF; | ||
| 307 | font-size: 20px; | ||
| 308 | font-family: BarlowSemiCondensed-Regular; | ||
| 309 | line-height: 1.8; | ||
| 310 | margin-bottom: 15px; | ||
| 311 | } | ||
| 312 | |||
| 313 | .notice-content p:last-child { | ||
| 314 | margin-bottom: 0; | ||
| 315 | } | ||
| 316 | |||
| 317 | .notice-content strong { | ||
| 318 | color: #FFF; | ||
| 319 | font-family: BarlowSemiCondensed-SemiBold; | ||
| 320 | } | ||
| 321 | |||
| 322 | /* Responsive Design */ | ||
| 323 | @media screen and (min-width: 769px) and (max-width: 1024px) { | ||
| 324 | .hero-section { | ||
| 325 | margin: 18px; | ||
| 326 | padding: 40px 18px; | ||
| 327 | } | ||
| 328 | |||
| 329 | .hero-title { | ||
| 330 | font-size: 48px; | ||
| 331 | } | ||
| 332 | |||
| 333 | .hero-subtitle { | ||
| 334 | font-size: 22px; | ||
| 335 | } | ||
| 336 | |||
| 337 | .stats-section { | ||
| 338 | margin: 18px; | ||
| 339 | } | ||
| 340 | |||
| 341 | .stats-grid { | ||
| 342 | gap: 18px; | ||
| 343 | } | ||
| 344 | |||
| 345 | .stats-container { | ||
| 346 | padding: 18px; | ||
| 347 | } | ||
| 348 | |||
| 349 | .stats-header h3 { | ||
| 350 | font-size: 44px; | ||
| 351 | } | ||
| 352 | |||
| 353 | .stats-header p { | ||
| 354 | font-size: 18px; | ||
| 355 | } | ||
| 356 | |||
| 357 | .chart-wrapper { | ||
| 358 | padding: 18px 10px; | ||
| 359 | } | ||
| 360 | |||
| 361 | .recent-scores-container { | ||
| 362 | padding: 18px; | ||
| 363 | } | ||
| 364 | |||
| 365 | .recent-scores-header h3 { | ||
| 366 | font-size: 28px; | ||
| 367 | } | ||
| 368 | |||
| 369 | .score-user { | ||
| 370 | font-size: 16px; | ||
| 371 | } | ||
| 372 | |||
| 373 | .score-map { | ||
| 374 | font-size: 14px; | ||
| 375 | } | ||
| 376 | |||
| 377 | .score-portals { | ||
| 378 | font-size: 12px; | ||
| 379 | } | ||
| 380 | |||
| 381 | .info-section { | ||
| 382 | margin: 18px; | ||
| 383 | } | ||
| 384 | |||
| 385 | .info-cards { | ||
| 386 | gap: 18px; | ||
| 387 | } | ||
| 388 | |||
| 389 | .info-card { | ||
| 390 | padding: 25px; | ||
| 391 | } | ||
| 392 | |||
| 393 | .info-card h3 { | ||
| 394 | font-size: 28px; | ||
| 395 | } | ||
| 396 | |||
| 397 | .info-card p { | ||
| 398 | font-size: 16px; | ||
| 399 | } | ||
| 400 | |||
| 401 | .notice-section { | ||
| 402 | margin: 18px; | ||
| 403 | padding: 25px; | ||
| 404 | } | ||
| 405 | |||
| 406 | .notice-content h3 { | ||
| 407 | font-size: 36px; | ||
| 408 | } | ||
| 409 | |||
| 410 | .notice-content p { | ||
| 411 | font-size: 18px; | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | @media screen and (max-width: 768px) { | ||
| 416 | .hero-section { | ||
| 417 | margin: 20px; | ||
| 418 | padding: 30px 20px; | ||
| 419 | } | ||
| 420 | |||
| 421 | .hero-title { | ||
| 422 | font-size: 40px; | ||
| 423 | } | ||
| 424 | |||
| 425 | .hero-subtitle { | ||
| 426 | font-size: 18px; | ||
| 427 | } | ||
| 428 | |||
| 429 | .stats-section { | ||
| 430 | margin: 20px; | ||
| 431 | } | ||
| 432 | |||
| 433 | .stats-grid { | ||
| 434 | grid-template-columns: 1fr; | ||
| 435 | gap: 15px; | ||
| 436 | } | ||
| 437 | |||
| 438 | .stats-container { | ||
| 439 | padding: 15px; | ||
| 440 | } | ||
| 441 | |||
| 442 | .stats-header h3 { | ||
| 443 | font-size: 36px; | ||
| 444 | margin-bottom: 8px; | ||
| 445 | } | ||
| 446 | |||
| 447 | .stats-header p { | ||
| 448 | font-size: 16px; | ||
| 449 | } | ||
| 450 | |||
| 451 | .chart-wrapper { | ||
| 452 | padding: 15px 5px; | ||
| 453 | margin-top: 15px; | ||
| 454 | } | ||
| 455 | |||
| 456 | .recent-scores-container { | ||
| 457 | padding: 15px; | ||
| 458 | } | ||
| 459 | |||
| 460 | .recent-scores-header h3 { | ||
| 461 | font-size: 28px; | ||
| 462 | } | ||
| 463 | |||
| 464 | .score-item { | ||
| 465 | padding: 10px 12px; | ||
| 466 | } | ||
| 467 | |||
| 468 | .score-user { | ||
| 469 | font-size: 16px; | ||
| 470 | } | ||
| 471 | |||
| 472 | .score-map { | ||
| 473 | font-size: 14px; | ||
| 474 | } | ||
| 475 | |||
| 476 | .score-portals { | ||
| 477 | font-size: 12px; | ||
| 478 | } | ||
| 479 | |||
| 480 | .chart-loading, | ||
| 481 | .chart-empty { | ||
| 482 | padding: 40px 15px; | ||
| 483 | } | ||
| 484 | |||
| 485 | .chart-loading p, | ||
| 486 | .chart-empty p { | ||
| 487 | font-size: 16px; | ||
| 488 | } | ||
| 489 | |||
| 490 | .loading-spinner { | ||
| 491 | width: 40px; | ||
| 492 | height: 40px; | ||
| 493 | border-width: 4px; | ||
| 494 | } | ||
| 495 | |||
| 496 | .info-section { | ||
| 497 | margin: 20px; | ||
| 498 | } | ||
| 499 | |||
| 500 | .info-cards { | ||
| 501 | grid-template-columns: 1fr; | ||
| 502 | gap: 15px; | ||
| 503 | } | ||
| 504 | |||
| 505 | .info-card { | ||
| 506 | padding: 20px; | ||
| 507 | } | ||
| 508 | |||
| 509 | .info-icon { | ||
| 510 | font-size: 40px; | ||
| 511 | margin-bottom: 15px; | ||
| 512 | } | ||
| 513 | |||
| 514 | .info-card h3 { | ||
| 515 | font-size: 28px; | ||
| 516 | margin-bottom: 12px; | ||
| 517 | } | ||
| 518 | |||
| 519 | .info-card p { | ||
| 520 | font-size: 16px; | ||
| 521 | } | ||
| 522 | |||
| 523 | .notice-section { | ||
| 524 | margin: 20px; | ||
| 525 | padding: 20px; | ||
| 526 | } | ||
| 527 | |||
| 528 | .notice-content h3 { | ||
| 529 | font-size: 32px; | ||
| 530 | margin-bottom: 15px; | ||
| 531 | } | ||
| 532 | |||
| 533 | .notice-content p { | ||
| 534 | font-size: 16px; | ||
| 535 | margin-bottom: 12px; | ||
| 536 | } | ||
| 537 | |||
| 538 | .tooltip-date { | ||
| 539 | font-size: 14px; | ||
| 540 | } | ||
| 541 | |||
| 542 | .tooltip-count { | ||
| 543 | font-size: 18px; | ||
| 544 | } | ||
| 545 | } \ No newline at end of file | ||
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx index 3f30d9a..88290dd 100644 --- a/frontend/src/pages/Homepage.tsx +++ b/frontend/src/pages/Homepage.tsx | |||
| @@ -1,20 +1,246 @@ | |||
| 1 | import React from "react"; | 1 | import React from "react"; |
| 2 | import { Helmet } from "react-helmet"; | 2 | import { Helmet } from "react-helmet"; |
| 3 | import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart } from "recharts"; | ||
| 4 | import { API } from "../api/Api"; | ||
| 5 | import { PortalCountData, ScoreLog } from "../api/Stats"; | ||
| 6 | import "../css/Homepage.css"; | ||
| 7 | import { Link } from "react-router-dom"; | ||
| 3 | 8 | ||
| 4 | const Homepage: React.FC = () => { | 9 | const Homepage: React.FC = () => { |
| 10 | const [portalCountDataSingleplayer, setPortalCountDataSingleplayer] = React.useState<PortalCountData[]>([]); | ||
| 11 | const [portalCountDataMultiplayer, setPortalCountDataMultiplayer] = React.useState<PortalCountData[]>([]); | ||
| 12 | const [recentScores, setRecentScores] = React.useState<ScoreLog[]>([]); | ||
| 13 | const [isLoading, setIsLoading] = React.useState<boolean>(true); | ||
| 14 | const [isLoadingScores, setIsLoadingScores] = React.useState<boolean>(true); | ||
| 15 | const [selectedMode, setSelectedMode] = React.useState<"singleplayer" | "multiplayer">("singleplayer"); | ||
| 16 | |||
| 17 | const processTimelineData = (data: PortalCountData[]): PortalCountData[] => { | ||
| 18 | if (data.length === 0) { | ||
| 19 | return []; | ||
| 20 | }; | ||
| 21 | const sortedData = [...data].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); | ||
| 22 | const startDate = new Date(sortedData[0].date); | ||
| 23 | const endDate = new Date(sortedData[sortedData.length - 1].date); | ||
| 24 | |||
| 25 | const result: PortalCountData[] = []; | ||
| 26 | let currentDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1); | ||
| 27 | |||
| 28 | let dataIndex = 0; | ||
| 29 | let currentCount = sortedData[0].count; | ||
| 30 | |||
| 31 | while (currentDate <= endDate) { | ||
| 32 | while (dataIndex < sortedData.length && new Date(sortedData[dataIndex].date) <= currentDate) { | ||
| 33 | currentCount = sortedData[dataIndex].count; | ||
| 34 | dataIndex++; | ||
| 35 | } | ||
| 36 | result.push({ | ||
| 37 | date: currentDate.toISOString(), | ||
| 38 | count: currentCount | ||
| 39 | }); | ||
| 40 | const nextDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 7); | ||
| 41 | if (nextDate.getMonth() !== currentDate.getMonth()) { | ||
| 42 | currentDate = new Date(nextDate.getFullYear(), nextDate.getMonth(), 1); | ||
| 43 | } else { | ||
| 44 | currentDate = nextDate; | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | return result; | ||
| 49 | }; | ||
| 50 | |||
| 51 | const processedDataSingleplayer = React.useMemo( | ||
| 52 | () => processTimelineData(portalCountDataSingleplayer), | ||
| 53 | [portalCountDataSingleplayer] | ||
| 54 | ); | ||
| 55 | |||
| 56 | const processedDataMultiplayer = React.useMemo( | ||
| 57 | () => processTimelineData(portalCountDataMultiplayer), | ||
| 58 | [portalCountDataMultiplayer] | ||
| 59 | ); | ||
| 60 | |||
| 61 | const getYearlyTicks = (data: PortalCountData[]): string[] => { | ||
| 62 | if (data.length === 0) { | ||
| 63 | return []; | ||
| 64 | } | ||
| 65 | const seenYears = new Set<number>(); | ||
| 66 | const ticks: string[] = []; | ||
| 67 | for (const point of data) { | ||
| 68 | const year = new Date(point.date).getFullYear(); | ||
| 69 | if (!seenYears.has(year)) { | ||
| 70 | seenYears.add(year); | ||
| 71 | ticks.push(point.date); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return ticks; | ||
| 75 | }; | ||
| 76 | |||
| 77 | const yearlyTicksSingleplayer = React.useMemo( | ||
| 78 | () => getYearlyTicks(processedDataSingleplayer), | ||
| 79 | [processedDataSingleplayer] | ||
| 80 | ); | ||
| 81 | |||
| 82 | const yearlyTicksMultiplayer = React.useMemo( | ||
| 83 | () => getYearlyTicks(processedDataMultiplayer), | ||
| 84 | [processedDataMultiplayer] | ||
| 85 | ); | ||
| 86 | |||
| 87 | const fetchPortalCountData = async () => { | ||
| 88 | setIsLoading(true); | ||
| 89 | const data = await API.get_portal_count_history(); | ||
| 90 | setPortalCountDataSingleplayer(data?.timeline_singleplayer || []); | ||
| 91 | setPortalCountDataMultiplayer(data?.timeline_multiplayer || []); | ||
| 92 | setIsLoading(false); | ||
| 93 | }; | ||
| 94 | |||
| 95 | const fetchRecentScores = async () => { | ||
| 96 | setIsLoadingScores(true); | ||
| 97 | const scores = await API.get_recent_scores(); | ||
| 98 | setRecentScores(scores); | ||
| 99 | setIsLoadingScores(false); | ||
| 100 | }; | ||
| 101 | |||
| 102 | React.useEffect(() => { | ||
| 103 | fetchPortalCountData(); | ||
| 104 | fetchRecentScores(); | ||
| 105 | }, []); | ||
| 106 | |||
| 107 | const CustomTooltip = ({ active, payload }: any) => { | ||
| 108 | if (active && payload && payload.length) { | ||
| 109 | return ( | ||
| 110 | <div className="custom-tooltip"> | ||
| 111 | <p className="tooltip-date">{new Date(payload[0].payload.date).toLocaleDateString("en-US", { | ||
| 112 | year: "numeric", | ||
| 113 | month: "long", | ||
| 114 | day: "numeric" | ||
| 115 | })}</p> | ||
| 116 | <p className="tooltip-count">{`Portal Count: ${payload[0].value}`}</p> | ||
| 117 | </div> | ||
| 118 | ); | ||
| 119 | } | ||
| 120 | return null; | ||
| 121 | }; | ||
| 5 | 122 | ||
| 6 | return ( | 123 | return ( |
| 7 | <main> | 124 | <main className="homepage"> |
| 8 | <Helmet> | 125 | <Helmet> |
| 9 | <title>LPHUB | Homepage</title> | 126 | <title>LPHUB | Homepage</title> |
| 10 | </Helmet> | 127 | </Helmet> |
| 11 | <section> | 128 | |
| 12 | <p /> | 129 | <section className="hero-section"> |
| 13 | <h1>Welcome to Least Portals Hub!</h1> | 130 | <div className="hero-content"> |
| 14 | <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> | 131 | <h1 className="hero-title">Welcome to Least Portals Hub!</h1> |
| 15 | <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> | 132 | <p className="hero-subtitle"> |
| 16 | <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> | 133 | Your ultimate destination for Portal 2 Least Portals speedrunning. |
| 134 | </p> | ||
| 135 | </div> | ||
| 17 | </section> | 136 | </section> |
| 137 | |||
| 138 | <section className="stats-section"> | ||
| 139 | <div className="stats-grid"> | ||
| 140 | <div className="stats-container"> | ||
| 141 | <div className="stats-header"> | ||
| 142 | <h3>Least Portals World Record Timeline</h3> | ||
| 143 | </div> | ||
| 144 | |||
| 145 | {isLoading ? ( | ||
| 146 | <div className="chart-loading"> | ||
| 147 | <div className="loading-spinner"></div> | ||
| 148 | </div> | ||
| 149 | ) : (selectedMode === "singleplayer" ? processedDataSingleplayer : processedDataMultiplayer).length > 0 ? ( | ||
| 150 | <> | ||
| 151 | <div className="chart-wrapper"> | ||
| 152 | <ResponsiveContainer width="100%" height={400}> | ||
| 153 | <AreaChart | ||
| 154 | data={selectedMode === "singleplayer" ? processedDataSingleplayer : processedDataMultiplayer} | ||
| 155 | margin={{ top: 10, right: 30, left: 0, bottom: 0 }} | ||
| 156 | > | ||
| 157 | <defs> | ||
| 158 | <linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1"> | ||
| 159 | <stop offset="5%" stopColor="#CDCFDF" stopOpacity={0.6} /> | ||
| 160 | <stop offset="95%" stopColor="#CDCFDF" stopOpacity={0.05} /> | ||
| 161 | </linearGradient> | ||
| 162 | </defs> | ||
| 163 | <CartesianGrid strokeDasharray="3 3" stroke="#202232" opacity={0.5} /> | ||
| 164 | <XAxis | ||
| 165 | dataKey="date" | ||
| 166 | stroke="#CDCFDF" | ||
| 167 | tick={{ fill: "#CDCFDF", fontFamily: "BarlowSemiCondensed-Regular" }} | ||
| 168 | ticks={selectedMode === "singleplayer" ? yearlyTicksSingleplayer : yearlyTicksMultiplayer} | ||
| 169 | tickFormatter={(date) => { | ||
| 170 | const d = new Date(date); | ||
| 171 | return d.getFullYear().toString(); | ||
| 172 | }} | ||
| 173 | /> | ||
| 174 | <YAxis | ||
| 175 | stroke="#CDCFDF" | ||
| 176 | tick={{ fill: "#CDCFDF", fontFamily: "BarlowSemiCondensed-Regular" }} | ||
| 177 | /> | ||
| 178 | <Tooltip content={<CustomTooltip />} /> | ||
| 179 | <Area | ||
| 180 | type="monotone" | ||
| 181 | dataKey="count" | ||
| 182 | stroke="#FFF" | ||
| 183 | strokeWidth={2} | ||
| 184 | fillOpacity={1} | ||
| 185 | fill="url(#colorCount)" | ||
| 186 | /> | ||
| 187 | </AreaChart> | ||
| 188 | </ResponsiveContainer> | ||
| 189 | </div> | ||
| 190 | <div className="mode-toggle-container"> | ||
| 191 | <button | ||
| 192 | onClick={() => setSelectedMode("singleplayer")} | ||
| 193 | className={`mode-toggle-button ${selectedMode === "singleplayer" ? "selected" : ""}`} | ||
| 194 | > | ||
| 195 | Singleplayer | ||
| 196 | </button> | ||
| 197 | <button | ||
| 198 | onClick={() => setSelectedMode("multiplayer")} | ||
| 199 | className={`mode-toggle-button ${selectedMode === "multiplayer" ? "selected" : ""}`} | ||
| 200 | > | ||
| 201 | Multiplayer | ||
| 202 | </button> | ||
| 203 | </div> | ||
| 204 | </> | ||
| 205 | ) : ( | ||
| 206 | <div className="chart-empty"> | ||
| 207 | <p>No data available yet.</p> | ||
| 208 | </div> | ||
| 209 | )} | ||
| 210 | </div> | ||
| 211 | |||
| 212 | <div className="recent-scores-container"> | ||
| 213 | <div className="recent-scores-header"> | ||
| 214 | <h3>Recent Scores</h3> | ||
| 215 | </div> | ||
| 216 | |||
| 217 | {isLoadingScores ? ( | ||
| 218 | <div className="scores-loading"> | ||
| 219 | <div className="loading-spinner"></div> | ||
| 220 | </div> | ||
| 221 | ) : recentScores.length > 0 ? ( | ||
| 222 | <div className="recent-scores-list"> | ||
| 223 | {recentScores.map((score, index) => ( | ||
| 224 | <div key={index} className="score-item"> | ||
| 225 | <div> | ||
| 226 | <Link key={index} to={`/users/${score.user.steam_id}`} className="score-user">{score.user.user_name}</Link> | ||
| 227 | </div> | ||
| 228 | <div className="score-map"> | ||
| 229 | <Link key={index} to={`/maps/${score.map.id}`} className="score-map">{score.map.name}</Link> | ||
| 230 | </div> | ||
| 231 | <div className="score-portals">{score.score_count} { } portals</div> | ||
| 232 | </div> | ||
| 233 | ))} | ||
| 234 | </div> | ||
| 235 | ) : ( | ||
| 236 | <div className="scores-empty"> | ||
| 237 | <p>No Recent Scores.</p> | ||
| 238 | </div> | ||
| 239 | )} | ||
| 240 | </div> | ||
| 241 | </div> | ||
| 242 | </section> | ||
| 243 | |||
| 18 | </main> | 244 | </main> |
| 19 | ); | 245 | ); |
| 20 | }; | 246 | }; |