aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--frontend/package-lock.json49
-rw-r--r--frontend/package.json6
-rw-r--r--frontend/src/App.css366
-rw-r--r--frontend/src/App.tsx106
-rw-r--r--frontend/src/components/GameCategory.tsx6
-rw-r--r--frontend/src/components/GameEntry.tsx6
-rw-r--r--frontend/src/components/Sidebar.tsx43
-rw-r--r--frontend/src/pages/About.tsx36
-rw-r--r--frontend/src/pages/About/About.tsx2
-rw-r--r--frontend/src/pages/Games.tsx29
-rw-r--r--frontend/src/pages/Games/Games.tsx6
-rw-r--r--frontend/src/pages/Home/Homepage.tsx6
-rw-r--r--frontend/src/pages/Homepage.tsx31
-rw-r--r--frontend/src/pages/Maplist.tsx249
-rw-r--r--frontend/src/pages/Maplist/Maplist.tsx267
-rw-r--r--frontend/src/pages/Maps.tsx185
-rw-r--r--frontend/src/pages/Profile.tsx633
-rw-r--r--frontend/src/pages/Rankings.tsx203
-rw-r--r--frontend/src/pages/Rules.tsx37
-rw-r--r--frontend/src/pages/User.tsx410
-rw-r--r--frontend/tsconfig.json3
21 files changed, 382 insertions, 2297 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index f2ae350..574c09e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -15,8 +15,6 @@
15 "@testing-library/user-event": "^13.5.0", 15 "@testing-library/user-event": "^13.5.0",
16 "@types/jest": "^27.5.2", 16 "@types/jest": "^27.5.2",
17 "@types/node": "^20.19.9", 17 "@types/node": "^20.19.9",
18 "@types/react": "^18.3.3",
19 "@types/react-dom": "^18.3.0",
20 "@types/react-helmet": "^6.1.11", 18 "@types/react-helmet": "^6.1.11",
21 "@vitejs/plugin-react": "^5.0.0", 19 "@vitejs/plugin-react": "^5.0.0",
22 "axios": "^1.7.4", 20 "axios": "^1.7.4",
@@ -31,6 +29,8 @@
31 "web-vitals": "^2.1.4" 29 "web-vitals": "^2.1.4"
32 }, 30 },
33 "devDependencies": { 31 "devDependencies": {
32 "@types/react": "^19.1.10",
33 "@types/react-dom": "^19.1.7",
34 "prettier": "^3.4.2" 34 "prettier": "^3.4.2"
35 } 35 }
36 }, 36 },
@@ -1423,6 +1423,26 @@
1423 "node": ">=12" 1423 "node": ">=12"
1424 } 1424 }
1425 }, 1425 },
1426 "node_modules/@testing-library/react/node_modules/@types/react": {
1427 "version": "18.3.23",
1428 "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
1429 "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
1430 "license": "MIT",
1431 "peer": true,
1432 "dependencies": {
1433 "@types/prop-types": "*",
1434 "csstype": "^3.0.2"
1435 }
1436 },
1437 "node_modules/@testing-library/react/node_modules/@types/react-dom": {
1438 "version": "18.3.7",
1439 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
1440 "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
1441 "license": "MIT",
1442 "peerDependencies": {
1443 "@types/react": "^18.0.0"
1444 }
1445 },
1426 "node_modules/@testing-library/react/node_modules/aria-query": { 1446 "node_modules/@testing-library/react/node_modules/aria-query": {
1427 "version": "5.1.3", 1447 "version": "5.1.3",
1428 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", 1448 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
@@ -1563,28 +1583,29 @@
1563 } 1583 }
1564 }, 1584 },
1565 "node_modules/@types/prop-types": { 1585 "node_modules/@types/prop-types": {
1566 "version": "15.7.14", 1586 "version": "15.7.15",
1567 "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", 1587 "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
1568 "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", 1588 "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
1569 "license": "MIT" 1589 "license": "MIT",
1590 "peer": true
1570 }, 1591 },
1571 "node_modules/@types/react": { 1592 "node_modules/@types/react": {
1572 "version": "18.3.18", 1593 "version": "19.1.10",
1573 "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", 1594 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
1574 "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", 1595 "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
1575 "license": "MIT", 1596 "license": "MIT",
1576 "dependencies": { 1597 "dependencies": {
1577 "@types/prop-types": "*",
1578 "csstype": "^3.0.2" 1598 "csstype": "^3.0.2"
1579 } 1599 }
1580 }, 1600 },
1581 "node_modules/@types/react-dom": { 1601 "node_modules/@types/react-dom": {
1582 "version": "18.3.5", 1602 "version": "19.1.7",
1583 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", 1603 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
1584 "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", 1604 "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
1605 "dev": true,
1585 "license": "MIT", 1606 "license": "MIT",
1586 "peerDependencies": { 1607 "peerDependencies": {
1587 "@types/react": "^18.0.0" 1608 "@types/react": "^19.0.0"
1588 } 1609 }
1589 }, 1610 },
1590 "node_modules/@types/react-helmet": { 1611 "node_modules/@types/react-helmet": {
diff --git a/frontend/package.json b/frontend/package.json
index f2a6d37..cf35deb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,10 +10,8 @@
10 "@testing-library/react": "^13.4.0", 10 "@testing-library/react": "^13.4.0",
11 "@testing-library/user-event": "^13.5.0", 11 "@testing-library/user-event": "^13.5.0",
12 "@types/jest": "^27.5.2", 12 "@types/jest": "^27.5.2",
13 "@types/react": "^18.3.3",
14 "@types/react-dom": "^18.3.0",
15 "@types/react-helmet": "^6.1.11",
16 "@types/node": "^20.19.9", 13 "@types/node": "^20.19.9",
14 "@types/react-helmet": "^6.1.11",
17 "@vitejs/plugin-react": "^5.0.0", 15 "@vitejs/plugin-react": "^5.0.0",
18 "axios": "^1.7.4", 16 "axios": "^1.7.4",
19 "react": "^18.3.1", 17 "react": "^18.3.1",
@@ -52,6 +50,8 @@
52 ] 50 ]
53 }, 51 },
54 "devDependencies": { 52 "devDependencies": {
53 "@types/react": "^19.1.10",
54 "@types/react-dom": "^19.1.7",
55 "prettier": "^3.4.2" 55 "prettier": "^3.4.2"
56 } 56 }
57} 57}
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 464b759..a39dcf1 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,298 +1,150 @@
1@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Montserrat+Alternates:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 1@layer theme, base, components, utilities;
2
2@import "tailwindcss"; 3@import "tailwindcss";
3 4
4@theme { 5@theme {
5 --color-rosewater: #f2d5cf; 6 --color-main: #141520;
6 --color-flamingo: #eebebe; 7 --color-panel: #202232;
7 --color-pink: #f4b8e4; 8 --color-block: #2b2e46;
8 --color-mauve: #ca9ee6;
9 --color-red: #e78284;
10 --color-maroon: #ea999c;
11 --color-peach: #ef9f76;
12 --color-yellow: #e5c890;
13 --color-green: #a6d189;
14 --color-teal: #81c8be;
15 --color-sky: #99d1db;
16 --color-sapphire: #85c1dc;
17 --color-blue: #8caaee;
18 --color-lavender: #babbf1;
19 --color-text: #c6d0f5;
20 --color-subtext1: #b5bfe2;
21 --color-subtext0: #a5adce;
22 --color-overlay2: #949cbb;
23 --color-overlay1: #838ba7;
24 --color-overlay0: #737994;
25 --color-surface2: #626880;
26 --color-surface1: #51576d;
27 --color-surface0: #414559;
28 --color-base: #303446;
29 --color-mantle: #292c3c;
30 --color-crust: #232634;
31 9
32 --color-primary: var(--color-mauve); 10 --color-white: #cdcfdf;
33 --color-secondary: var(--color-blue);
34 --color-accent: var(--color-peach);
35 --color-background: var(--color-base);
36 --color-surface: var(--color-surface0);
37 --color-muted: var(--color-overlay0);
38 --color-border: var(--color-surface2);
39 --color-input: var(--color-surface1);
40 --color-foreground: var(--color-text);
41 --color-success: var(--color-green);
42 --color-warning: var(--color-yellow);
43 --color-error: var(--color-red);
44 --color-info: var(--color-blue);
45 11
46 --font-barlow-condensed-regular: 'BarlowCondensed-Regular'; 12 --font-barlow-condensed-regular: BarlowCondensed-Regular;
47 --font-barlow-condensed-bold: 'BarlowCondensed-Bold'; 13 --font-barlow-condensed-bold: BarlowCondensed-Bold;
48 --font-barlow-semicondensed-regular: 'BarlowSemiCondensed-Regular'; 14 --font-barlow-semicondensed-regular: BarlowSemiCondensed-Regular;
49 --font-barlow-semicondensed-semibold: 'BarlowSemiCondensed-SemiBold'; 15 --font-barlow-semicondensed-semibold: BarlowSemiCondensed-SemiBold;
50} 16}
51 17
18@layer utilities {}
52 19
53a { 20a {
54 color: inherit; 21 color: inherit;
22}
23
24h1,
25h2,
26h3,
27h4,
28h5,
29h6 {
30 font-family: var(--font-barlow-condensed-bold);
31}
32
33h1 {
34 font-size: 48px;
35}
36
37h2 {
38 font-size: 32px;
39}
40
41h2 {
42 font-size: 24px;
43}
44
45h3 {
46 font-size: 20px;
47}
48
49h4 {
50 font-size: 18px;
51}
52
53h5 {
54 font-size: 16px;
55}
56
57h6 {
58 font-size: 14px;
59}
60
61p {
62 font-size: 16px;
55} 63}
56 64
57body { 65body {
58 overflow: hidden; 66 overflow: hidden;
59 background-color: var(--color-crust); 67 background-color: var(--color-main);
60 margin: 0; 68 color: var(--color-white);
69 font-family: var(--font-barlow-semicondensed-regular);
70 margin: 0;
71}
72
73main {
74 overflow-y: auto;
75}
76
77main>div {
78 width: 100%;
61} 79}
62 80
63.loader { 81.loader {
64 animation: loader 1.2s ease infinite; 82 animation: loader 1.2s ease infinite;
65 background-size: 400% 300%; 83 background-size: 400% 300%;
66 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); 84 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
67 user-select: none; 85 user-select: none;
68} 86}
69 87
70.loader-text { 88.loader-text {
71 animation: loader 1.2s ease infinite; 89 animation: loader 1.2s ease infinite;
72 background-size: 400% 300%; 90 background-size: 400% 300%;
73 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); 91 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
74 user-select: none; 92 user-select: none;
75 color: #00000000; 93 color: #00000000;
76 border-radius: 1000px; 94 border-radius: 1000px;
77} 95}
78 96
79@keyframes loader { 97@keyframes loader {
80 0% { 98 0% {
81 background-position: 100% 20%; 99 background-position: 100% 20%;
82 } 100 }
83 101
84 50% { 102 50% {
85 background-position: 0% 50%; 103 background-position: 0% 50%;
86 } 104 }
87 105
88 100% { 106 100% {
89 background-position: 0% 50%; 107 background-position: 0% 50%;
90 } 108 }
91} 109}
92 110
93.loader { 111.loader {
94 width: 48px; 112 width: 48px;
95 height: 48px; 113 height: 48px;
96 border: 5px solid #FFF; 114 border: 5px solid #FFF;
97 border-bottom-color: transparent; 115 border-bottom-color: transparent;
98 border-radius: 50%; 116 border-radius: 50%;
99 display: inline-block; 117 display: inline-block;
100 box-sizing: border-box; 118 box-sizing: border-box;
101 animation: rotation 1s linear infinite; 119 animation: rotation 1s linear infinite;
102 } 120}
103 121
104@keyframes rotation { 122@keyframes rotation {
105 0% { 123 0% {
106 transform: rotate(0deg); 124 transform: rotate(0deg);
107 } 125 }
108 100% {
109 transform: rotate(360deg);
110 }
111}
112 126
113/* Custom Tailwind utilities for Catppuccin Frappe theme */ 127 100% {
114@layer utilities { 128 transform: rotate(360deg);
115 .bg-primary { 129 }
116 background-color: var(--color-primary);
117 }
118
119 .bg-secondary {
120 background-color: var(--color-secondary);
121 }
122
123 .bg-accent {
124 background-color: var(--color-accent);
125 }
126
127 .bg-background {
128 background-color: var(--color-background);
129 }
130
131 .bg-surface {
132 background-color: var(--color-surface);
133 }
134
135 .bg-muted {
136 background-color: var(--color-muted);
137 }
138
139 .text-primary {
140 color: var(--color-primary);
141 }
142
143 .text-secondary {
144 color: var(--color-secondary);
145 }
146
147 .text-accent {
148 color: var(--color-accent);
149 }
150
151 .text-foreground {
152 color: var(--color-foreground);
153 }
154
155 .text-muted {
156 color: var(--color-muted);
157 }
158
159 .border-primary {
160 border-color: var(--color-primary);
161 }
162
163 .border-secondary {
164 border-color: var(--color-secondary);
165 }
166
167 .border-muted {
168 border-color: var(--color-border);
169 }
170
171 .hover\:bg-primary:hover {
172 background-color: var(--color-primary);
173 }
174
175 .hover\:bg-secondary:hover {
176 background-color: var(--color-secondary);
177 }
178
179 .hover\:bg-surface:hover {
180 background-color: var(--color-surface);
181 }
182
183 .hover\:text-primary:hover {
184 color: var(--color-primary);
185 }
186
187 .focus\:ring-primary:focus {
188 --tw-ring-color: var(--color-primary);
189 }
190
191 .triangle {
192 width: 0;
193 height: 0;
194 border-left: 5px solid transparent;
195 border-right: 5px solid transparent;
196 border-bottom: 8px solid var(--color-foreground);
197 display: inline-block;
198 }
199
200 .sidebar-button-selected {
201 background-color: var(--color-primary) !important;
202 color: var(--color-background) !important;
203 }
204
205 .sidebar-button-deselected {
206 background-color: var(--color-surface) !important;
207 color: var(--color-foreground) !important;
208 }
209
210 .profileboard-record {
211 background-color: var(--color-surface);
212 border: 1px solid var(--color-border);
213 border-radius: 0.5rem;
214 padding: 0.75rem;
215 margin-bottom: 0.5rem;
216 transition: all 0.2s ease;
217 }
218
219 .profileboard-record:hover {
220 background-color: var(--color-surface1);
221 }
222
223 .difficulty-rating {
224 width: 20px;
225 height: 20px;
226 background-color: var(--color-muted);
227 border-radius: 50%;
228 margin: 0 2px;
229 display: inline-block;
230 }
231
232 .nav-button {
233 background-color: var(--color-surface);
234 color: var(--color-foreground);
235 border: 1px solid var(--color-border);
236 border-radius: 0.5rem;
237 padding: 0.5rem 1rem;
238 transition: all 0.2s ease;
239 display: inline-flex;
240 align-items: center;
241 gap: 0.5rem;
242 text-decoration: none;
243 }
244
245 .nav-button:hover {
246 background-color: var(--color-surface1);
247 }
248
249 .record {
250 background-color: var(--color-surface);
251 border: 1px solid var(--color-border);
252 border-radius: 0.5rem;
253 padding: 0.5rem;
254 margin: 0.25rem;
255 cursor: pointer;
256 transition: all 0.2s ease;
257 }
258
259 .record:hover {
260 background-color: var(--color-surface1);
261 }
262
263 .portal-count {
264 font-size: 3rem;
265 font-weight: bold;
266 color: var(--color-primary);
267 }
268
269 .titles {
270 background-color: var(--color-accent);
271 color: var(--color-background);
272 padding: 0.25rem 0.5rem;
273 border-radius: 1rem;
274 font-size: 0.875rem;
275 margin-right: 0.5rem;
276 display: inline-block;
277 }
278} 130}
279 131
280@font-face { 132@font-face {
281 font-family: 'BarlowCondensed-Bold'; 133 font-family: 'BarlowCondensed-Bold';
282 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); 134 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype');
283} 135}
284 136
285@font-face { 137@font-face {
286 font-family: 'BarlowCondensed-Regular'; 138 font-family: 'BarlowCondensed-Regular';
287 src: local('BarlowCondensed-Regular'), url(./fonts/BarlowCondensed-Regular.ttf) format('truetype'); 139 src: local('BarlowCondensed-Regular'), url(./fonts/BarlowCondensed-Regular.ttf) format('truetype');
288} 140}
289 141
290@font-face { 142@font-face {
291 font-family: 'BarlowSemiCondensed-Regular'; 143 font-family: 'BarlowSemiCondensed-Regular';
292 src: local('BarlowSemiCondensed-Regular'), url(./fonts/BarlowSemiCondensed-Regular.ttf) format('truetype'); 144 src: local('BarlowSemiCondensed-Regular'), url(./fonts/BarlowSemiCondensed-Regular.ttf) format('truetype');
293} 145}
294 146
295@font-face { 147@font-face {
296 font-family: 'BarlowSemiCondensed-SemiBold'; 148 font-family: 'BarlowSemiCondensed-SemiBold';
297 src: local('BarlowSemiCondensed-Regular'), url(./fonts/BarlowSemiCondensed-SemiBold.ttf) format('truetype'); 149 src: local('BarlowSemiCondensed-Regular'), url(./fonts/BarlowSemiCondensed-SemiBold.ttf) format('truetype');
298} 150} \ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index fbfa59f..8a95e77 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,18 +6,18 @@ import { UserProfile } from "@customTypes/Profile";
6import Sidebar from "./components/Sidebar"; 6import Sidebar from "./components/Sidebar";
7import "./App.css"; 7import "./App.css";
8 8
9import Profile from "@pages/Profile"; 9import Profile from "@pages/Profile/Profile.tsx";
10import Games from "@pages/Games"; 10import Games from "@pages/Games/Games.tsx";
11import Maps from "@pages/Maps"; 11import Maps from "@pages/Maps/Maps.tsx";
12import User from "@pages/User"; 12import User from "@pages/User/User.tsx";
13import Homepage from "@pages/Homepage"; 13import Homepage from "@pages/Home/Homepage.tsx";
14import UploadRunDialog from "./components/UploadRunDialog"; 14import UploadRunDialog from "./components/UploadRunDialog";
15import Rules from "@pages/Rules"; 15import Rules from "@pages/Rules/Rules.tsx";
16import About from "@pages/About"; 16import About from "@pages/About/About.tsx";
17import { Game } from "@customTypes/Game"; 17import { Game } from "@customTypes/Game";
18import { API } from "./api/Api"; 18import { API } from "./api/Api";
19import Maplist from "@pages/Maplist"; 19import Maplist from "@pages/Maplist/Maplist.tsx";
20import Rankings from "@pages/Rankings"; 20import Rankings from "@pages/Rankings/Rankings.tsx";
21import { get_user_id_from_token, get_user_mod_from_token } from "./utils/Jwt"; 21import { get_user_id_from_token, get_user_mod_from_token } from "./utils/Jwt";
22 22
23const App: React.FC = () => { 23const App: React.FC = () => {
@@ -76,6 +76,7 @@ const App: React.FC = () => {
76 <title>LPHUB</title> 76 <title>LPHUB</title>
77 <meta name="description" content="Least Portals Hub" /> 77 <meta name="description" content="Least Portals Hub" />
78 </Helmet> 78 </Helmet>
79
79 <UploadRunDialog 80 <UploadRunDialog
80 token={token} 81 token={token}
81 open={uploadRunDialog} 82 open={uploadRunDialog}
@@ -87,40 +88,61 @@ const App: React.FC = () => {
87 }} 88 }}
88 games={games} 89 games={games}
89 /> 90 />
90 <Sidebar 91
91 setToken={setToken} 92 <div className="flex flex-row not-md:flex-col h-[100vh]">
92 profile={profile} 93
93 setProfile={setProfile} 94 <Sidebar
94 onUploadRun={() => setUploadRunDialog(true)} 95 setToken={setToken}
95 /> 96 profile={profile}
96 <Routes> 97 setProfile={setProfile}
97 <Route path="/" element={<Homepage />} /> 98 onUploadRun={() => setUploadRunDialog(true)}
98 <Route
99 path="/profile"
100 element={
101 <Profile
102 profile={profile}
103 token={token}
104 gameData={games}
105 onDeleteRecord={() => _set_profile(get_user_id_from_token(token))}
106 />
107 }
108 />
109 <Route
110 path="/users/*"
111 element={<User profile={profile} token={token} gameData={games} />}
112 />
113 <Route path="/games" element={<Games games={games} />} />
114 <Route path="/games/:id" element={<Maplist />}></Route>
115 <Route
116 path="/maps/*"
117 element={<Maps token={token} isModerator={isModerator} />}
118 /> 99 />
119 <Route path="/rules" element={<Rules />} /> 100
120 <Route path="/about" element={<About />} /> 101 <main className="w-full">
121 <Route path="/rankings" element={<Rankings />}></Route> 102
122 <Route path="*" element={"404"} /> 103 <Routes>
123 </Routes> 104 <Route path="/" element={<Homepage />} />
105
106 <Route
107 path="/profile"
108 element={
109 <Profile
110 profile={profile}
111 token={token}
112 gameData={games}
113 onDeleteRecord={() => _set_profile(get_user_id_from_token(token))}
114 />
115 }
116 />
117
118 <Route
119 path="/users/*"
120 element={<User profile={profile} token={token} gameData={games} />}
121 />
122
123 <Route path="/games" element={<Games games={games} />} />
124
125 <Route path="/games/:id" element={<Maplist />}></Route>
126
127 <Route
128 path="/maps/*"
129 element={<Maps token={token} isModerator={isModerator} />}
130 />
131
132 <Route path="/rules" element={<Rules />} />
133
134 <Route path="/about" element={<About />} />
135
136 <Route path="/rankings" element={<Rankings />}></Route>
137
138 <Route path="*" element={"404"} />
139
140 </Routes>
141
142 </main>
143
144
145 </div>
124 </> 146 </>
125 ); 147 );
126}; 148};
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index b18c9d9..7ae3850 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -11,12 +11,12 @@ interface GameCategoryProps {
11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { 11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
12 return ( 12 return (
13 <Link 13 <Link
14 className="bg-surface text-center w-full h-[100px] rounded-3xl text-foreground m-3 hover:bg-surface1 transition-colors flex flex-col justify-between p-4" 14 className="bg-block text-center w-full rounded-3xl text-foreground transition-colors flex flex-col justify-between p-2"
15 to={"/games/" + game.id + "?cat=" + cat.category.id} 15 to={"/games/" + game.id + "?cat=" + cat.category.id}
16 > 16 >
17 <p className="text-3xl font-semibold">{cat.category.name}</p> 17 <span className="text-2xl font-barlow-semicondensed-regular">{cat.category.name}</span>
18 <br /> 18 <br />
19 <p className="font-bold text-4xl">{cat.portal_count}</p> 19 <span className="text-5xl font-barlow-semicondensed-semibold">{cat.portal_count}</span>
20 </Link> 20 </Link>
21 ); 21 );
22}; 22};
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index f8fd179..6f2b194 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -18,18 +18,18 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 18
19 return ( 19 return (
20 <Link to={"/games/" + game.id} className="w-full"> 20 <Link to={"/games/" + game.id} className="w-full">
21 <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6"> 21 <div className="w-full h-64 bg-panel rounded-3xl overflow-hidden my-6">
22 <div className="w-full h-1/2 bg-cover overflow-hidden relative"> 22 <div className="w-full h-1/2 bg-cover overflow-hidden relative">
23 <div 23 <div
24 style={{ backgroundImage: `url(${game.image})` }} 24 style={{ backgroundImage: `url(${game.image})` }}
25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover" 25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover"
26 ></div> 26 ></div>
27 <span className="absolute inset-0 flex justify-center items-center"> 27 <span className="absolute inset-0 flex justify-center items-center">
28 <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b> 28 <b className="text-[56px] font-barlow-condensed-bold text-white">{game.name}</b>
29 </span> 29 </span>
30 </div> 30 </div>
31 <div className="flex justify-center items-center h-1/2"> 31 <div className="flex justify-center items-center h-1/2">
32 <div className="flex flex-row justify-between w-full"> 32 <div className="flex flex-row justify-between w-full gap-3 m-3">
33 {catInfo.map((cat, index) => { 33 {catInfo.map((cat, index) => {
34 return ( 34 return (
35 <GameCategory key={index} cat={cat} game={game} /> 35 <GameCategory key={index} cat={cat} game={game} />
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 88a5297..0083a3e 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -23,15 +23,15 @@ interface SidebarProps {
23 onUploadRun: () => void; 23 onUploadRun: () => void;
24} 24}
25 25
26function OpenSidebarIcon(){ 26function OpenSidebarIcon() {
27 return ( 27 return (
28 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/></svg> 28 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M15 3v18" /><path d="m8 9 3 3-3 3" /></svg>
29 ) 29 )
30} 30}
31 31
32function ClosedSidebarIcon(){ 32function ClosedSidebarIcon() {
33 return ( 33 return (
34<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg> ) 34 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M15 3v18" /><path d="m10 15-3-3 3-3" /></svg>)
35} 35}
36 36
37const Sidebar: React.FC<SidebarProps> = ({ 37const Sidebar: React.FC<SidebarProps> = ({
@@ -43,7 +43,7 @@ const Sidebar: React.FC<SidebarProps> = ({
43 const [searchData, setSearchData] = React.useState<Search | undefined>( 43 const [searchData, setSearchData] = React.useState<Search | undefined>(
44 undefined 44 undefined
45 ); 45 );
46 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 46 // const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false); 47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1); 48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
49 49
@@ -100,17 +100,16 @@ const Sidebar: React.FC<SidebarProps> = ({
100 const getButtonClasses = (buttonIndex: number) => { 100 const getButtonClasses = (buttonIndex: number) => {
101 const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1"; 101 const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1";
102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground"; 102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
103 103
104 return `${baseClasses} ${selectedClasses}`; 104 return `${baseClasses} ${selectedClasses}`;
105 }; 105 };
106 106
107 const iconClasses = "w-6 h-6 flex-shrink-0"; 107 const iconClasses = "w-6 h-6 flex-shrink-0";
108 108
109 return ( 109 return (
110 <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${ 110 <div className={`w-80 not-md:w-full text-white bg-block
111 isSidebarOpen ? 'w-80' : 'w-20' 111 }`}>
112 }`}> 112 <div className="flex items-center px-4 border-b border-border">
113 <div className="flex items-center h-20 px-4 border-b border-border">
114 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0"> 113 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
115 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" /> 114 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
116 {isSidebarOpen && ( 115 {isSidebarOpen && (
@@ -124,7 +123,7 @@ const Sidebar: React.FC<SidebarProps> = ({
124 </div> 123 </div>
125 )} 124 )}
126 </Link> 125 </Link>
127 126
128 <button 127 <button
129 onClick={_handle_sidebar_toggle} 128 onClick={_handle_sidebar_toggle}
130 className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground" 129 className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground"
@@ -135,9 +134,9 @@ const Sidebar: React.FC<SidebarProps> = ({
135 </div> 134 </div>
136 135
137 {/* Sidebar Content */} 136 {/* Sidebar Content */}
138 <div 137 <div
139 ref={sidebarRef} 138 ref={sidebarRef}
140 className="flex flex-col h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden" 139 className="flex flex-col overflow-y-auto overflow-x-hidden"
141 > 140 >
142 {isSidebarOpen && ( 141 {isSidebarOpen && (
143 <div className="p-4 border-b border-border min-w-0"> 142 <div className="p-4 border-b border-border min-w-0">
@@ -145,7 +144,7 @@ const Sidebar: React.FC<SidebarProps> = ({
145 <img src={SearchIcon} alt="Search" className={iconClasses} /> 144 <img src={SearchIcon} alt="Search" className={iconClasses} />
146 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span> 145 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span>
147 </div> 146 </div>
148 147
149 <div className="min-w-0"> 148 <div className="min-w-0">
150 <input 149 <input
151 ref={searchbarRef} 150 ref={searchbarRef}
@@ -214,7 +213,9 @@ const Sidebar: React.FC<SidebarProps> = ({
214 ].map(({ to, refIndex, icon, alt, label }) => ( 213 ].map(({ to, refIndex, icon, alt, label }) => (
215 <Link to={to} tabIndex={-1} key={refIndex}> 214 <Link to={to} tabIndex={-1} key={refIndex}>
216 <button 215 <button
217 ref={el => sidebarButtonRefs.current[refIndex] = el} 216 ref={el => {
217 sidebarButtonRefs.current[refIndex] = el
218 }}
218 className={getButtonClasses(refIndex)} 219 className={getButtonClasses(refIndex)}
219 onClick={() => handle_sidebar_click(refIndex)} 220 onClick={() => handle_sidebar_click(refIndex)}
220 > 221 >
@@ -254,8 +255,10 @@ const Sidebar: React.FC<SidebarProps> = ({
254 </div> 255 </div>
255 256
256 <Link to="/rules" tabIndex={-1}> 257 <Link to="/rules" tabIndex={-1}>
257 <button 258 <button
258 ref={el => sidebarButtonRefs.current[5] = el} 259 ref={el => {
260 sidebarButtonRefs.current[5] = el
261 }}
259 className={getButtonClasses(5)} 262 className={getButtonClasses(5)}
260 onClick={() => handle_sidebar_click(5)} 263 onClick={() => handle_sidebar_click(5)}
261 > 264 >
@@ -265,8 +268,10 @@ const Sidebar: React.FC<SidebarProps> = ({
265 </Link> 268 </Link>
266 269
267 <Link to="/about" tabIndex={-1}> 270 <Link to="/about" tabIndex={-1}>
268 <button 271 <button
269 ref={el => sidebarButtonRefs.current[6] = el} 272 ref={el => {
273 sidebarButtonRefs.current[6] = el
274 }}
270 className={getButtonClasses(6)} 275 className={getButtonClasses(6)}
271 onClick={() => handle_sidebar_click(6)} 276 onClick={() => handle_sidebar_click(6)}
272 > 277 >
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
deleted file mode 100644
index 7802d75..0000000
--- a/frontend/src/pages/About.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
1import React from "react";
2import ReactMarkdown from "react-markdown";
3import { Helmet } from "react-helmet";
4
5const About: React.FC = () => {
6 const [aboutText, setAboutText] = React.useState<string>("");
7
8 React.useEffect(() => {
9 const fetchReadme = async () => {
10 try {
11 const response = await fetch(
12 "https://raw.githubusercontent.com/pektezol/lphub/main/README.md"
13 );
14 if (!response.ok) {
15 throw new Error("Failed to fetch README");
16 }
17 const readmeText = await response.text();
18 setAboutText(readmeText);
19 } catch (error) {
20 console.error("Error fetching README:", error);
21 }
22 };
23 fetchReadme();
24 }, []);
25
26 return (
27 <div className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
28 <Helmet>
29 <title>LPHUB | About</title>
30 </Helmet>
31 <ReactMarkdown className={"overflow-auto"}>{aboutText}</ReactMarkdown>
32 </div>
33 );
34};
35
36export default About;
diff --git a/frontend/src/pages/About/About.tsx b/frontend/src/pages/About/About.tsx
index 7802d75..0db4bf2 100644
--- a/frontend/src/pages/About/About.tsx
+++ b/frontend/src/pages/About/About.tsx
@@ -24,7 +24,7 @@ const About: React.FC = () => {
24 }, []); 24 }, []);
25 25
26 return ( 26 return (
27 <div className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none"> 27 <div>
28 <Helmet> 28 <Helmet>
29 <title>LPHUB | About</title> 29 <title>LPHUB | About</title>
30 </Helmet> 30 </Helmet>
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
deleted file mode 100644
index 8587635..0000000
--- a/frontend/src/pages/Games.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
1import React from "react";
2import { Helmet } from "react-helmet";
3
4import GameEntry from "@components/GameEntry";
5import { Game } from "@customTypes/Game";
6
7interface GamesProps {
8 games: Game[];
9}
10
11const Games: React.FC<GamesProps> = ({ games }) => {
12 return (
13 <div className="ml-20 min-h-screen text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin">
14 <Helmet>
15 <title>LPHUB | Games</title>
16 </Helmet>
17 <section className="py-12 px-12 w-full">
18 <h1 className="text-3xl font-bold mb-8">Games</h1>
19 <div className="flex flex-col w-full">
20 {games.map((game, index) => (
21 <GameEntry game={game} key={index} />
22 ))}
23 </div>
24 </section>
25 </div>
26 );
27};
28
29export default Games;
diff --git a/frontend/src/pages/Games/Games.tsx b/frontend/src/pages/Games/Games.tsx
index e23b245..ea46733 100644
--- a/frontend/src/pages/Games/Games.tsx
+++ b/frontend/src/pages/Games/Games.tsx
@@ -10,12 +10,12 @@ interface GamesProps {
10 10
11const Games: React.FC<GamesProps> = ({ games }) => { 11const Games: React.FC<GamesProps> = ({ games }) => {
12 return ( 12 return (
13 <div className="ml-20 min-h-screen text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin"> 13 <div className="py-12 px-12 w-full">
14 <Helmet> 14 <Helmet>
15 <title>LPHUB | Games</title> 15 <title>LPHUB | Games</title>
16 </Helmet> 16 </Helmet>
17 <section className="py-12 px-12 w-full"> 17 <section>
18 <h1 className="text-3xl font-bold mb-8">Games</h1> 18 <h1 className="text-3xl mb-8">Games</h1>
19 <div className="flex flex-col w-full"> 19 <div className="flex flex-col w-full">
20 {games.map((game, index) => ( 20 {games.map((game, index) => (
21 <GameEntry game={game} key={index} /> 21 <GameEntry game={game} key={index} />
diff --git a/frontend/src/pages/Home/Homepage.tsx b/frontend/src/pages/Home/Homepage.tsx
index b4ac3b0..e4c2261 100644
--- a/frontend/src/pages/Home/Homepage.tsx
+++ b/frontend/src/pages/Home/Homepage.tsx
@@ -3,13 +3,13 @@ import { Helmet } from "react-helmet";
3 3
4const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
5 return ( 5 return (
6 <main className="ml-12 relative left-0 w-fullmin-h-screen p-4 sm:p-8 text-foreground font-[--font-barlow-semicondensed-regular]"> 6 <div>
7 <Helmet> 7 <Helmet>
8 <title>LPHUB | Homepage</title> 8 <title>LPHUB | Homepage</title>
9 </Helmet> 9 </Helmet>
10 <section className="p-8"> 10 <section className="p-8">
11 <p /> 11 <p />
12 <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1> 12 <h1 className="text-5xl font-barlow-condensed-bold mb-6 text-primary">Welcome to Least Portals Hub!</h1>
13 <p className="text-lg mb-4 leading-relaxed"> 13 <p className="text-lg mb-4 leading-relaxed">
14 At the moment, LPHUB is in beta state. This means that the site has 14 At the moment, LPHUB is in beta state. This means that the site has
15 only the core functionalities enabled for providing both collaborative 15 only the core functionalities enabled for providing both collaborative
@@ -24,7 +24,7 @@ const Homepage: React.FC = () => {
24 and the 'About LPHUB' pages. 24 and the 'About LPHUB' pages.
25 </p> 25 </p>
26 </section> 26 </section>
27 </main> 27 </div>
28 ); 28 );
29}; 29};
30 30
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
deleted file mode 100644
index b4ac3b0..0000000
--- a/frontend/src/pages/Homepage.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
1import React from "react";
2import { Helmet } from "react-helmet";
3
4const Homepage: React.FC = () => {
5 return (
6 <main className="ml-12 relative left-0 w-fullmin-h-screen p-4 sm:p-8 text-foreground font-[--font-barlow-semicondensed-regular]">
7 <Helmet>
8 <title>LPHUB | Homepage</title>
9 </Helmet>
10 <section className="p-8">
11 <p />
12 <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1>
13 <p className="text-lg mb-4 leading-relaxed">
14 At the moment, LPHUB is in beta state. This means that the site has
15 only the core functionalities enabled for providing both collaborative
16 information and competitive leaderboards.
17 </p>
18 <p className="text-lg mb-4 leading-relaxed">
19 The website should feel intuitive to navigate around. For any type of
20 feedback, reach us at LPHUB Discord server.
21 </p>
22 <p className="text-lg mb-4 leading-relaxed">
23 By using LPHUB, you agree that you have read the 'Leaderboard Rules'
24 and the 'About LPHUB' pages.
25 </p>
26 </section>
27 </main>
28 );
29};
30
31export default Homepage;
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
deleted file mode 100644
index 2f0491e..0000000
--- a/frontend/src/pages/Maplist.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
1import React, { useEffect } from "react";
2import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
3import { Helmet } from "react-helmet";
4
5import { API } from "@api/Api";
6import { Game } from "@customTypes/Game";
7import { GameChapter, GamesChapters } from "@customTypes/Chapters";
8
9const Maplist: React.FC = () => {
10 const [game, setGame] = React.useState<Game | null>(null);
11 const [catNum, setCatNum] = React.useState(0);
12 const [id, setId] = React.useState(0);
13 const [load, setLoad] = React.useState(false);
14 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0);
15 const [hasClicked, setHasClicked] = React.useState(false);
16 const [gameChapters, setGameChapters] = React.useState<GamesChapters>();
17 const [curChapter, setCurChapter] = React.useState<GameChapter>();
18 const [numChapters, setNumChapters] = React.useState<number>(0);
19
20 const [dropdownActive, setDropdownActive] = React.useState("none");
21
22 const params = useParams<{ id: string; chapter: string }>();
23 const location = useLocation();
24 const navigate = useNavigate();
25
26 function _update_currently_selected(catNum2: number) {
27 setCurrentlySelected(catNum2);
28 navigate("/games/" + game?.id + "?cat=" + catNum2);
29 setHasClicked(true);
30 }
31
32 const _fetch_chapters = async (chapter_id: string) => {
33 const chapters = await API.get_chapters(chapter_id);
34 setCurChapter(chapters);
35 };
36
37 const _handle_dropdown_click = () => {
38 if (dropdownActive === "none") {
39 setDropdownActive("block");
40 } else {
41 setDropdownActive("none");
42 }
43 };
44
45 // im sorry but im too lazy to fix this right now
46 useEffect(() => {
47 // gameID
48 const gameId = parseFloat(params.id || "");
49 setId(gameId);
50
51 // location query params
52 const queryParams = new URLSearchParams(location.search);
53 if (queryParams.get("chapter")) {
54 let cat = parseFloat(queryParams.get("chapter") || "");
55 if (gameId === 2) {
56 cat += 10;
57 }
58 _fetch_chapters(cat.toString());
59 }
60
61 const _fetch_game = async () => {
62 const games = await API.get_games();
63 const foundGame = games.find(game => game.id === gameId);
64 // console.log(foundGame)
65 if (foundGame) {
66 setGame(foundGame);
67 setLoad(false);
68 }
69 };
70
71 const _fetch_game_chapters = async () => {
72 const games_chapters = await API.get_games_chapters(gameId.toString());
73 setGameChapters(games_chapters);
74 setNumChapters(games_chapters.chapters.length);
75 };
76
77 setLoad(true);
78 _fetch_game();
79 _fetch_game_chapters();
80 }, [location.search]);
81
82 useEffect(() => {
83 const queryParams = new URLSearchParams(location.search);
84 if (gameChapters !== undefined && !queryParams.get("chapter")) {
85 _fetch_chapters(gameChapters!.chapters[0].id.toString());
86 }
87 }, [gameChapters, location.search]);
88
89 return (
90 <main className="*:text-foreground w-[calc(100vw-80px)] relative left-0 ml-20 min-h-screen p-4 sm:p-8">
91 <Helmet>
92 <title>LPHUB | Maplist</title>
93 </Helmet>
94
95 <section className="mt-5">
96 <Link to="/games">
97 <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2">
98 <i className="triangle mr-2"></i>
99 <span className="px-2">Games List</span>
100 </button>
101 </Link>
102 </section>
103
104 {load ? (
105 <div></div>
106 ) : (
107 <section>
108 <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground">
109 {game?.name}
110 </h1>
111
112 <div
113 className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative"
114 style={{ backgroundImage: `url(${game?.image})` }}
115 >
116 <div className="backdrop-blur-sm flex flex-col w-full">
117 <div className="h-full flex flex-col justify-center items-center py-6">
118 <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground">
119 {
120 game?.category_portals.find(
121 obj => obj.category.id === catNum + 1
122 )?.portal_count
123 }
124 </h2>
125 <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground">
126 portals
127 </h3>
128 </div>
129
130 <div className="flex h-12 bg-surface gap-0.5">
131 {game?.category_portals.map((cat, index) => (
132 <button
133 key={index}
134 className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${
135 currentlySelected === cat.category.id ||
136 (cat.category.id - 1 === catNum && !hasClicked)
137 ? "bg-surface"
138 : "bg-surface1 hover:bg-surface"
139 }`}
140 onClick={() => {
141 setCatNum(cat.category.id - 1);
142 _update_currently_selected(cat.category.id);
143 }}
144 >
145 <span className="truncate">{cat.category.name}</span>
146 </button>
147 ))}
148 </div>
149 </div>
150 </div>
151
152 <div>
153 <section>
154 <div>
155 <span className="text-lg sm:text-lg translate-y-1.5 block mt-2.5 text-foreground">
156 {curChapter?.chapter.name.split(" - ")[0]}
157 </span>
158 </div>
159 <div
160 onClick={_handle_dropdown_click}
161 className="cursor-pointer select-none flex w-fit items-center"
162 >
163 <span className="text-foreground text-base sm:text-2xl">
164 {curChapter?.chapter.name.split(" - ")[1]}
165 </span>
166 <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i>
167 </div>
168 \
169 <div
170 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${
171 dropdownActive === "none" ? "hidden" : "block"
172 }`}
173 >
174 {gameChapters?.chapters.map((chapter, i) => {
175 return (
176 <div
177 key={i}
178 className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground"
179 onClick={() => {
180 _fetch_chapters(chapter.id.toString());
181 _handle_dropdown_click();
182 }}
183 >
184 {chapter.name}
185 </div>
186 );
187 })}
188 </div>
189 </section>
190
191 <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5">
192 {curChapter?.maps.map((map, i) => {
193 return (
194 <div key={i} className="bg-surface rounded-3xl overflow-hidden">
195 <Link to={`/maps/${map.id}`}>
196 <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate">
197 {map.name}
198 </span>
199 <div
200 className="flex h-40 sm:h-48 bg-cover relative"
201 style={{ backgroundImage: `url(${map.image})` }}
202 >
203 <div className="backdrop-blur-sm w-full flex items-center justify-center">
204 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5">
205 {map.is_disabled
206 ? map.category_portals[0].portal_count
207 : map.category_portals.find(
208 obj => obj.category.id === catNum + 1
209 )?.portal_count}
210 </span>
211 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white">
212 portals
213 </span>
214 </div>
215 </div>
216
217 <div className="flex mx-2.5 my-4">
218 <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px">
219 {[1, 2, 3, 4, 5].map((point) => (
220 <div
221 key={point}
222 className={`flex h-0.5 w-full rounded-3xl ${
223 point <= (map.difficulty + 1)
224 ? map.difficulty === 0
225 ? "bg-green-500"
226 : map.difficulty === 1 || map.difficulty === 2
227 ? "bg-lime-500"
228 : map.difficulty === 3
229 ? "bg-red-400"
230 : "bg-red-600"
231 : "bg-surface1"
232 }`}
233 />
234 ))}
235 </div>
236 </div>
237 </Link>
238 </div>
239 );
240 })}
241 </section>
242 </div>
243 </section>
244 )}
245 </main>
246 );
247};
248
249export default Maplist;
diff --git a/frontend/src/pages/Maplist/Maplist.tsx b/frontend/src/pages/Maplist/Maplist.tsx
index 572eb27..8d9c14a 100644
--- a/frontend/src/pages/Maplist/Maplist.tsx
+++ b/frontend/src/pages/Maplist/Maplist.tsx
@@ -87,162 +87,159 @@ const Maplist: React.FC = () => {
87 }, [gameChapters, location.search]); 87 }, [gameChapters, location.search]);
88 88
89 return ( 89 return (
90 <main className="*:text-foreground w-[calc(100vw-80px)] relative left-0 ml-20 min-h-screen p-4 sm:p-8"> 90 <div>
91 <Helmet> 91 <Helmet>
92 <title>LPHUB | Maplist</title> 92 <title>LPHUB | Maplist</title>
93 </Helmet> 93 </Helmet>
94 94
95 <section className="mt-5"> 95 <section className="mt-5">
96 <Link to="/games"> 96 <Link to="/games">
97 <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"> 97 <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2">
98 <i className="triangle mr-2"></i> 98 <i className="triangle mr-2"></i>
99 <span className="px-2">Games List</span> 99 <span className="px-2">Games List</span>
100 </button> 100 </button>
101 </Link> 101 </Link>
102 </section> 102 </section>
103 103
104 {load ? ( 104 {load ? (
105 <div></div> 105 <div></div>
106 ) : ( 106 ) : (
107 <section>
108 <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground">
109 {game?.name}
110 </h1>
111
112 <div
113 className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative"
114 style={{ backgroundImage: `url(${game?.image})` }}
115 >
116 <div className="backdrop-blur-sm flex flex-col w-full">
117 <div className="h-full flex flex-col justify-center items-center py-6">
118 <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground">
119 {
120 game?.category_portals.find(
121 obj => obj.category.id === catNum + 1
122 )?.portal_count
123 }
124 </h2>
125 <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground">
126 portals
127 </h3>
128 </div>
129
130 <div className="flex h-12 bg-surface gap-0.5">
131 {game?.category_portals.map((cat, index) => (
132 <button
133 key={index}
134 className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${
135 currentlySelected === cat.category.id ||
136 (cat.category.id - 1 === catNum && !hasClicked)
137 ? "bg-surface"
138 : "bg-surface1 hover:bg-surface"
139 }`}
140 onClick={() => {
141 setCatNum(cat.category.id - 1);
142 _update_currently_selected(cat.category.id);
143 }}
144 >
145 <span className="truncate">{cat.category.name}</span>
146 </button>
147 ))}
148 </div>
149 </div>
150 </div>
151
152 <div>
153 <section> 107 <section>
154 <div> 108 <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground">
155 <span className="text-lg sm:text-lg translate-y-1.5 block mt-2.5 text-foreground"> 109 {game?.name}
156 {curChapter?.chapter.name.split(" - ")[0]} 110 </h1>
157 </span> 111
158 </div>
159 <div
160 onClick={_handle_dropdown_click}
161 className="cursor-pointer select-none flex w-fit items-center"
162 >
163 <span className="text-foreground text-base sm:text-2xl">
164 {curChapter?.chapter.name.split(" - ")[1]}
165 </span>
166 <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i>
167 </div>
168 \
169 <div 112 <div
170 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${ 113 className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative"
171 dropdownActive === "none" ? "hidden" : "block" 114 style={{ backgroundImage: `url(${game?.image})` }}
172 }`}
173 > 115 >
174 {gameChapters?.chapters.map((chapter, i) => { 116 <div className="backdrop-blur-sm flex flex-col w-full">
175 return ( 117 <div className="h-full flex flex-col justify-center items-center py-6">
176 <div 118 <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground">
177 key={i} 119 {
178 className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground" 120 game?.category_portals.find(
179 onClick={() => { 121 obj => obj.category.id === catNum + 1
180 _fetch_chapters(chapter.id.toString()); 122 )?.portal_count
181 _handle_dropdown_click(); 123 }
182 }} 124 </h2>
183 > 125 <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground">
184 {chapter.name} 126 portals
127 </h3>
128 </div>
129
130 <div className="flex h-12 bg-surface gap-0.5">
131 {game?.category_portals.map((cat, index) => (
132 <button
133 key={index}
134 className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${currentlySelected === cat.category.id ||
135 (cat.category.id - 1 === catNum && !hasClicked)
136 ? "bg-surface"
137 : "bg-surface1 hover:bg-surface"
138 }`}
139 onClick={() => {
140 setCatNum(cat.category.id - 1);
141 _update_currently_selected(cat.category.id);
142 }}
143 >
144 <span className="truncate">{cat.category.name}</span>
145 </button>
146 ))}
147 </div>
185 </div> 148 </div>
186 );
187 })}
188 </div> 149 </div>
189 </section>
190 150
191 <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5"> 151 <div>
192 {curChapter?.maps.map((map, i) => { 152 <section>
193 return ( 153 <div>
194 <div key={i} className="bg-surface rounded-3xl overflow-hidden"> 154 <span className="text-lg sm:text-lg translate-y-1.5 block mt-2.5 text-foreground">
195 <Link to={`/maps/${map.id}`}> 155 {curChapter?.chapter.name.split(" - ")[0]}
196 <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate"> 156 </span>
197 {map.name} 157 </div>
198 </span>
199 <div 158 <div
200 className="flex h-40 sm:h-48 bg-cover relative" 159 onClick={_handle_dropdown_click}
201 style={{ backgroundImage: `url(${map.image})` }} 160 className="cursor-pointer select-none flex w-fit items-center"
202 > 161 >
203 <div className="backdrop-blur-sm w-full flex items-center justify-center"> 162 <span className="text-foreground text-base sm:text-2xl">
204 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5"> 163 {curChapter?.chapter.name.split(" - ")[1]}
205 {map.is_disabled
206 ? map.category_portals[0].portal_count
207 : map.category_portals.find(
208 obj => obj.category.id === catNum + 1
209 )?.portal_count}
210 </span> 164 </span>
211 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white"> 165 <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i>
212 portals
213 </span>
214 </div>
215 </div> 166 </div>
216 167 \
217 <div className="flex mx-2.5 my-4"> 168 <div
218 <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px"> 169 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${dropdownActive === "none" ? "hidden" : "block"
219 {[1, 2, 3, 4, 5].map((point) => (
220 <div
221 key={point}
222 className={`flex h-0.5 w-full rounded-3xl ${
223 point <= (map.difficulty + 1)
224 ? map.difficulty === 0
225 ? "bg-green-500"
226 : map.difficulty === 1 || map.difficulty === 2
227 ? "bg-lime-500"
228 : map.difficulty === 3
229 ? "bg-red-400"
230 : "bg-red-600"
231 : "bg-surface1"
232 }`} 170 }`}
233 /> 171 >
234 ))} 172 {gameChapters?.chapters.map((chapter, i) => {
235 </div> 173 return (
174 <div
175 key={i}
176 className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground"
177 onClick={() => {
178 _fetch_chapters(chapter.id.toString());
179 _handle_dropdown_click();
180 }}
181 >
182 {chapter.name}
183 </div>
184 );
185 })}
236 </div> 186 </div>
237 </Link> 187 </section>
238 </div> 188
239 ); 189 <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5">
240 })} 190 {curChapter?.maps.map((map, i) => {
191 return (
192 <div key={i} className="bg-surface rounded-3xl overflow-hidden">
193 <Link to={`/maps/${map.id}`}>
194 <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate">
195 {map.name}
196 </span>
197 <div
198 className="flex h-40 sm:h-48 bg-cover relative"
199 style={{ backgroundImage: `url(${map.image})` }}
200 >
201 <div className="backdrop-blur-sm w-full flex items-center justify-center">
202 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5">
203 {map.is_disabled
204 ? map.category_portals[0].portal_count
205 : map.category_portals.find(
206 obj => obj.category.id === catNum + 1
207 )?.portal_count}
208 </span>
209 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white">
210 portals
211 </span>
212 </div>
213 </div>
214
215 <div className="flex mx-2.5 my-4">
216 <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px">
217 {[1, 2, 3, 4, 5].map((point) => (
218 <div
219 key={point}
220 className={`flex h-0.5 w-full rounded-3xl ${point <= (map.difficulty + 1)
221 ? map.difficulty === 0
222 ? "bg-green-500"
223 : map.difficulty === 1 || map.difficulty === 2
224 ? "bg-lime-500"
225 : map.difficulty === 3
226 ? "bg-red-400"
227 : "bg-red-600"
228 : "bg-surface1"
229 }`}
230 />
231 ))}
232 </div>
233 </div>
234 </Link>
235 </div>
236 );
237 })}
238 </section>
239 </div>
241 </section> 240 </section>
242 </div>
243 </section>
244 )} 241 )}
245 </main> 242 </div>
246 ); 243 );
247}; 244};
248 245
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
deleted file mode 100644
index 50fe03b..0000000
--- a/frontend/src/pages/Maps.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
1import React from "react";
2import { Link, useLocation } from "react-router-dom";
3import { Helmet } from "react-helmet";
4
5import { PortalIcon, FlagIcon, ChatIcon } from "../images/Images";
6import Summary from "@components/Summary";
7import Leaderboards from "@components/Leaderboards";
8import Discussions from "@components/Discussions";
9import ModMenu from "@components/ModMenu";
10import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map";
11import { API } from "@api/Api";
12
13interface MapProps {
14 token?: string;
15 isModerator: boolean;
16}
17
18const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
19 const [selectedRun, setSelectedRun] = React.useState<number>(0);
20
21 const [mapSummaryData, setMapSummaryData] = React.useState<
22 MapSummary | undefined
23 >(undefined);
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<
25 MapLeaderboard | undefined
26 >(undefined);
27 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<
28 MapDiscussions | undefined
29 >(undefined);
30
31 const [navState, setNavState] = React.useState<number>(0);
32
33 const location = useLocation();
34
35 const mapID = location.pathname.split("/")[2];
36
37 const _fetch_map_summary = React.useCallback(async () => {
38 const mapSummary = await API.get_map_summary(mapID);
39 setMapSummaryData(mapSummary);
40 }, [mapID]);
41
42 const _fetch_map_leaderboards = React.useCallback(async () => {
43 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1");
44 setMapLeaderboardData(mapLeaderboards);
45 }, [mapID]);
46
47 const _fetch_map_discussions = React.useCallback(async () => {
48 const mapDiscussions = await API.get_map_discussions(mapID);
49 setMapDiscussionsData(mapDiscussions);
50 }, [mapID]);
51
52 React.useEffect(() => {
53 _fetch_map_summary();
54 _fetch_map_leaderboards();
55 _fetch_map_discussions();
56 }, [
57 mapID,
58 _fetch_map_discussions,
59 _fetch_map_leaderboards,
60 _fetch_map_summary,
61 ]);
62
63 if (!mapSummaryData) {
64 // loading placeholder
65 return (
66 <>
67 <main className="*:text-foreground relative left-0 w-[calc(100%-20rem)] min-h-screen p-4 sm:p-8">
68 <section id="section1" className="summary1">
69 <div>
70 <Link to="/games">
71 <button
72 className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"
73 >
74 <i className="triangle"></i>
75 <span className="px-2">Games List</span>
76 </button>
77 </Link>
78 </div>
79 </section>
80
81 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
82 <button className="nav-button">
83 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
84 <span>Summary</span>
85 </button>
86 <button className="nav-button">
87 <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
88 <span>Leaderboards</span>
89 </button>
90 <button className="nav-button">
91 <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
92 <span>Discussions</span>
93 </button>
94 </section>
95
96 <section id="section6" className="summary2 mt-4" />
97 </main>
98 </>
99 );
100 }
101
102 return (
103 <>
104 <Helmet>
105 <title>LPHUB | {mapSummaryData.map.map_name}</title>
106 <meta name="description" content={mapSummaryData.map.map_name} />
107 </Helmet>
108 {isModerator && (
109 <ModMenu
110 token={token}
111 data={mapSummaryData}
112 selectedRun={selectedRun}
113 mapID={mapID}
114 />
115 )}
116
117 <div id="background-image">
118 <img src={mapSummaryData.map.image} alt="" />
119 </div>
120 <main className="relative left-0 w-full sm:ml-80 sm:w-[calc(100%-20rem)] min-h-screen max-h-screen overflow-y-auto p-4 sm:p-8 scrollbar-thin scrollbar-track-surface scrollbar-thumb-muted hover:scrollbar-thumb-surface1">
121 <section id="section1" className="summary1">
122 <div>
123 <Link to="/games">
124 <button
125 className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"
126 >
127 <i className="triangle"></i>
128 <span className="px-2">Games List</span>
129 </button>
130 </Link>
131 <Link
132 to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}
133 >
134 <button
135 className="nav-button ml-2"
136 >
137 <i className="triangle"></i>
138 <span className="px-2">{mapSummaryData.map.chapter_name}</span>
139 </button>
140 </Link>
141 <br />
142 <span className="block mt-2 text-lg sm:text-xl text-foreground">
143 <b>{mapSummaryData.map.map_name}</b>
144 </span>
145 </div>
146 </section>
147
148 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
149 <button className="nav-button" onClick={() => setNavState(0)}>
150 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
151 <span>Summary</span>
152 </button>
153 <button className="nav-button" onClick={() => setNavState(1)}>
154 <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
155 <span>Leaderboards</span>
156 </button>
157 <button className="nav-button" onClick={() => setNavState(2)}>
158 <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
159 <span>Discussions</span>
160 </button>
161 </section>
162
163 {navState === 0 && (
164 <Summary
165 selectedRun={selectedRun}
166 setSelectedRun={setSelectedRun}
167 data={mapSummaryData}
168 />
169 )}
170 {navState === 1 && <Leaderboards mapID={mapID} />}
171 {navState === 2 && (
172 <Discussions
173 data={mapDiscussionsData}
174 token={token}
175 isModerator={isModerator}
176 mapID={mapID}
177 onRefresh={() => _fetch_map_discussions()}
178 />
179 )}
180 </main>
181 </>
182 );
183};
184
185export default Maps;
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
deleted file mode 100644
index f44f587..0000000
--- a/frontend/src/pages/Profile.tsx
+++ /dev/null
@@ -1,633 +0,0 @@
1import React from "react";
2import { Link, useNavigate } from "react-router-dom";
3import { Helmet } from "react-helmet";
4
5import {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16 DeleteIcon,
17} from "@images/Images";
18import { UserProfile } from "@customTypes/Profile";
19import { Game, GameChapters } from "@customTypes/Game";
20import { Map } from "@customTypes/Map";
21import { ticks_to_time } from "@utils/Time";
22import { API } from "@api/Api";
23import useConfirm from "@hooks/UseConfirm";
24import useMessage from "@hooks/UseMessage";
25import useMessageLoad from "@hooks/UseMessageLoad";
26
27interface ProfileProps {
28 profile?: UserProfile;
29 token?: string;
30 gameData: Game[];
31 onDeleteRecord: () => void;
32}
33
34const Profile: React.FC<ProfileProps> = ({
35 profile,
36 token,
37 gameData,
38 onDeleteRecord,
39}) => {
40 const { confirm, ConfirmDialogComponent } = useConfirm();
41 const { message, MessageDialogComponent } = useMessage();
42 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
43 useMessageLoad();
44 const [navState, setNavState] = React.useState(0);
45 const [pageNumber, setPageNumber] = React.useState(1);
46 const [pageMax, setPageMax] = React.useState(0);
47
48 const [game, setGame] = React.useState("0");
49 const [chapter, setChapter] = React.useState("0");
50 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
51 null
52 );
53 const [maps, setMaps] = React.useState<Map[]>([]);
54
55 const navigate = useNavigate();
56
57 const _update_profile = () => {
58 if (token) {
59 API.post_profile(token).then(() => navigate(0));
60 }
61 };
62
63 const _get_game_chapters = React.useCallback(async () => {
64 if (game && game !== "0") {
65 const gameChapters = await API.get_games_chapters(game);
66 setChapterData(gameChapters);
67 } else if (game && game === "0") {
68 setPageMax(Math.ceil(profile!.records.length / 20));
69 setPageNumber(1);
70 }
71 }, [game, profile]);
72
73 const _get_game_maps = React.useCallback(async () => {
74 if (chapter === "0") {
75 const gameMaps = await API.get_game_maps(game);
76 setMaps(gameMaps);
77 setPageMax(Math.ceil(gameMaps.length / 20));
78 setPageNumber(1);
79 } else {
80 const gameChapters = await API.get_chapters(chapter);
81 setMaps(gameChapters.maps);
82 setPageMax(Math.ceil(gameChapters.maps.length / 20));
83 setPageNumber(1);
84 }
85 }, [chapter, game]);
86
87 const _delete_submission = async (map_id: number, record_id: number) => {
88 const userConfirmed = await confirm(
89 "Delete Record",
90 "Are you sure you want to delete this record?"
91 );
92
93 if (!userConfirmed) {
94 return;
95 }
96
97 messageLoad("Deleting...");
98
99 const api_success = await API.delete_map_record(token!, map_id, record_id);
100 messageLoadClose();
101 if (api_success) {
102 await message("Delete Record", "Successfully deleted record.");
103 onDeleteRecord();
104 } else {
105 await message("Delete Record", "Could not delete record.");
106 }
107 };
108
109 React.useEffect(() => {
110 if (!profile) {
111 navigate("/");
112 }
113 }, [profile, navigate]);
114
115 React.useEffect(() => {
116 if (profile) {
117 _get_game_chapters();
118 }
119 }, [profile, game, _get_game_chapters]);
120
121 React.useEffect(() => {
122 if (profile && game !== "0") {
123 _get_game_maps();
124 }
125 }, [profile, game, chapter, chapterData, _get_game_maps]);
126
127 if (!profile) {
128 return <></>;
129 }
130
131 return (
132 <div>
133 <Helmet>
134 <title>LPHUB | {profile.user_name}</title>
135 <meta name="description" content={profile.user_name} />
136 </Helmet>
137 {MessageDialogComponent}
138 {MessageDialogLoadComponent}
139 {ConfirmDialogComponent}
140
141 <main>
142 <section id="section1" className="profile">
143 {profile.profile ? (
144 <div id="profile-image" onClick={_update_profile}>
145 <img src={profile.avatar_link} alt="profile-image"></img>
146 <span>Refresh</span>
147 </div>
148 ) : (
149 <div>
150 <img src={profile.avatar_link} alt="profile-image"></img>
151 </div>
152 )}
153
154 <div id="profile-top">
155 <div>
156 <div>{profile.user_name}</div>
157 <div>
158 {profile.country_code === "XX" ? (
159 ""
160 ) : (
161 <img
162 src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`}
163 alt={profile.country_code}
164 />
165 )}
166 </div>
167 <div>
168 {profile.titles.map(e => (
169 <span
170 className="titles"
171 style={{ backgroundColor: `#${e.color}` }}
172 >
173 {e.name}
174 </span>
175 ))}
176 </div>
177 </div>
178 <div>
179 {profile.links.steam === "-" ? (
180 ""
181 ) : (
182 <a href={profile.links.steam}>
183 <img src={SteamIcon} alt="Steam" />
184 </a>
185 )}
186 {profile.links.twitch === "-" ? (
187 ""
188 ) : (
189 <a href={profile.links.twitch}>
190 <img src={TwitchIcon} alt="Twitch" />
191 </a>
192 )}
193 {profile.links.youtube === "-" ? (
194 ""
195 ) : (
196 <a href={profile.links.youtube}>
197 <img src={YouTubeIcon} alt="Youtube" />
198 </a>
199 )}
200 {profile.links.p2sr === "-" ? (
201 ""
202 ) : (
203 <a href={profile.links.p2sr}>
204 <img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} />
205 </a>
206 )}
207 </div>
208 </div>
209 <div id="profile-bottom">
210 <div>
211 <span>Overall</span>
212 <span>
213 {profile.rankings.overall.rank === 0
214 ? "N/A "
215 : "#" + profile.rankings.overall.rank + " "}
216 <span>
217 ({profile.rankings.overall.completion_count}/
218 {profile.rankings.overall.completion_total})
219 </span>
220 </span>
221 </div>
222 <div>
223 <span>Singleplayer</span>
224 <span>
225 {profile.rankings.singleplayer.rank === 0
226 ? "N/A "
227 : "#" + profile.rankings.singleplayer.rank + " "}
228 <span>
229 ({profile.rankings.singleplayer.completion_count}/
230 {profile.rankings.singleplayer.completion_total})
231 </span>
232 </span>
233 </div>
234 <div>
235 <span>Cooperative</span>
236 <span>
237 {profile.rankings.cooperative.rank === 0
238 ? "N/A "
239 : "#" + profile.rankings.cooperative.rank + " "}
240 <span>
241 ({profile.rankings.cooperative.completion_count}/
242 {profile.rankings.cooperative.completion_total})
243 </span>
244 </span>
245 </div>
246 </div>
247 </section>
248
249 <section id="section2" className="profile">
250 <button onClick={() => setNavState(0)}>
251 <img src={FlagIcon} alt="" />
252 &nbsp;Player Records
253 </button>
254 <button onClick={() => setNavState(1)}>
255 <img src={StatisticsIcon} alt="" />
256 &nbsp;Statistics
257 </button>
258 </section>
259
260 <section id="section3" className="profile1">
261 <div id="profileboard-nav">
262 {gameData === null ? (
263 <select>error</select>
264 ) : (
265 <select
266 id="select-game"
267 onChange={() => {
268 setGame(
269 (document.querySelector("#select-game") as HTMLInputElement)
270 .value
271 );
272 setChapter("0");
273 const chapterSelect = document.querySelector(
274 "#select-chapter"
275 ) as HTMLSelectElement;
276 if (chapterSelect) {
277 chapterSelect.value = "0";
278 }
279 }}
280 >
281 <option value={0} key={0}>
282 All Scores
283 </option>
284 {gameData.map((e, i) => (
285 <option value={e.id} key={i + 1}>
286 {e.name}
287 </option>
288 ))}
289 </select>
290 )}
291
292 {game === "0" ? (
293 <select disabled>
294 <option>All Chapters</option>
295 </select>
296 ) : chapterData === null ? (
297 <select></select>
298 ) : (
299 <select
300 id="select-chapter"
301 onChange={() =>
302 setChapter(
303 (
304 document.querySelector(
305 "#select-chapter"
306 ) as HTMLInputElement
307 ).value
308 )
309 }
310 >
311 <option value="0" key="0">
312 All Chapters
313 </option>
314 {chapterData.chapters
315 .filter(e => e.is_disabled === false)
316 .map((e, i) => (
317 <option value={e.id} key={i + 1}>
318 {e.name}
319 </option>
320 ))}
321 </select>
322 )}
323 </div>
324 <div id="profileboard-top">
325 <span>
326 <span>Map Name</span>
327 <img src={SortIcon} alt="" />
328 </span>
329 <span style={{ justifyContent: "center" }}>
330 <span>Portals</span>
331 <img src={SortIcon} alt="" />
332 </span>
333 <span style={{ justifyContent: "center" }}>
334 <span>WRΔ </span>
335 <img src={SortIcon} alt="" />
336 </span>
337 <span style={{ justifyContent: "center" }}>
338 <span>Time</span>
339 <img src={SortIcon} alt="" />
340 </span>
341 <span> </span>
342 <span>
343 <span>Rank</span>
344 <img src={SortIcon} alt="" />
345 </span>
346 <span>
347 <span>Date</span>
348 <img src={SortIcon} alt="" />
349 </span>
350 <div id="page-number">
351 <div>
352 <button
353 onClick={() => {
354 if (pageNumber !== 1) {
355 setPageNumber(prevPageNumber => prevPageNumber - 1);
356 const records = document.querySelectorAll(
357 ".profileboard-record"
358 );
359 records.forEach(r => {
360 (r as HTMLInputElement).style.height = "44px";
361 });
362 }
363 }}
364 >
365 <i
366 className="triangle"
367 style={{ position: "relative", left: "-5px" }}
368 ></i>{" "}
369 </button>
370 <span>
371 {pageNumber}/{pageMax}
372 </span>
373 <button
374 onClick={() => {
375 if (pageNumber !== pageMax) {
376 setPageNumber(prevPageNumber => prevPageNumber + 1);
377 const records = document.querySelectorAll(
378 ".profileboard-record"
379 );
380 records.forEach(r => {
381 (r as HTMLInputElement).style.height = "44px";
382 });
383 }
384 }}
385 >
386 <i
387 className="triangle"
388 style={{
389 position: "relative",
390 left: "5px",
391 transform: "rotate(180deg)",
392 }}
393 ></i>{" "}
394 </button>
395 </div>
396 </div>
397 </div>
398 <hr />
399 <div id="profileboard-records">
400 {game === "0" ? (
401 profile.records
402 .sort((a, b) => a.map_id - b.map_id)
403 .map((r, index) =>
404 Math.ceil((index + 1) / 20) === pageNumber ? (
405 <button className="profileboard-record" key={index}>
406 {r.scores.map((e, i) => (
407 <>
408 {i !== 0 ? (
409 <hr style={{ gridColumn: "1 / span 8" }} />
410 ) : (
411 ""
412 )}
413
414 <Link to={`/maps/${r.map_id}`}>
415 <span>{r.map_name}</span>
416 </Link>
417
418 <span style={{ display: "grid" }}>
419 {e.score_count}
420 </span>
421
422 <span style={{ display: "grid" }}>
423 {e.score_count - r.map_wr_count > 0
424 ? `+${e.score_count - r.map_wr_count}`
425 : `-`}
426 </span>
427 <span style={{ display: "grid" }}>
428 {ticks_to_time(e.score_time)}
429 </span>
430 <span> </span>
431 {i === 0 ? (
432 <span>#{r.placement}</span>
433 ) : (
434 <span> </span>
435 )}
436 <span>{e.date.split("T")[0]}</span>
437 <span style={{ flexDirection: "row-reverse" }}>
438 <button
439 style={{ marginRight: "10px" }}
440 onClick={() => {
441 message(
442 "Demo Information",
443 `Demo ID: ${e.demo_id}`
444 );
445 }}
446 >
447 <img src={ThreedotIcon} alt="demo_id" />
448 </button>
449 <button
450 onClick={() => {
451 _delete_submission(r.map_id, e.record_id);
452 }}
453 >
454 <img src={DeleteIcon} alt="delete icon"></img>
455 </button>
456 <button
457 onClick={() =>
458 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
459 }
460 >
461 <img src={DownloadIcon} alt="download" />
462 </button>
463 {i === 0 && r.scores.length > 1 ? (
464 <button
465 onClick={() => {
466 (
467 document.querySelectorAll(
468 ".profileboard-record"
469 )[index % 20] as HTMLInputElement
470 ).style.height === "44px" ||
471 (
472 document.querySelectorAll(
473 ".profileboard-record"
474 )[index % 20] as HTMLInputElement
475 ).style.height === ""
476 ? ((
477 document.querySelectorAll(
478 ".profileboard-record"
479 )[index % 20] as HTMLInputElement
480 ).style.height =
481 `${r.scores.length * 46}px`)
482 : ((
483 document.querySelectorAll(
484 ".profileboard-record"
485 )[index % 20] as HTMLInputElement
486 ).style.height = "44px");
487 }}
488 >
489 <img src={HistoryIcon} alt="history" />
490 </button>
491 ) : (
492 ""
493 )}
494 </span>
495 </>
496 ))}
497 </button>
498 ) : (
499 ""
500 )
501 )
502 ) : maps ? (
503 maps
504 .filter(e => e.is_disabled === false)
505 .sort((a, b) => a.id - b.id)
506 .map((r, index) => {
507 if (Math.ceil((index + 1) / 20) === pageNumber) {
508 let record = profile.records.find(e => e.map_id === r.id);
509 return record === undefined ? (
510 <button
511 className="profileboard-record"
512 key={index}
513 style={{ backgroundColor: "#1b1b20" }}
514 >
515 <Link to={`/maps/${r.id}`}>
516 <span>{r.name}</span>
517 </Link>
518 <span style={{ display: "grid" }}>N/A</span>
519 <span style={{ display: "grid" }}>N/A</span>
520 <span>N/A</span>
521 <span> </span>
522 <span>N/A</span>
523 <span>N/A</span>
524 <span style={{ flexDirection: "row-reverse" }}></span>
525 </button>
526 ) : (
527 <button className="profileboard-record" key={index}>
528 {record.scores.map((e, i) => (
529 <>
530 {i !== 0 ? (
531 <hr style={{ gridColumn: "1 / span 8" }} />
532 ) : (
533 ""
534 )}
535 <Link to={`/maps/${r.id}`}>
536 <span>{r.name}</span>
537 </Link>
538 <span style={{ display: "grid" }}>
539 {record!.scores[i].score_count}
540 </span>
541 <span style={{ display: "grid" }}>
542 {record!.scores[i].score_count -
543 record!.map_wr_count >
544 0
545 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
546 : `-`}
547 </span>
548 <span style={{ display: "grid" }}>
549 {ticks_to_time(record!.scores[i].score_time)}
550 </span>
551 <span> </span>
552 {i === 0 ? (
553 <span>#{record!.placement}</span>
554 ) : (
555 <span> </span>
556 )}
557 <span>{record!.scores[i].date.split("T")[0]}</span>
558 <span style={{ flexDirection: "row-reverse" }}>
559 <button
560 onClick={() => {
561 message(
562 "Demo Information",
563 `Demo ID: ${e.demo_id}`
564 );
565 }}
566 >
567 <img src={ThreedotIcon} alt="demo_id" />
568 </button>
569 <button
570 onClick={() => {
571 _delete_submission(r.id, e.record_id);
572 }}
573 >
574 <img src={DeleteIcon} alt="delete icon"></img>
575 </button>
576 <button
577 onClick={() =>
578 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
579 }
580 >
581 <img src={DownloadIcon} alt="download" />
582 </button>
583 {i === 0 && record!.scores.length > 1 ? (
584 <button
585 onClick={() => {
586 (
587 document.querySelectorAll(
588 ".profileboard-record"
589 )[index % 20] as HTMLInputElement
590 ).style.height === "44px" ||
591 (
592 document.querySelectorAll(
593 ".profileboard-record"
594 )[index % 20] as HTMLInputElement
595 ).style.height === ""
596 ? ((
597 document.querySelectorAll(
598 ".profileboard-record"
599 )[index % 20] as HTMLInputElement
600 ).style.height =
601 `${record!.scores.length * 46}px`)
602 : ((
603 document.querySelectorAll(
604 ".profileboard-record"
605 )[index % 20] as HTMLInputElement
606 ).style.height = "44px");
607 }}
608 >
609 <img src={HistoryIcon} alt="history" />
610 </button>
611 ) : (
612 ""
613 )}
614 </span>
615 </>
616 ))}
617 </button>
618 );
619 } else {
620 return null;
621 }
622 })
623 ) : (
624 <>{console.warn(maps)}</>
625 )}
626 </div>
627 </section>
628 </main>
629 </div>
630 );
631};
632
633export default Profile;
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
deleted file mode 100644
index dec0e17..0000000
--- a/frontend/src/pages/Rankings.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
1import React, { useEffect } from "react";
2import { Helmet } from "react-helmet";
3
4import RankingEntry from "@components/RankingEntry";
5import {
6 Ranking,
7 SteamRanking,
8 RankingType,
9 SteamRankingType,
10} from "@customTypes/Ranking";
11import { API } from "@api/Api";
12
13import "@css/Rankings.css";
14
15enum LeaderboardTypes {
16 official,
17 unofficial,
18}
19
20enum RankingCategories {
21 rankings_overall,
22 rankings_multiplayer,
23 rankings_singleplayer,
24}
25
26const Rankings: React.FC = () => {
27 const [leaderboardData, setLeaderboardData] = React.useState<
28 Ranking | SteamRanking
29 >();
30 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<
31 RankingType[] | SteamRankingType[]
32 >();
33 const [currentRankingType, setCurrentRankingType] =
34 React.useState<LeaderboardTypes>(LeaderboardTypes.official);
35
36 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false);
37
38 const [currentLeaderboardType, setCurrentLeaderboardType] =
39 React.useState<RankingCategories>(RankingCategories.rankings_singleplayer);
40 const [load, setLoad] = React.useState<boolean>(false);
41
42 const _fetch_rankings = React.useCallback(async () => {
43 setLeaderboardLoad(false);
44 const rankings = await API.get_official_rankings();
45 setLeaderboardData(rankings);
46 if (currentLeaderboardType === RankingCategories.rankings_singleplayer) {
47 setCurrentLeaderboard(rankings.rankings_singleplayer);
48 } else if (
49 currentLeaderboardType === RankingCategories.rankings_multiplayer
50 ) {
51 setCurrentLeaderboard(rankings.rankings_multiplayer);
52 } else {
53 setCurrentLeaderboard(rankings.rankings_overall);
54 }
55 setLoad(true);
56 setLeaderboardLoad(true);
57 }, [currentLeaderboardType]);
58
59 const __dev_fetch_unofficial_rankings = async () => {
60 try {
61 setLeaderboardLoad(false);
62 const rankings = await API.get_unofficial_rankings();
63 setLeaderboardData(rankings);
64 if (currentLeaderboardType === RankingCategories.rankings_singleplayer) {
65 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
66 setCurrentLeaderboard(rankings.rankings_singleplayer);
67 } else if (
68 currentLeaderboardType === RankingCategories.rankings_multiplayer
69 ) {
70 setCurrentLeaderboard(rankings.rankings_multiplayer);
71 } else {
72 setCurrentLeaderboard(rankings.rankings_overall);
73 }
74 setLeaderboardLoad(true);
75 } catch (e) {
76 console.log(e);
77 }
78 };
79
80 const _set_current_leaderboard = React.useCallback(
81 (ranking_cat: RankingCategories) => {
82 if (ranking_cat === RankingCategories.rankings_singleplayer) {
83 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
84 } else if (ranking_cat === RankingCategories.rankings_multiplayer) {
85 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
86 } else {
87 setCurrentLeaderboard(leaderboardData!.rankings_overall);
88 }
89
90 setCurrentLeaderboardType(ranking_cat);
91 },
92 [leaderboardData]
93 );
94
95 // unused func
96 // const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => {
97 // if (leaderboard_type === LeaderboardTypes.official) {
98 // _fetch_rankings();
99 // } else {
100 // }
101 // };
102
103 useEffect(() => {
104 _fetch_rankings();
105 }, [_fetch_rankings]);
106
107 return (
108 <main className="*:text-foreground">
109 <Helmet>
110 <title>LPHUB | Rankings</title>
111 </Helmet>
112 <section className="nav-container nav-1">
113 <div>
114 <button
115 onClick={() => {
116 _fetch_rankings();
117 setCurrentRankingType(LeaderboardTypes.official);
118 }}
119 className={`nav-1-btn ${currentRankingType === LeaderboardTypes.official ? "selected" : ""}`}
120 >
121 <span>Official (LPHUB)</span>
122 </button>
123 <button
124 onClick={() => {
125 __dev_fetch_unofficial_rankings();
126 setCurrentRankingType(LeaderboardTypes.unofficial);
127 }}
128 className={`nav-1-btn ${currentRankingType === LeaderboardTypes.unofficial ? "selected" : ""}`}
129 >
130 <span>Unofficial (Steam)</span>
131 </button>
132 </div>
133 </section>
134 <section className="nav-container nav-2">
135 <div>
136 <button
137 onClick={() =>
138 _set_current_leaderboard(RankingCategories.rankings_singleplayer)
139 }
140 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_singleplayer ? "selected" : ""}`}
141 >
142 <span>Singleplayer</span>
143 </button>
144 <button
145 onClick={() =>
146 _set_current_leaderboard(RankingCategories.rankings_multiplayer)
147 }
148 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_multiplayer ? "selected" : ""}`}
149 >
150 <span>Cooperative</span>
151 </button>
152 <button
153 onClick={() =>
154 _set_current_leaderboard(RankingCategories.rankings_overall)
155 }
156 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_overall ? "selected" : ""}`}
157 >
158 <span>Overall</span>
159 </button>
160 </div>
161 </section>
162
163 {load ? (
164 <section className="rankings-leaderboard">
165 <div className="ranks-container">
166 <div className="leaderboard-entry header">
167 <span>Rank</span>
168 <span>Player</span>
169 <span>Portals</span>
170 </div>
171
172 <div className="splitter"></div>
173
174 {leaderboardLoad &&
175 currentLeaderboard?.map((curRankingData, i) => {
176 return (
177 <RankingEntry
178 currentLeaderboardType={currentLeaderboardType}
179 curRankingData={curRankingData}
180 key={i}
181 ></RankingEntry>
182 );
183 })}
184
185 {leaderboardLoad ? null : (
186 <div
187 style={{
188 display: "flex",
189 justifyContent: "center",
190 margin: "30px 0px",
191 }}
192 >
193 <span className="loader"></span>
194 </div>
195 )}
196 </div>
197 </section>
198 ) : null}
199 </main>
200 );
201};
202
203export default Rankings;
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
deleted file mode 100644
index 9c7885c..0000000
--- a/frontend/src/pages/Rules.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
1import React from "react";
2import ReactMarkdown from "react-markdown";
3import { Helmet } from "react-helmet";
4
5const Rules: React.FC = () => {
6 const [rulesText, setRulesText] = React.useState<string>("");
7
8 React.useEffect(() => {
9 const fetchRules = async () => {
10 try {
11 const response = await fetch(
12 "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md"
13 );
14 if (!response.ok) {
15 throw new Error("Failed to fetch README");
16 }
17 const rulesText = await response.text();
18 setRulesText(rulesText);
19 } catch (error) {
20 console.error("Error fetching Rules:", error);
21 }
22 // setRulesText(rulesText)
23 };
24 fetchRules();
25 }, []);
26
27 return (
28 <main className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
29 <Helmet>
30 <title>LPHUB | Rules</title>
31 </Helmet>
32 <ReactMarkdown>{rulesText}</ReactMarkdown>
33 </main>
34 );
35};
36
37export default Rules;
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
deleted file mode 100644
index 8c699b1..0000000
--- a/frontend/src/pages/User.tsx
+++ /dev/null
@@ -1,410 +0,0 @@
1import React from "react";
2import { Link, useLocation, useNavigate } from "react-router-dom";
3import { Helmet } from "react-helmet";
4
5import {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16} from "@images/Images";
17import { UserProfile } from "@customTypes/Profile";
18import { Game, GameChapters } from "@customTypes/Game";
19import { Map } from "@customTypes/Map";
20import { API } from "@api/Api";
21import { ticks_to_time } from "@utils/Time";
22import useMessage from "@hooks/UseMessage";
23
24interface UserProps {
25 profile?: UserProfile;
26 token?: string;
27 gameData: Game[];
28}
29
30const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
31 const { message, MessageDialogComponent } = useMessage();
32
33 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
34
35 const [navState, setNavState] = React.useState(0);
36 const [pageNumber, setPageNumber] = React.useState(1);
37 const [pageMax, setPageMax] = React.useState(0);
38
39 const [game, setGame] = React.useState("0");
40 const [chapter, setChapter] = React.useState("0");
41 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
42 null
43 );
44 const [maps, setMaps] = React.useState<Map[]>([]);
45
46 const location = useLocation();
47 const navigate = useNavigate();
48
49 const _fetch_user = React.useCallback(async () => {
50 const userID = location.pathname.split("/")[2];
51 if (token && profile && profile.profile && profile.steam_id === userID) {
52 navigate("/profile");
53 return;
54 }
55 const userData = await API.get_user(userID);
56 setUser(userData);
57 }, [location.pathname, token, profile, navigate]);
58
59 const _get_game_chapters = React.useCallback(async () => {
60 if (game !== "0") {
61 const gameChapters = await API.get_games_chapters(game);
62 setChapterData(gameChapters);
63 } else {
64 setPageMax(Math.ceil(user!.records.length / 20));
65 setPageNumber(1);
66 }
67 }, [game, user]);
68
69 const _get_game_maps = React.useCallback(async () => {
70 if (chapter === "0") {
71 const gameMaps = await API.get_game_maps(game);
72 setMaps(gameMaps);
73 setPageMax(Math.ceil(gameMaps.length / 20));
74 setPageNumber(1);
75 } else {
76 const gameChapters = await API.get_chapters(chapter);
77 setMaps(gameChapters.maps);
78 setPageMax(Math.ceil(gameChapters.maps.length / 20));
79 setPageNumber(1);
80 }
81 }, [chapter, game]);
82
83 React.useEffect(() => {
84 _fetch_user();
85 }, [location, _fetch_user]);
86
87 React.useEffect(() => {
88 if (user) {
89 _get_game_chapters();
90 }
91 }, [user, game, location, _get_game_chapters]);
92
93 React.useEffect(() => {
94 if (user && game !== "0") {
95 _get_game_maps();
96 }
97 }, [user, game, chapter, location, _get_game_maps]);
98
99 if (!user) {
100 return (
101 <div className="flex justify-center items-center h-[50vh] text-lg text-foreground">
102 Loading...
103 </div>
104 );
105 }
106
107 return (
108 <main className="ml-20 overflow-auto overflow-x-hidden relative w-[calc(100%px)] h-screen font-[--font-barlow-semicondensed-regular] text-foreground text-xl">
109 <Helmet>
110 <title>LPHUB | {user.user_name}</title>
111 <meta name="description" content={user.user_name} />
112 </Helmet>
113
114 {MessageDialogComponent}
115
116 <section className="m-5 bg-gradient-to-t from-[#202232] from-50% to-[#2b2e46] to-50% rounded-3xl p-[30px] mb-[30px] text-foreground">
117 <div className="grid grid-cols-[200px_1fr_auto] items-center gap-[25px] mb-[25px]">
118 <img
119 src={user.avatar_link}
120 alt="Profile"
121 className="w-[120px] h-[120px] rounded-full border-[3px] border-[rgba(205,207,223,0.2)]"
122 />
123 <div>
124 <h1 className="m-0 mb-[10px] text-[50px] font-bold text-white font-[--font-barlow-semicondensed-regular]">
125 {user.user_name}
126 </h1>
127 {user.country_code !== "XX" && (
128 <div className="flex items-center gap-3 mb-[15px]">
129 <img
130 src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`}
131 alt={user.country_code}
132 className="w-6 h-4 rounded-[10px]"
133 />
134 <span>{user.country_code}</span>
135 </div>
136 )}
137 <div className="flex flex-wrap gap-2">
138 {user.titles.map((title, index) => (
139 <span
140 key={index}
141 className="py-[6px] px-5 pt-[6px] rounded-[10px] text-lg font-normal text-white"
142 style={{ backgroundColor: `#${title.color}` }}
143 >
144 {title.name}
145 </span>
146 ))}
147 </div>
148 </div>
149 <div className="flex gap-[15px] items-center pr-[10px]">
150 {user.links.steam !== "-" && (
151 <a href={user.links.steam} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
152 <img src={SteamIcon} alt="Steam" className="h-[50px] px-[5px] scale-90 brightness-200" />
153 </a>
154 )}
155 {user.links.twitch !== "-" && (
156 <a href={user.links.twitch} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
157 <img src={TwitchIcon} alt="Twitch" className="h-[50px] px-[5px] scale-90 brightness-200" />
158 </a>
159 )}
160 {user.links.youtube !== "-" && (
161 <a href={user.links.youtube} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
162 <img src={YouTubeIcon} alt="YouTube" className="h-[50px] px-[5px] scale-90 brightness-200" />
163 </a>
164 )}
165 {user.links.p2sr !== "-" && (
166 <a href={user.links.p2sr} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
167 <img src={PortalIcon} alt="P2SR" className="h-[50px] px-[5px] scale-90 brightness-200" />
168 </a>
169 )}
170 </div>
171 </div>
172
173 <div className="grid grid-cols-3 gap-3 mt-24">
174 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
175 <div className="text-inherit text-lg">Overall</div>
176 <div className="text-white text-[40px]">
177 {user.rankings.overall.rank === 0 ? "N/A" : `#${user.rankings.overall.rank}`}
178 </div>
179 <div className="text-white text-xl">
180 {user.rankings.overall.completion_count}/{user.rankings.overall.completion_total}
181 </div>
182 </div>
183 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
184 <div className="text-inherit text-lg">Singleplayer</div>
185 <div className="text-white text-[40px]">
186 {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`}
187 </div>
188 <div className="text-white text-xl">
189 {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total}
190 </div>
191 </div>
192 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
193 <div className="text-inherit text-lg">Cooperative</div>
194 <div className="text-white text-[40px]">
195 {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`}
196 </div>
197 <div className="text-white text-xl">
198 {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total}
199 </div>
200 </div>
201 </div>
202 </section>
203
204 <section className="m-5 h-[60px] grid grid-cols-2">
205 <button
206 className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-l-3xl hover:bg-[#202232] ${
207 navState === 0 ? 'bg-[#202232]' : ''
208 }`}
209 onClick={() => setNavState(0)}
210 >
211 <img src={FlagIcon} alt="" className="w-5 h-5 scale-[1.2]" />
212 Player Records
213 </button>
214 <button
215 className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-r-3xl hover:bg-[#202232] ${
216 navState === 1 ? 'bg-[#202232]' : ''
217 }`}
218 onClick={() => setNavState(1)}
219 >
220 <img src={StatisticsIcon} alt="" className="w-5 h-5 scale-[1.2]" />
221 Statistics
222 </button>
223 </section>
224
225 {navState === 0 && (
226 <section className="m-5 block bg-[#202232] rounded-3xl overflow-hidden">
227 <div className="grid grid-cols-2 mx-5 my-5 mt-[10px] mb-5">
228 <select
229 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px]"
230 value={game}
231 onChange={(e) => {
232 setGame(e.target.value);
233 setChapter("0");
234 }}
235 >
236 <option value="0">All Games</option>
237 {gameData?.map((g) => (
238 <option key={g.id} value={g.id}>
239 {g.name}
240 </option>
241 ))}
242 </select>
243
244 <select
245 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px] disabled:opacity-50"
246 value={chapter}
247 onChange={(e) => setChapter(e.target.value)}
248 disabled={game === "0"}
249 >
250 <option value="0">All Chapters</option>
251 {chapterData?.chapters
252 .filter(c => !c.is_disabled)
253 .map((c) => (
254 <option key={c.id} value={c.id}>
255 {c.name}
256 </option>
257 ))}
258 </select>
259 </div>
260
261 <div className="h-[34px] grid text-xl pl-[60px] mx-5 my-0 grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%]">
262 <div className="flex place-items-end cursor-pointer">
263 <span>Map Name</span>
264 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
265 </div>
266 <div className="flex place-items-end cursor-pointer">
267 <span>Portals</span>
268 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
269 </div>
270 <div className="flex place-items-end cursor-pointer">
271 <span>WRΔ</span>
272 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
273 </div>
274 <div className="flex place-items-end cursor-pointer">
275 <span>Time</span>
276 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
277 </div>
278 <div></div>
279 <div className="flex place-items-end cursor-pointer">
280 <span>Rank</span>
281 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
282 </div>
283 <div className="flex place-items-end cursor-pointer">
284 <span>Date</span>
285 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
286 </div>
287 <div className="flex items-center gap-[10px] justify-center">
288 <button
289 className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed"
290 onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
291 disabled={pageNumber === 1}
292 >
293
294 </button>
295 <span className="text-sm text-foreground">{pageNumber}/{pageMax}</span>
296 <button
297 className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed"
298 onClick={() => setPageNumber(Math.min(pageMax, pageNumber + 1))}
299 disabled={pageNumber === pageMax}
300 >
301
302 </button>
303 </div>
304 </div>
305
306 <div>
307 {game === "0" ? (
308 user.records
309 .sort((a, b) => a.map_id - b.map_id)
310 .map((record, index) =>
311 Math.ceil((index + 1) / 20) === pageNumber ? (
312 <div key={index} className="w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232]">
313 <Link to={`/maps/${record.map_id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
314 {record.map_name}
315 </Link>
316 <span className="flex place-items-center h-11">{record.scores[0]?.score_count || 'N/A'}</span>
317 <span className={`flex place-items-center h-11 ${record.scores[0]?.score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}>
318 {record.scores[0]?.score_count - record.map_wr_count > 0
319 ? `+${record.scores[0].score_count - record.map_wr_count}`
320 : '–'}
321 </span>
322 <span className="flex place-items-center h-11">{record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
323 <span className="flex place-items-center h-11"></span>
324 <span className="flex place-items-center h-11 font-semibold">#{record.placement}</span>
325 <span className="flex place-items-center h-11">{record.scores[0]?.date.split("T")[0] || 'N/A'}</span>
326 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
327 <button
328 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
329 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0]?.demo_id}`)}
330 title="Demo Info"
331 >
332 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
333 </button>
334 <button
335 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
336 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0]?.demo_id}`}
337 title="Download Demo"
338 >
339 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
340 </button>
341 {record.scores.length > 1 && (
342 <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History">
343 <img src={HistoryIcon} alt="History" className="w-4 h-4" />
344 </button>
345 )}
346 </div>
347 </div>
348 ) : null
349 )
350 ) : (
351 maps
352 ?.filter(map => !map.is_disabled)
353 .sort((a, b) => a.id - b.id)
354 .map((map, index) => {
355 if (Math.ceil((index + 1) / 20) !== pageNumber) return null;
356
357 const record = user.records.find(r => r.map_id === map.id);
358
359 return (
360 <div key={index} className={`w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232] ${!record ? 'opacity-65' : ''}`}>
361 <Link to={`/maps/${map.id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
362 {map.name}
363 </Link>
364 <span className="flex place-items-center h-11">{record?.scores[0]?.score_count || 'N/A'}</span>
365 <span className={`flex place-items-center h-11 ${record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}>
366 {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0
367 ? `+${record.scores[0].score_count - record.map_wr_count}`
368 : '–'}
369 </span>
370 <span className="flex place-items-center h-11">{record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
371 <span className="flex place-items-center h-11"></span>
372 <span className="flex place-items-center h-11 font-semibold">{record ? `#${record.placement}` : 'N/A'}</span>
373 <span className="flex place-items-center h-11">{record?.scores[0]?.date.split("T")[0] || 'N/A'}</span>
374 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
375 {record?.scores[0] && (
376 <>
377 <button
378 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
379 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0].demo_id}`)}
380 title="Demo Info"
381 >
382 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
383 </button>
384 <button
385 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
386 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0].demo_id}`}
387 title="Download Demo"
388 >
389 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
390 </button>
391 {record.scores.length > 1 && (
392 <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History">
393 <img src={HistoryIcon} alt="History" className="w-4 h-4" />
394 </button>
395 )}
396 </>
397 )}
398 </div>
399 </div>
400 );
401 })
402 )}
403 </div>
404 </section>
405 )}
406 </main>
407 );
408};
409
410export default User;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index a78cf3a..93af3f1 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -4,9 +4,10 @@
4 "lib": ["ES2020", "DOM", "DOM.Iterable"], 4 "lib": ["ES2020", "DOM", "DOM.Iterable"],
5 "module": "ESNext", 5 "module": "ESNext",
6 "skipLibCheck": true, 6 "skipLibCheck": true,
7 "moduleResolution": "bundler", 7 "moduleResolution": "node",
8 "allowImportingTsExtensions": true, 8 "allowImportingTsExtensions": true,
9 "resolveJsonModule": true, 9 "resolveJsonModule": true,
10 "allowSyntheticDefaultImports": true,
10 "isolatedModules": true, 11 "isolatedModules": true,
11 "noEmit": true, 12 "noEmit": true,
12 "jsx": "react-jsx", 13 "jsx": "react-jsx",