aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/pages/User/User.tsx
diff options
context:
space:
mode:
authorWolfboy248 <georgejvindkarlsen@gmail.com>2025-08-19 13:23:17 +0200
committerWolfboy248 <georgejvindkarlsen@gmail.com>2025-08-19 13:23:17 +0200
commitc04bc9a36ebfcdf6d8e2db8a6cdeb44062b66bec (patch)
tree42dfa70f41f701b561455aac01b45ec72f816184 /frontend/src/pages/User/User.tsx
parentfix/frontend: smol syntax fix (diff)
downloadlphub-c04bc9a36ebfcdf6d8e2db8a6cdeb44062b66bec.tar.gz
lphub-c04bc9a36ebfcdf6d8e2db8a6cdeb44062b66bec.tar.bz2
lphub-c04bc9a36ebfcdf6d8e2db8a6cdeb44062b66bec.zip
organised pages, started work on theme
Diffstat (limited to '')
-rw-r--r--frontend/src/pages/User/User.tsx410
1 files changed, 410 insertions, 0 deletions
diff --git a/frontend/src/pages/User/User.tsx b/frontend/src/pages/User/User.tsx
new file mode 100644
index 0000000..30c9e45
--- /dev/null
+++ b/frontend/src/pages/User/User.tsx
@@ -0,0 +1,410 @@
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.ts";
18import { Game, GameChapters } from "@customTypes/Game.ts";
19import { Map } from "@customTypes/Map.ts";
20import { API } from "@api/Api.ts";
21import { ticks_to_time } from "@utils/Time.ts";
22import useMessage from "@hooks/UseMessage.tsx";
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;