aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/About.tsx62
-rw-r--r--frontend/src/pages/Games.tsx55
-rw-r--r--frontend/src/pages/Homepage.tsx43
-rw-r--r--frontend/src/pages/Maplist.tsx234
-rw-r--r--frontend/src/pages/Maps.tsx166
-rw-r--r--frontend/src/pages/Profile.tsx672
-rw-r--r--frontend/src/pages/Rankings.tsx320
-rw-r--r--frontend/src/pages/Rules.tsx64
-rw-r--r--frontend/src/pages/User.tsx525
9 files changed, 1344 insertions, 797 deletions
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index a8b7826..7802d75 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -1,40 +1,36 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4
5import '@css/About.css';
6 4
7const About: React.FC = () => { 5const About: React.FC = () => {
6 const [aboutText, setAboutText] = React.useState<string>("");
8 7
9 const [aboutText, setAboutText] = React.useState<string>(""); 8 React.useEffect(() => {
10 9 const fetchReadme = async () => {
11 React.useEffect(() => { 10 try {
12 const fetchReadme = async () => { 11 const response = await fetch(
13 try { 12 "https://raw.githubusercontent.com/pektezol/lphub/main/README.md"
14 const response = await fetch( 13 );
15 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' 14 if (!response.ok) {
16 ); 15 throw new Error("Failed to fetch README");
17 if (!response.ok) { 16 }
18 throw new Error('Failed to fetch README'); 17 const readmeText = await response.text();
19 } 18 setAboutText(readmeText);
20 const readmeText = await response.text(); 19 } catch (error) {
21 setAboutText(readmeText); 20 console.error("Error fetching README:", error);
22 } catch (error) { 21 }
23 console.error('Error fetching README:', error); 22 };
24 } 23 fetchReadme();
25 }; 24 }, []);
26 fetchReadme();
27 }, []);
28
29 25
30 return ( 26 return (
31 <div id="about"> 27 <div className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
32 <Helmet> 28 <Helmet>
33 <title>LPHUB | About</title> 29 <title>LPHUB | About</title>
34 </Helmet> 30 </Helmet>
35 <ReactMarkdown>{aboutText}</ReactMarkdown> 31 <ReactMarkdown className={"overflow-auto"}>{aboutText}</ReactMarkdown>
36 </div> 32 </div>
37 ); 33 );
38}; 34};
39 35
40export default About; 36export default About;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index 15cc891..8587635 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -1,46 +1,29 @@
1import React from 'react'; 1import React from "react";
2import { Helmet } from 'react-helmet'; 2import { Helmet } from "react-helmet";
3 3
4import GameEntry from '@components/GameEntry'; 4import GameEntry from "@components/GameEntry";
5import { Game } from '@customTypes/Game'; 5import { Game } from "@customTypes/Game";
6import "@css/Maps.css"
7 6
8interface GamesProps { 7interface GamesProps {
9 games: Game[]; 8 games: Game[];
10} 9}
11 10
12const Games: React.FC<GamesProps> = ({ games }) => { 11const Games: React.FC<GamesProps> = ({ games }) => {
13 12 return (
14 const _page_load = () => { 13 <div className="ml-20 min-h-screen text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin">
15 const loaders = document.querySelectorAll(".loader"); 14 <Helmet>
16 loaders.forEach((loader) => { 15 <title>LPHUB | Games</title>
17 (loader as HTMLElement).style.display = "none"; 16 </Helmet>
18 }); 17 <section className="py-12 px-12 w-full">
19 } 18 <h1 className="text-3xl font-bold mb-8">Games</h1>
20 19 <div className="flex flex-col w-full">
21 React.useEffect(() => { 20 {games.map((game, index) => (
22 document.querySelectorAll(".games-page-item-body").forEach((game, index) => { 21 <GameEntry game={game} key={index} />
23 game.innerHTML = ""; 22 ))}
24 });
25 _page_load();
26 }, []);
27
28 return (
29 <div className='games-page'>
30 <Helmet>
31 <title>LPHUB | Games</title>
32 </Helmet>
33 <section>
34 <div className='games-page-content'>
35 <div className='games-page-item-content'>
36 {games.map((game, index) => (
37 <GameEntry game={game} key={index} />
38 ))}
39 </div>
40 </div>
41 </section>
42 </div> 23 </div>
43 ); 24 </section>
25 </div>
26 );
44}; 27};
45 28
46export default Games; 29export default Games;
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 4f46af5..b4ac3b0 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -1,22 +1,31 @@
1import React from 'react'; 1import React from "react";
2import { Helmet } from 'react-helmet'; 2import { Helmet } from "react-helmet";
3 3
4const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
5 5 return (
6 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 <main> 7 <Helmet>
8 <Helmet> 8 <title>LPHUB | Homepage</title>
9 <title>LPHUB | Homepage</title> 9 </Helmet>
10 </Helmet> 10 <section className="p-8">
11 <section> 11 <p />
12 <p /> 12 <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1>
13 <h1>Welcome to Least Portals Hub!</h1> 13 <p className="text-lg mb-4 leading-relaxed">
14 <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> 14 At the moment, LPHUB is in beta state. This means that the site has
15 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> 15 only the core functionalities enabled for providing both collaborative
16 <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> 16 information and competitive leaderboards.
17 </section> 17 </p>
18 </main> 18 <p className="text-lg mb-4 leading-relaxed">
19 ); 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 );
20}; 29};
21 30
22export default Homepage; 31export default Homepage;
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index 76f9a52..2f0491e 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -2,7 +2,6 @@ import React, { useEffect } from "react";
2import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; 2import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import "@css/Maplist.css";
6import { API } from "@api/Api"; 5import { API } from "@api/Api";
7import { Game } from "@customTypes/Game"; 6import { Game } from "@customTypes/Game";
8import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 7import { GameChapter, GamesChapters } from "@customTypes/Chapters";
@@ -11,7 +10,6 @@ const Maplist: React.FC = () => {
11 const [game, setGame] = React.useState<Game | null>(null); 10 const [game, setGame] = React.useState<Game | null>(null);
12 const [catNum, setCatNum] = React.useState(0); 11 const [catNum, setCatNum] = React.useState(0);
13 const [id, setId] = React.useState(0); 12 const [id, setId] = React.useState(0);
14 const [category, setCategory] = React.useState(0);
15 const [load, setLoad] = React.useState(false); 13 const [load, setLoad] = React.useState(false);
16 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0); 14 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0);
17 const [hasClicked, setHasClicked] = React.useState(false); 15 const [hasClicked, setHasClicked] = React.useState(false);
@@ -21,7 +19,7 @@ const Maplist: React.FC = () => {
21 19
22 const [dropdownActive, setDropdownActive] = React.useState("none"); 20 const [dropdownActive, setDropdownActive] = React.useState("none");
23 21
24 const params = useParams<{ id: string, chapter: string }>(); 22 const params = useParams<{ id: string; chapter: string }>();
25 const location = useLocation(); 23 const location = useLocation();
26 const navigate = useNavigate(); 24 const navigate = useNavigate();
27 25
@@ -34,15 +32,15 @@ const Maplist: React.FC = () => {
34 const _fetch_chapters = async (chapter_id: string) => { 32 const _fetch_chapters = async (chapter_id: string) => {
35 const chapters = await API.get_chapters(chapter_id); 33 const chapters = await API.get_chapters(chapter_id);
36 setCurChapter(chapters); 34 setCurChapter(chapters);
37 } 35 };
38 36
39 const _handle_dropdown_click = () => { 37 const _handle_dropdown_click = () => {
40 if (dropdownActive == "none") { 38 if (dropdownActive === "none") {
41 setDropdownActive("block"); 39 setDropdownActive("block");
42 } else { 40 } else {
43 setDropdownActive("none"); 41 setDropdownActive("none");
44 } 42 }
45 } 43 };
46 44
47 // im sorry but im too lazy to fix this right now 45 // im sorry but im too lazy to fix this right now
48 useEffect(() => { 46 useEffect(() => {
@@ -54,7 +52,7 @@ const Maplist: React.FC = () => {
54 const queryParams = new URLSearchParams(location.search); 52 const queryParams = new URLSearchParams(location.search);
55 if (queryParams.get("chapter")) { 53 if (queryParams.get("chapter")) {
56 let cat = parseFloat(queryParams.get("chapter") || ""); 54 let cat = parseFloat(queryParams.get("chapter") || "");
57 if (gameId == 2) { 55 if (gameId === 2) {
58 cat += 10; 56 cat += 10;
59 } 57 }
60 _fetch_chapters(cat.toString()); 58 _fetch_chapters(cat.toString());
@@ -62,7 +60,7 @@ const Maplist: React.FC = () => {
62 60
63 const _fetch_game = async () => { 61 const _fetch_game = async () => {
64 const games = await API.get_games(); 62 const games = await API.get_games();
65 const foundGame = games.find((game) => game.id === gameId); 63 const foundGame = games.find(game => game.id === gameId);
66 // console.log(foundGame) 64 // console.log(foundGame)
67 if (foundGame) { 65 if (foundGame) {
68 setGame(foundGame); 66 setGame(foundGame);
@@ -74,111 +72,175 @@ const Maplist: React.FC = () => {
74 const games_chapters = await API.get_games_chapters(gameId.toString()); 72 const games_chapters = await API.get_games_chapters(gameId.toString());
75 setGameChapters(games_chapters); 73 setGameChapters(games_chapters);
76 setNumChapters(games_chapters.chapters.length); 74 setNumChapters(games_chapters.chapters.length);
77 } 75 };
78 76
79 setLoad(true); 77 setLoad(true);
80 _fetch_game(); 78 _fetch_game();
81 _fetch_game_chapters(); 79 _fetch_game_chapters();
82 }, []); 80 }, [location.search]);
83 81
84 useEffect(() => { 82 useEffect(() => {
85 const queryParams = new URLSearchParams(location.search); 83 const queryParams = new URLSearchParams(location.search);
86 if (gameChapters != undefined && !queryParams.get("chapter")) { 84 if (gameChapters !== undefined && !queryParams.get("chapter")) {
87 _fetch_chapters(gameChapters!.chapters[0].id.toString()); 85 _fetch_chapters(gameChapters!.chapters[0].id.toString());
88 } 86 }
89 }, [gameChapters]) 87 }, [gameChapters, location.search]);
90
91
92 88
93 return ( 89 return (
94 <main> 90 <main className="*:text-foreground w-[calc(100vw-80px)] relative left-0 ml-20 min-h-screen p-4 sm:p-8">
95 <Helmet> 91 <Helmet>
96 <title>LPHUB | Maplist</title> 92 <title>LPHUB | Maplist</title>
97 </Helmet> 93 </Helmet>
98 <section style={{ marginTop: "20px" }}> 94
99 <Link to="/games"> 95 <section className="mt-5">
100 <button className="nav-button" style={{ borderRadius: "20px" }}> 96 <Link to="/games">
101 <i className="triangle"></i> 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">
102 <span>Games List</span> 98 <i className="triangle mr-2"></i>
103 </button> 99 <span className="px-2">Games List</span>
104 </Link> 100 </button>
101 </Link>
105 </section> 102 </section>
103
106 {load ? ( 104 {load ? (
107 <div></div> 105 <div></div>
108 ) : ( 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>
109 <section> 153 <section>
110 <h1>{game?.name}</h1> 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 \
111 <div 169 <div
112 style={{ backgroundImage: `url(${game?.image})` }} 170 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${
113 className="game-header" 171 dropdownActive === "none" ? "hidden" : "block"
172 }`}
114 > 173 >
115 <div className="blur"> 174 {gameChapters?.chapters.map((chapter, i) => {
116 <div className="game-header-portal-count"> 175 return (
117 <h2 className="portal-count"> 176 <div
118 { 177 key={i}
119 game?.category_portals.find( 178 className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground"
120 (obj) => obj.category.id === catNum + 1 179 onClick={() => {
121 )?.portal_count 180 _fetch_chapters(chapter.id.toString());
122 } 181 _handle_dropdown_click();
123 </h2> 182 }}
124 <h3>portals</h3> 183 >
125 </div> 184 {chapter.name}
126 <div className="game-header-categories">
127 {game?.category_portals.map((cat, index) => (
128 <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => { setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id) }}>
129 <span>{cat.category.name}</span>
130 </button>
131 ))}
132 </div>
133 </div> 185 </div>
186 );
187 })}
134 </div> 188 </div>
189 </section>
135 190
136 <div> 191 <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5">
137 <section className="chapter-select-container"> 192 {curChapter?.maps.map((map, i) => {
138 <div> 193 return (
139 <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> 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>
140 </div> 214 </div>
141 <div onClick={_handle_dropdown_click} className="dropdown">
142 <span>{curChapter?.chapter.name.split(" - ")[1]}</span>
143 <i className="triangle"></i>
144 </div> 215 </div>
145 <div className="dropdown-elements" style={{ display: dropdownActive }}>
146 {gameChapters?.chapters.map((chapter, i) => {
147 return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div>
148 })
149 216
150 } 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 ))}
151 </div> 235 </div>
152 </section> 236 </div>
153 <section className="maplist"> 237 </Link>
154 {curChapter?.maps.map((map, i) => { 238 </div>
155 return <div className="maplist-entry"> 239 );
156 <Link to={`/maps/${map.id}`}> 240 })}
157 <span>{map.name}</span>
158 <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}>
159 <div className="blur map">
160 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find(
161 (obj) => obj.category.id === catNum + 1
162 )?.portal_count}</span>
163 <span>portals</span>
164 </div>
165 </div>
166 <div className="difficulty-bar">
167 {/* <span>Difficulty:</span> */}
168 <div className={map.difficulty <= 2 ? "one" : map.difficulty <= 4 ? "two" : map.difficulty <= 6 ? "three" : map.difficulty <= 8 ? "four" : map.difficulty <= 10 ? "five" : "one"}>
169 <div className="difficulty-point"></div>
170 <div className="difficulty-point"></div>
171 <div className="difficulty-point"></div>
172 <div className="difficulty-point"></div>
173 <div className="difficulty-point"></div>
174 </div>
175 </div>
176 </Link>
177 </div>
178 })}
179 </section>
180 </div>
181 </section> 241 </section>
242 </div>
243 </section>
182 )} 244 )}
183 </main> 245 </main>
184 ); 246 );
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index fb13563..50fe03b 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -1,28 +1,32 @@
1import React from 'react'; 1import React from "react";
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images'; 5import { PortalIcon, FlagIcon, ChatIcon } from "../images/Images";
6import Summary from '@components/Summary'; 6import Summary from "@components/Summary";
7import Leaderboards from '@components/Leaderboards'; 7import Leaderboards from "@components/Leaderboards";
8import Discussions from '@components/Discussions'; 8import Discussions from "@components/Discussions";
9import ModMenu from '@components/ModMenu'; 9import ModMenu from "@components/ModMenu";
10import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map'; 10import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map";
11import { API } from '@api/Api'; 11import { API } from "@api/Api";
12import "@css/Maps.css";
13 12
14interface MapProps { 13interface MapProps {
15 token?: string; 14 token?: string;
16 isModerator: boolean; 15 isModerator: boolean;
17}; 16}
18 17
19const Maps: React.FC<MapProps> = ({ token, isModerator }) => { 18const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
20
21 const [selectedRun, setSelectedRun] = React.useState<number>(0); 19 const [selectedRun, setSelectedRun] = React.useState<number>(0);
22 20
23 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); 21 const [mapSummaryData, setMapSummaryData] = React.useState<
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); 22 MapSummary | undefined
25 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(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);
26 30
27 const [navState, setNavState] = React.useState<number>(0); 31 const [navState, setNavState] = React.useState<number>(0);
28 32
@@ -30,45 +34,66 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
30 34
31 const mapID = location.pathname.split("/")[2]; 35 const mapID = location.pathname.split("/")[2];
32 36
33 const _fetch_map_summary = async () => { 37 const _fetch_map_summary = React.useCallback(async () => {
34 const mapSummary = await API.get_map_summary(mapID); 38 const mapSummary = await API.get_map_summary(mapID);
35 setMapSummaryData(mapSummary); 39 setMapSummaryData(mapSummary);
36 }; 40 }, [mapID]);
37 41
38 const _fetch_map_leaderboards = async () => { 42 const _fetch_map_leaderboards = React.useCallback(async () => {
39 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); 43 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1");
40 setMapLeaderboardData(mapLeaderboards); 44 setMapLeaderboardData(mapLeaderboards);
41 }; 45 }, [mapID]);
42 46
43 const _fetch_map_discussions = async () => { 47 const _fetch_map_discussions = React.useCallback(async () => {
44 const mapDiscussions = await API.get_map_discussions(mapID); 48 const mapDiscussions = await API.get_map_discussions(mapID);
45 setMapDiscussionsData(mapDiscussions); 49 setMapDiscussionsData(mapDiscussions);
46 }; 50 }, [mapID]);
47 51
48 React.useEffect(() => { 52 React.useEffect(() => {
49 _fetch_map_summary(); 53 _fetch_map_summary();
50 _fetch_map_leaderboards(); 54 _fetch_map_leaderboards();
51 _fetch_map_discussions(); 55 _fetch_map_discussions();
52 }, [mapID]); 56 }, [
57 mapID,
58 _fetch_map_discussions,
59 _fetch_map_leaderboards,
60 _fetch_map_summary,
61 ]);
53 62
54 if (!mapSummaryData) { 63 if (!mapSummaryData) {
55 // loading placeholder 64 // loading placeholder
56 return ( 65 return (
57 <> 66 <>
58 <main> 67 <main className="*:text-foreground relative left-0 w-[calc(100%-20rem)] min-h-screen p-4 sm:p-8">
59 <section id='section1' className='summary1'> 68 <section id="section1" className="summary1">
60 <div> 69 <div>
61 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 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>
62 </div> 78 </div>
63 </section> 79 </section>
64 80
65 <section id='section2' className='summary1'> 81 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
66 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> 82 <button className="nav-button">
67 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 83 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
68 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> 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>
69 </section> 94 </section>
70 95
71 <section id='section6' className='summary2' /> 96 <section id="section6" className="summary2 mt-4" />
72 </main> 97 </main>
73 </> 98 </>
74 ); 99 );
@@ -80,29 +105,78 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
80 <title>LPHUB | {mapSummaryData.map.map_name}</title> 105 <title>LPHUB | {mapSummaryData.map.map_name}</title>
81 <meta name="description" content={mapSummaryData.map.map_name} /> 106 <meta name="description" content={mapSummaryData.map.map_name} />
82 </Helmet> 107 </Helmet>
83 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 108 {isModerator && (
84 109 <ModMenu
85 <div id='background-image'> 110 token={token}
111 data={mapSummaryData}
112 selectedRun={selectedRun}
113 mapID={mapID}
114 />
115 )}
116
117 <div id="background-image">
86 <img src={mapSummaryData.map.image} alt="" /> 118 <img src={mapSummaryData.map.image} alt="" />
87 </div> 119 </div>
88 <main> 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">
89 <section id='section1' className='summary1'> 121 <section id="section1" className="summary1">
90 <div> 122 <div>
91 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 123 <Link to="/games">
92 <Link to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> 124 <button
93 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 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>
94 </div> 145 </div>
95 </section> 146 </section>
96 147
97 <section id='section2' className='summary1'> 148 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
98 <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> 149 <button className="nav-button" onClick={() => setNavState(0)}>
99 <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 150 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
100 <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> 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>
101 </section> 161 </section>
102 162
103 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 163 {navState === 0 && (
164 <Summary
165 selectedRun={selectedRun}
166 setSelectedRun={setSelectedRun}
167 data={mapSummaryData}
168 />
169 )}
104 {navState === 1 && <Leaderboards mapID={mapID} />} 170 {navState === 1 && <Leaderboards mapID={mapID} />}
105 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 171 {navState === 2 && (
172 <Discussions
173 data={mapDiscussionsData}
174 token={token}
175 isModerator={isModerator}
176 mapID={mapID}
177 onRefresh={() => _fetch_map_discussions()}
178 />
179 )}
106 </main> 180 </main>
107 </> 181 </>
108 ); 182 );
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 48233bf..f44f587 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,16 +1,27 @@
1import React from 'react'; 1import React from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; 5import {
6import { UserProfile } from '@customTypes/Profile'; 6 SteamIcon,
7import { Game, GameChapters } from '@customTypes/Game'; 7 TwitchIcon,
8import { Map } from '@customTypes/Map'; 8 YouTubeIcon,
9import { ticks_to_time } from '@utils/Time'; 9 PortalIcon,
10import "@css/Profile.css"; 10 FlagIcon,
11import { API } from '@api/Api'; 11 StatisticsIcon,
12import useConfirm from '@hooks/UseConfirm'; 12 SortIcon,
13import useMessage from '@hooks/UseMessage'; 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";
14import useMessageLoad from "@hooks/UseMessageLoad"; 25import useMessageLoad from "@hooks/UseMessageLoad";
15 26
16interface ProfileProps { 27interface ProfileProps {
@@ -20,17 +31,25 @@ interface ProfileProps {
20 onDeleteRecord: () => void; 31 onDeleteRecord: () => void;
21} 32}
22 33
23const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { 34const Profile: React.FC<ProfileProps> = ({
35 profile,
36 token,
37 gameData,
38 onDeleteRecord,
39}) => {
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 40 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { message, MessageDialogComponent } = useMessage(); 41 const { message, MessageDialogComponent } = useMessage();
26 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 42 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
43 useMessageLoad();
27 const [navState, setNavState] = React.useState(0); 44 const [navState, setNavState] = React.useState(0);
28 const [pageNumber, setPageNumber] = React.useState(1); 45 const [pageNumber, setPageNumber] = React.useState(1);
29 const [pageMax, setPageMax] = React.useState(0); 46 const [pageMax, setPageMax] = React.useState(0);
30 47
31 const [game, setGame] = React.useState("0") 48 const [game, setGame] = React.useState("0");
32 const [chapter, setChapter] = React.useState("0") 49 const [chapter, setChapter] = React.useState("0");
33 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 50 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
51 null
52 );
34 const [maps, setMaps] = React.useState<Map[]>([]); 53 const [maps, setMaps] = React.useState<Map[]>([]);
35 54
36 const navigate = useNavigate(); 55 const navigate = useNavigate();
@@ -41,7 +60,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
41 } 60 }
42 }; 61 };
43 62
44 const _get_game_chapters = async () => { 63 const _get_game_chapters = React.useCallback(async () => {
45 if (game && game !== "0") { 64 if (game && game !== "0") {
46 const gameChapters = await API.get_games_chapters(game); 65 const gameChapters = await API.get_games_chapters(game);
47 setChapterData(gameChapters); 66 setChapterData(gameChapters);
@@ -49,9 +68,9 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
49 setPageMax(Math.ceil(profile!.records.length / 20)); 68 setPageMax(Math.ceil(profile!.records.length / 20));
50 setPageNumber(1); 69 setPageNumber(1);
51 } 70 }
52 }; 71 }, [game, profile]);
53 72
54 const _get_game_maps = async () => { 73 const _get_game_maps = React.useCallback(async () => {
55 if (chapter === "0") { 74 if (chapter === "0") {
56 const gameMaps = await API.get_game_maps(game); 75 const gameMaps = await API.get_game_maps(game);
57 setMaps(gameMaps); 76 setMaps(gameMaps);
@@ -63,10 +82,13 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
63 setPageMax(Math.ceil(gameChapters.maps.length / 20)); 82 setPageMax(Math.ceil(gameChapters.maps.length / 20));
64 setPageNumber(1); 83 setPageNumber(1);
65 } 84 }
66 }; 85 }, [chapter, game]);
67 86
68 const _delete_submission = async (map_id: number, record_id: number) => { 87 const _delete_submission = async (map_id: number, record_id: number) => {
69 const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?"); 88 const userConfirmed = await confirm(
89 "Delete Record",
90 "Are you sure you want to delete this record?"
91 );
70 92
71 if (!userConfirmed) { 93 if (!userConfirmed) {
72 return; 94 return;
@@ -87,26 +109,24 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
87 React.useEffect(() => { 109 React.useEffect(() => {
88 if (!profile) { 110 if (!profile) {
89 navigate("/"); 111 navigate("/");
90 }; 112 }
91 }, [profile]); 113 }, [profile, navigate]);
92 114
93 React.useEffect(() => { 115 React.useEffect(() => {
94 if (profile) { 116 if (profile) {
95 _get_game_chapters(); 117 _get_game_chapters();
96 } 118 }
97 }, [profile, game]); 119 }, [profile, game, _get_game_chapters]);
98 120
99 React.useEffect(() => { 121 React.useEffect(() => {
100 if (profile && game !== "0") { 122 if (profile && game !== "0") {
101 _get_game_maps(); 123 _get_game_maps();
102 } 124 }
103 }, [profile, game, chapter, chapterData]) 125 }, [profile, game, chapter, chapterData, _get_game_maps]);
104 126
105 if (!profile) { 127 if (!profile) {
106 return ( 128 return <></>;
107 <></> 129 }
108 );
109 };
110 130
111 return ( 131 return (
112 <div> 132 <div>
@@ -119,230 +139,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
119 {ConfirmDialogComponent} 139 {ConfirmDialogComponent}
120 140
121 <main> 141 <main>
122 <section id='section1' className='profile'> 142 <section id="section1" className="profile">
123 143 {profile.profile ? (
124 {profile.profile 144 <div id="profile-image" onClick={_update_profile}>
125 ? ( 145 <img src={profile.avatar_link} alt="profile-image"></img>
126 <div id='profile-image' onClick={_update_profile}> 146 <span>Refresh</span>
127 <img src={profile.avatar_link} alt="profile-image"></img> 147 </div>
128 <span>Refresh</span> 148 ) : (
129 </div> 149 <div>
130 ) : ( 150 <img src={profile.avatar_link} alt="profile-image"></img>
131 <div> 151 </div>
132 <img src={profile.avatar_link} alt="profile-image"></img> 152 )}
133 </div>
134 )}
135 153
136 <div id='profile-top'> 154 <div id="profile-top">
137 <div> 155 <div>
138 <div>{profile.user_name}</div> 156 <div>{profile.user_name}</div>
139 <div> 157 <div>
140 {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} 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 )}
141 </div> 166 </div>
142 <div> 167 <div>
143 {profile.titles.map(e => ( 168 {profile.titles.map(e => (
144 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 169 <span
170 className="titles"
171 style={{ backgroundColor: `#${e.color}` }}
172 >
145 {e.name} 173 {e.name}
146 </span> 174 </span>
147 ))} 175 ))}
148 </div> 176 </div>
149 </div> 177 </div>
150 <div> 178 <div>
151 {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 179 {profile.links.steam === "-" ? (
152 {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 180 ""
153 {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 181 ) : (
154 {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 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 )}
155 </div> 207 </div>
156
157 </div> 208 </div>
158 <div id='profile-bottom'> 209 <div id="profile-bottom">
159 <div> 210 <div>
160 <span>Overall</span> 211 <span>Overall</span>
161 <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} 212 <span>
162 <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</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>
163 </span> 220 </span>
164 </div> 221 </div>
165 <div> 222 <div>
166 <span>Singleplayer</span> 223 <span>Singleplayer</span>
167 <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} 224 <span>
168 <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</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>
169 </span> 232 </span>
170 </div> 233 </div>
171 <div> 234 <div>
172 <span>Cooperative</span> 235 <span>Cooperative</span>
173 <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} 236 <span>
174 <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</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>
175 </span> 244 </span>
176 </div> 245 </div>
177 </div> 246 </div>
178 </section> 247 </section>
179 248
180 249 <section id="section2" className="profile">
181 <section id='section2' className='profile'> 250 <button onClick={() => setNavState(0)}>
182 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 251 <img src={FlagIcon} alt="" />
183 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 252 &nbsp;Player Records
253 </button>
254 <button onClick={() => setNavState(1)}>
255 <img src={StatisticsIcon} alt="" />
256 &nbsp;Statistics
257 </button>
184 </section> 258 </section>
185 259
186 260 <section id="section3" className="profile1">
187 261 <div id="profileboard-nav">
188 262 {gameData === null ? (
189 263 <select>error</select>
190 <section id='section3' className='profile1'> 264 ) : (
191 <div id='profileboard-nav'> 265 <select
192 {gameData === null ? <select>error</select> : 266 id="select-game"
193
194 <select id='select-game'
195 onChange={() => { 267 onChange={() => {
196 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 268 setGame(
269 (document.querySelector("#select-game") as HTMLInputElement)
270 .value
271 );
197 setChapter("0"); 272 setChapter("0");
198 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 273 const chapterSelect = document.querySelector(
274 "#select-chapter"
275 ) as HTMLSelectElement;
199 if (chapterSelect) { 276 if (chapterSelect) {
200 chapterSelect.value = "0"; 277 chapterSelect.value = "0";
201 } 278 }
202 }}> 279 }}
203 <option value={0} key={0}>All Scores</option> 280 >
281 <option value={0} key={0}>
282 All Scores
283 </option>
204 {gameData.map((e, i) => ( 284 {gameData.map((e, i) => (
205 <option value={e.id} key={i + 1}>{e.name}</option> 285 <option value={e.id} key={i + 1}>
206 ))}</select> 286 {e.name}
207 } 287 </option>
288 ))}
289 </select>
290 )}
208 291
209 {game === "0" ? 292 {game === "0" ? (
210 <select disabled> 293 <select disabled>
211 <option>All Chapters</option> 294 <option>All Chapters</option>
212 </select> 295 </select>
213 : chapterData === null ? <select></select> : 296 ) : chapterData === null ? (
214 297 <select></select>
215 <select id='select-chapter' 298 ) : (
216 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 299 <select
217 <option value="0" key="0">All Chapters</option> 300 id="select-chapter"
218 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 301 onChange={() =>
219 <option value={e.id} key={i + 1}>{e.name}</option> 302 setChapter(
220 ))}</select> 303 (
221 } 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 )}
222 </div> 323 </div>
223 <div id='profileboard-top'> 324 <div id="profileboard-top">
224 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 325 <span>
225 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 326 <span>Map Name</span>
226 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 327 <img src={SortIcon} alt="" />
227 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 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>
228 <span> </span> 341 <span> </span>
229 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 342 <span>
230 <span><span>Date</span><img src={SortIcon} alt="" /></span> 343 <span>Rank</span>
231 <div id='page-number'> 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">
232 <div> 351 <div>
233 <button onClick={() => { 352 <button
234 if (pageNumber !== 1) { 353 onClick={() => {
235 setPageNumber(prevPageNumber => prevPageNumber - 1); 354 if (pageNumber !== 1) {
236 const records = document.querySelectorAll(".profileboard-record"); 355 setPageNumber(prevPageNumber => prevPageNumber - 1);
237 records.forEach((r) => { 356 const records = document.querySelectorAll(
238 (r as HTMLInputElement).style.height = "44px"; 357 ".profileboard-record"
239 }); 358 );
240 } 359 records.forEach(r => {
241 }} 360 (r as HTMLInputElement).style.height = "44px";
242 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 361 });
243 <span>{pageNumber}/{pageMax}</span> 362 }
244 <button onClick={() => { 363 }}
245 if (pageNumber !== pageMax) { 364 >
246 setPageNumber(prevPageNumber => prevPageNumber + 1); 365 <i
247 const records = document.querySelectorAll(".profileboard-record"); 366 className="triangle"
248 records.forEach((r) => { 367 style={{ position: "relative", left: "-5px" }}
249 (r as HTMLInputElement).style.height = "44px"; 368 ></i>{" "}
250 }); 369 </button>
251 } 370 <span>
252 }} 371 {pageNumber}/{pageMax}
253 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 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>
254 </div> 395 </div>
255 </div> 396 </div>
256 </div> 397 </div>
257 <hr /> 398 <hr />
258 <div id='profileboard-records'> 399 <div id="profileboard-records">
259 400 {game === "0" ? (
260 {game === "0" 401 profile.records
261 ? ( 402 .sort((a, b) => a.map_id - b.map_id)
262 403 .map((r, index) =>
263 profile.records.sort((a, b) => a.map_id - b.map_id) 404 Math.ceil((index + 1) / 20) === pageNumber ? (
264 .map((r, index) => ( 405 <button className="profileboard-record" key={index}>
265 406 {r.scores.map((e, i) => (
266 Math.ceil((index + 1) / 20) === pageNumber ? ( 407 <>
267 <button className="profileboard-record" key={index}> 408 {i !== 0 ? (
268 {r.scores.map((e, i) => (<> 409 <hr style={{ gridColumn: "1 / span 8" }} />
269 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 410 ) : (
270 411 ""
271 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 412 )}
272 413
273 <span style={{ display: "grid" }}>{e.score_count}</span> 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>
274 421
275 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 422 <span style={{ display: "grid" }}>
276 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 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>
277 <span> </span> 430 <span> </span>
278 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 431 {i === 0 ? (
432 <span>#{r.placement}</span>
433 ) : (
434 <span> </span>
435 )}
279 <span>{e.date.split("T")[0]}</span> 436 <span>{e.date.split("T")[0]}</span>
280 <span style={{ flexDirection: "row-reverse" }}> 437 <span style={{ flexDirection: "row-reverse" }}>
281 438 <button
282 <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 439 style={{ marginRight: "10px" }}
283 <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button> 440 onClick={() => {
284 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 441 message(
285 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 442 "Demo Information",
286 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 443 `Demo ID: ${e.demo_id}`
287 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 444 );
288 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 445 }}
289 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 446 >
290 } 447 <img src={ThreedotIcon} alt="demo_id" />
291 }><img src={HistoryIcon} alt="history" /></button> : ""} 448 </button>
292 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 )}
293 </span> 494 </span>
294 </>))} 495 </>
295 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>
296 </button> 525 </button>
297 ) : "" 526 ) : (
298 ))) : maps ? 527 <button className="profileboard-record" key={index}>
299 528 {record.scores.map((e, i) => (
300 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 529 <>
301 .map((r, index) => { 530 {i !== 0 ? (
302 if (Math.ceil((index + 1) / 20) === pageNumber) { 531 <hr style={{ gridColumn: "1 / span 8" }} />
303 let record = profile.records.find((e) => e.map_id === r.id); 532 ) : (
304 return record === undefined ? ( 533 ""
305 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 534 )}
306 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 535 <Link to={`/maps/${r.id}`}>
307 <span style={{ display: "grid" }}>N/A</span> 536 <span>{r.name}</span>
308 <span style={{ display: "grid" }}>N/A</span> 537 </Link>
309 <span>N/A</span> 538 <span style={{ display: "grid" }}>
310 <span> </span> 539 {record!.scores[i].score_count}
311 <span>N/A</span> 540 </span>
312 <span>N/A</span> 541 <span style={{ display: "grid" }}>
313 <span style={{ flexDirection: "row-reverse" }}></span> 542 {record!.scores[i].score_count -
314 </button> 543 record!.map_wr_count >
315 ) : ( 544 0
316 <button className="profileboard-record" key={index}> 545 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
317 {record.scores.map((e, i) => (<> 546 : `-`}
318 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 547 </span>
319 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 548 <span style={{ display: "grid" }}>
320 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 549 {ticks_to_time(record!.scores[i].score_time)}
321 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> 550 </span>
322 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
323 <span> </span> 551 <span> </span>
324 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 552 {i === 0 ? (
553 <span>#{record!.placement}</span>
554 ) : (
555 <span> </span>
556 )}
325 <span>{record!.scores[i].date.split("T")[0]}</span> 557 <span>{record!.scores[i].date.split("T")[0]}</span>
326 <span style={{ flexDirection: "row-reverse" }}> 558 <span style={{ flexDirection: "row-reverse" }}>
327 559 <button
328 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 560 onClick={() => {
329 <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> 561 message(
330 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 562 "Demo Information",
331 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 563 `Demo ID: ${e.demo_id}`
332 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 564 );
333 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 565 }}
334 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 566 >
335 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 567 <img src={ThreedotIcon} alt="demo_id" />
336 } 568 </button>
337 }><img src={HistoryIcon} alt="history" /></button> : ""} 569 <button
338 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 )}
339 </span> 614 </span>
340 </>))} 615 </>
341 </button> 616 ))}
342 617 </button>
343 ) 618 );
344 } else { return null } 619 } else {
345 }) : (<>{console.warn(maps)}</>)} 620 return null;
621 }
622 })
623 ) : (
624 <>{console.warn(maps)}</>
625 )}
346 </div> 626 </div>
347 </section> 627 </section>
348 </main> 628 </main>
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index 71aa427..dec0e17 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -2,146 +2,202 @@ import React, { useEffect } from "react";
2import { Helmet } from "react-helmet"; 2import { Helmet } from "react-helmet";
3 3
4import RankingEntry from "@components/RankingEntry"; 4import RankingEntry from "@components/RankingEntry";
5import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; 5import {
6 Ranking,
7 SteamRanking,
8 RankingType,
9 SteamRankingType,
10} from "@customTypes/Ranking";
6import { API } from "@api/Api"; 11import { API } from "@api/Api";
7 12
8import "@css/Rankings.css"; 13import "@css/Rankings.css";
9 14
10const Rankings: React.FC = () => { 15enum LeaderboardTypes {
11 const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); 16 official,
12 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); 17 unofficial,
13 enum LeaderboardTypes { 18}
14 official,
15 unofficial
16 }
17 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official);
18 19
19 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); 20enum RankingCategories {
21 rankings_overall,
22 rankings_multiplayer,
23 rankings_singleplayer,
24}
20 25
21 enum RankingCategories { 26const Rankings: React.FC = () => {
22 rankings_overall, 27 const [leaderboardData, setLeaderboardData] = React.useState<
23 rankings_multiplayer, 28 Ranking | SteamRanking
24 rankings_singleplayer 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);
25 } 54 }
26 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); 55 setLoad(true);
27 const [load, setLoad] = React.useState<boolean>(false); 56 setLeaderboardLoad(true);
28 57 }, [currentLeaderboardType]);
29 const _fetch_rankings = async () => { 58
30 setLeaderboardLoad(false); 59 const __dev_fetch_unofficial_rankings = async () => {
31 const rankings = await API.get_official_rankings(); 60 try {
32 setLeaderboardData(rankings); 61 setLeaderboardLoad(false);
33 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 62 const rankings = await API.get_unofficial_rankings();
34 setCurrentLeaderboard(rankings.rankings_singleplayer) 63 setLeaderboardData(rankings);
35 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 64 if (currentLeaderboardType === RankingCategories.rankings_singleplayer) {
36 setCurrentLeaderboard(rankings.rankings_multiplayer) 65 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
37 } else { 66 setCurrentLeaderboard(rankings.rankings_singleplayer);
38 setCurrentLeaderboard(rankings.rankings_overall) 67 } else if (
39 } 68 currentLeaderboardType === RankingCategories.rankings_multiplayer
40 setLoad(true); 69 ) {
41 setLeaderboardLoad(true); 70 setCurrentLeaderboard(rankings.rankings_multiplayer);
71 } else {
72 setCurrentLeaderboard(rankings.rankings_overall);
73 }
74 setLeaderboardLoad(true);
75 } catch (e) {
76 console.log(e);
42 } 77 }
43 78 };
44 const __dev_fetch_unofficial_rankings = async () => { 79
45 try { 80 const _set_current_leaderboard = React.useCallback(
46 setLeaderboardLoad(false); 81 (ranking_cat: RankingCategories) => {
47 const rankings = await API.get_unofficial_rankings(); 82 if (ranking_cat === RankingCategories.rankings_singleplayer) {
48 setLeaderboardData(rankings); 83 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
49 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 84 } else if (ranking_cat === RankingCategories.rankings_multiplayer) {
50 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) 85 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
51 setCurrentLeaderboard(rankings.rankings_singleplayer) 86 } else {
52 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 87 setCurrentLeaderboard(leaderboardData!.rankings_overall);
53 setCurrentLeaderboard(rankings.rankings_multiplayer) 88 }
54 } else { 89
55 setCurrentLeaderboard(rankings.rankings_overall) 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)
56 } 139 }
57 setLeaderboardLoad(true); 140 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_singleplayer ? "selected" : ""}`}
58 } catch (e) { 141 >
59 console.log(e) 142 <span>Singleplayer</span>
60 } 143 </button>
61 } 144 <button
62 145 onClick={() =>
63 const _set_current_leaderboard = (ranking_cat: RankingCategories) => { 146 _set_current_leaderboard(RankingCategories.rankings_multiplayer)
64 if (ranking_cat == RankingCategories.rankings_singleplayer) { 147 }
65 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); 148 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_multiplayer ? "selected" : ""}`}
66 } else if (ranking_cat == RankingCategories.rankings_multiplayer) { 149 >
67 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); 150 <span>Cooperative</span>
68 } else { 151 </button>
69 setCurrentLeaderboard(leaderboardData!.rankings_overall); 152 <button
70 } 153 onClick={() =>
71 154 _set_current_leaderboard(RankingCategories.rankings_overall)
72 setCurrentLeaderboardType(ranking_cat); 155 }
73 } 156 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_overall ? "selected" : ""}`}
74 157 >
75 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { 158 <span>Overall</span>
76 if (leaderboard_type == LeaderboardTypes.official) { 159 </button>
77 _fetch_rankings(); 160 </div>
78 } else { 161 </section>
79 162
80 } 163 {load ? (
81 } 164 <section className="rankings-leaderboard">
82 165 <div className="ranks-container">
83 useEffect(() => { 166 <div className="leaderboard-entry header">
84 _fetch_rankings(); 167 <span>Rank</span>
85 if (load) { 168 <span>Player</span>
86 _set_current_leaderboard(RankingCategories.rankings_singleplayer); 169 <span>Portals</span>
87 } 170 </div>
88 }, [load]) 171
89 172 <div className="splitter"></div>
90 return ( 173
91 <main> 174 {leaderboardLoad &&
92 <Helmet> 175 currentLeaderboard?.map((curRankingData, i) => {
93 <title>LPHUB | Rankings</title> 176 return (
94 </Helmet> 177 <RankingEntry
95 <section className="nav-container nav-1"> 178 currentLeaderboardType={currentLeaderboardType}
96 <div> 179 curRankingData={curRankingData}
97 <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> 180 key={i}
98 <span>Official (LPHUB)</span> 181 ></RankingEntry>
99 </button> 182 );
100 <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> 183 })}
101 <span>Unofficial (Steam)</span> 184
102 </button> 185 {leaderboardLoad ? null : (
103 </div> 186 <div
104 </section> 187 style={{
105 <section className="nav-container nav-2"> 188 display: "flex",
106 <div> 189 justifyContent: "center",
107 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> 190 margin: "30px 0px",
108 <span>Singleplayer</span> 191 }}
109 </button> 192 >
110 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> 193 <span className="loader"></span>
111 <span>Cooperative</span> 194 </div>
112 </button> 195 )}
113 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> 196 </div>
114 <span>Overall</span> 197 </section>
115 </button> 198 ) : null}
116 </div> 199 </main>
117 </section> 200 );
118 201};
119 {load ?
120 <section className="rankings-leaderboard">
121 <div className="ranks-container">
122 <div className="leaderboard-entry header">
123 <span>Rank</span>
124 <span>Player</span>
125 <span>Portals</span>
126 </div>
127
128 <div className="splitter"></div>
129
130 {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => {
131 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry>
132 })
133 }
134
135 {leaderboardLoad ? null :
136 <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}>
137 <span className="loader"></span>
138 </div>
139 }
140 </div>
141 </section>
142 : null}
143 </main>
144 )
145}
146 202
147export default Rankings; 203export default Rankings;
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 9f57b7e..9c7885c 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -1,41 +1,37 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4
5import '@css/Rules.css';
6 4
7const Rules: React.FC = () => { 5const Rules: React.FC = () => {
6 const [rulesText, setRulesText] = React.useState<string>("");
8 7
9 const [rulesText, setRulesText] = React.useState<string>(""); 8 React.useEffect(() => {
10 9 const fetchRules = async () => {
11 React.useEffect(() => { 10 try {
12 const fetchRules = async () => { 11 const response = await fetch(
13 try { 12 "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md"
14 const response = await fetch( 13 );
15 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' 14 if (!response.ok) {
16 ); 15 throw new Error("Failed to fetch README");
17 if (!response.ok) { 16 }
18 throw new Error('Failed to fetch README'); 17 const rulesText = await response.text();
19 } 18 setRulesText(rulesText);
20 const rulesText = await response.text(); 19 } catch (error) {
21 setRulesText(rulesText); 20 console.error("Error fetching Rules:", error);
22 } catch (error) { 21 }
23 console.error('Error fetching Rules:', error); 22 // setRulesText(rulesText)
24 } 23 };
25 // setRulesText(rulesText) 24 fetchRules();
26 }; 25 }, []);
27 fetchRules();
28 }, []);
29
30 26
31 return ( 27 return (
32 <main> 28 <main className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
33 <Helmet> 29 <Helmet>
34 <title>LPHUB | Rules</title> 30 <title>LPHUB | Rules</title>
35 </Helmet> 31 </Helmet>
36 <ReactMarkdown>{rulesText}</ReactMarkdown> 32 <ReactMarkdown>{rulesText}</ReactMarkdown>
37 </main> 33 </main>
38 ); 34 );
39}; 35};
40 36
41export default Rules; 37export default Rules;
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index d43c0c6..8c699b1 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -1,15 +1,25 @@
1import React from 'react'; 1import React from "react";
2import { Link, useLocation, useNavigate } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; 5import {
6import { UserProfile } from '@customTypes/Profile'; 6 SteamIcon,
7import { Game, GameChapters } from '@customTypes/Game'; 7 TwitchIcon,
8import { Map } from '@customTypes/Map'; 8 YouTubeIcon,
9import { API } from '@api/Api'; 9 PortalIcon,
10import { ticks_to_time } from '@utils/Time'; 10 FlagIcon,
11import "@css/Profile.css"; 11 StatisticsIcon,
12import useMessage from '@hooks/UseMessage'; 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";
13 23
14interface UserProps { 24interface UserProps {
15 profile?: UserProfile; 25 profile?: UserProfile;
@@ -18,7 +28,6 @@ interface UserProps {
18} 28}
19 29
20const User: React.FC<UserProps> = ({ token, profile, gameData }) => { 30const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
21
22 const { message, MessageDialogComponent } = useMessage(); 31 const { message, MessageDialogComponent } = useMessage();
23 32
24 const [user, setUser] = React.useState<UserProfile | undefined>(undefined); 33 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
@@ -29,13 +38,15 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
29 38
30 const [game, setGame] = React.useState("0"); 39 const [game, setGame] = React.useState("0");
31 const [chapter, setChapter] = React.useState("0"); 40 const [chapter, setChapter] = React.useState("0");
32 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 41 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
42 null
43 );
33 const [maps, setMaps] = React.useState<Map[]>([]); 44 const [maps, setMaps] = React.useState<Map[]>([]);
34 45
35 const location = useLocation(); 46 const location = useLocation();
36 const navigate = useNavigate(); 47 const navigate = useNavigate();
37 48
38 const _fetch_user = async () => { 49 const _fetch_user = React.useCallback(async () => {
39 const userID = location.pathname.split("/")[2]; 50 const userID = location.pathname.split("/")[2];
40 if (token && profile && profile.profile && profile.steam_id === userID) { 51 if (token && profile && profile.profile && profile.steam_id === userID) {
41 navigate("/profile"); 52 navigate("/profile");
@@ -43,9 +54,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
43 } 54 }
44 const userData = await API.get_user(userID); 55 const userData = await API.get_user(userID);
45 setUser(userData); 56 setUser(userData);
46 }; 57 }, [location.pathname, token, profile, navigate]);
47 58
48 const _get_game_chapters = async () => { 59 const _get_game_chapters = React.useCallback(async () => {
49 if (game !== "0") { 60 if (game !== "0") {
50 const gameChapters = await API.get_games_chapters(game); 61 const gameChapters = await API.get_games_chapters(game);
51 setChapterData(gameChapters); 62 setChapterData(gameChapters);
@@ -53,9 +64,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
53 setPageMax(Math.ceil(user!.records.length / 20)); 64 setPageMax(Math.ceil(user!.records.length / 20));
54 setPageNumber(1); 65 setPageNumber(1);
55 } 66 }
56 }; 67 }, [game, user]);
57 68
58 const _get_game_maps = async () => { 69 const _get_game_maps = React.useCallback(async () => {
59 if (chapter === "0") { 70 if (chapter === "0") {
60 const gameMaps = await API.get_game_maps(game); 71 const gameMaps = await API.get_game_maps(game);
61 setMaps(gameMaps); 72 setMaps(gameMaps);
@@ -67,251 +78,331 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
67 setPageMax(Math.ceil(gameChapters.maps.length / 20)); 78 setPageMax(Math.ceil(gameChapters.maps.length / 20));
68 setPageNumber(1); 79 setPageNumber(1);
69 } 80 }
70 }; 81 }, [chapter, game]);
71 82
72 React.useEffect(() => { 83 React.useEffect(() => {
73 _fetch_user(); 84 _fetch_user();
74 }, [location]); 85 }, [location, _fetch_user]);
75 86
76 React.useEffect(() => { 87 React.useEffect(() => {
77 if (user) { 88 if (user) {
78 _get_game_chapters(); 89 _get_game_chapters();
79 } 90 }
80 }, [user, game, location]); 91 }, [user, game, location, _get_game_chapters]);
81 92
82 React.useEffect(() => { 93 React.useEffect(() => {
83 if (user && game !== "0") { 94 if (user && game !== "0") {
84 _get_game_maps(); 95 _get_game_maps();
85 } 96 }
86 }, [user, game, chapter, location]) 97 }, [user, game, chapter, location, _get_game_maps]);
87 98
88 if (!user) { 99 if (!user) {
89 return ( 100 return (
90 <></> 101 <div className="flex justify-center items-center h-[50vh] text-lg text-foreground">
102 Loading...
103 </div>
91 ); 104 );
92 }; 105 }
93 106
94 return ( 107 return (
95 <main> 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">
96 <Helmet> 109 <Helmet>
97 <title>LPHUB | {user.user_name}</title> 110 <title>LPHUB | {user.user_name}</title>
98 <meta name="description" content={user.user_name} /> 111 <meta name="description" content={user.user_name} />
99 </Helmet> 112 </Helmet>
113
100 {MessageDialogComponent} 114 {MessageDialogComponent}
101 <section id='section1' className='profile'> 115
102 <div> 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">
103 <img src={user.avatar_link} alt="profile-image"></img> 117 <div className="grid grid-cols-[200px_1fr_auto] items-center gap-[25px] mb-[25px]">
104 </div> 118 <img
105 <div id='profile-top'> 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 />
106 <div> 123 <div>
107 <div>{user.user_name}</div> 124 <h1 className="m-0 mb-[10px] text-[50px] font-bold text-white font-[--font-barlow-semicondensed-regular]">
108 <div> 125 {user.user_name}
109 {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} 126 </h1>
110 </div> 127 {user.country_code !== "XX" && (
111 <div> 128 <div className="flex items-center gap-3 mb-[15px]">
112 {user.titles.map(e => ( 129 <img
113 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 130 src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`}
114 {e.name} 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}
115 </span> 145 </span>
116 ))} 146 ))}
117 </div> 147 </div>
118 </div> 148 </div>
119 <div> 149 <div className="flex gap-[15px] items-center pr-[10px]">
120 {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 150 {user.links.steam !== "-" && (
121 {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 151 <a href={user.links.steam} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
122 {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 152 <img src={SteamIcon} alt="Steam" className="h-[50px] px-[5px] scale-90 brightness-200" />
123 {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 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 )}
124 </div> 170 </div>
125
126 </div> 171 </div>
127 <div id='profile-bottom'> 172
128 <div> 173 <div className="grid grid-cols-3 gap-3 mt-24">
129 <span>Overall</span> 174 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
130 <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} 175 <div className="text-inherit text-lg">Overall</div>
131 <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> 176 <div className="text-white text-[40px]">
132 </span> 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>
133 </div> 182 </div>
134 <div> 183 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
135 <span>Singleplayer</span> 184 <div className="text-inherit text-lg">Singleplayer</div>
136 <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} 185 <div className="text-white text-[40px]">
137 <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> 186 {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`}
138 </span> 187 </div>
188 <div className="text-white text-xl">
189 {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total}
190 </div>
139 </div> 191 </div>
140 <div> 192 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
141 <span>Cooperative</span> 193 <div className="text-inherit text-lg">Cooperative</div>
142 <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} 194 <div className="text-white text-[40px]">
143 <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> 195 {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`}
144 </span> 196 </div>
197 <div className="text-white text-xl">
198 {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total}
199 </div>
145 </div> 200 </div>
146 </div> 201 </div>
147 </section> 202 </section>
148 203
149 204 <section className="m-5 h-[60px] grid grid-cols-2">
150 <section id='section2' className='profile'> 205 <button
151 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</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] ${
152 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 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>
153 </section> 223 </section>
154 224
155 225 {navState === 0 && (
156 226 <section className="m-5 block bg-[#202232] rounded-3xl overflow-hidden">
157 227 <div className="grid grid-cols-2 mx-5 my-5 mt-[10px] mb-5">
158 228 <select
159 <section id='section3' className='profile1'> 229 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px]"
160 <div id='profileboard-nav'> 230 value={game}
161 {gameData === null ? <select>error</select> : 231 onChange={(e) => {
162 232 setGame(e.target.value);
163 <select id='select-game'
164 onChange={() => {
165 setGame((document.querySelector('#select-game') as HTMLInputElement).value);
166 setChapter("0"); 233 setChapter("0");
167 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 234 }}
168 if (chapterSelect) { 235 >
169 chapterSelect.value = "0"; 236 <option value="0">All Games</option>
170 } 237 {gameData?.map((g) => (
171 }}> 238 <option key={g.id} value={g.id}>
172 <option value={0} key={0}>All Scores</option> 239 {g.name}
173 {gameData.map((e, i) => ( 240 </option>
174 <option value={e.id} key={i + 1}>{e.name}</option> 241 ))}
175 ))}</select> 242 </select>
176 }
177 243
178 {game === "0" ? 244 <select
179 <select disabled> 245 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px] disabled:opacity-50"
180 <option>All Chapters</option> 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 ))}
181 </select> 258 </select>
182 : chapterData === null ? <select></select> : 259 </div>
183 260
184 <select id='select-chapter' 261 <div className="h-[34px] grid text-xl pl-[60px] mx-5 my-0 grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%]">
185 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 262 <div className="flex place-items-end cursor-pointer">
186 <option value="0" key="0">All Chapters</option> 263 <span>Map Name</span>
187 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 264 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
188 <option value={e.id} key={i + 1}>{e.name}</option> 265 </div>
189 ))}</select> 266 <div className="flex place-items-end cursor-pointer">
190 } 267 <span>Portals</span>
191 </div> 268 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
192 <div id='profileboard-top'> 269 </div>
193 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 270 <div className="flex place-items-end cursor-pointer">
194 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 271 <span>WRΔ</span>
195 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 272 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
196 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 273 </div>
197 <span> </span> 274 <div className="flex place-items-end cursor-pointer">
198 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 275 <span>Time</span>
199 <span><span>Date</span><img src={SortIcon} alt="" /></span> 276 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
200 <div id='page-number'> 277 </div>
201 <div> 278 <div></div>
202 <button onClick={() => { 279 <div className="flex place-items-end cursor-pointer">
203 if (pageNumber !== 1) { 280 <span>Rank</span>
204 setPageNumber(prevPageNumber => prevPageNumber - 1); 281 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
205 const records = document.querySelectorAll(".profileboard-record"); 282 </div>
206 records.forEach((r) => { 283 <div className="flex place-items-end cursor-pointer">
207 (r as HTMLInputElement).style.height = "44px"; 284 <span>Date</span>
208 }); 285 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
209 } 286 </div>
210 }} 287 <div className="flex items-center gap-[10px] justify-center">
211 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 288 <button
212 <span>{pageNumber}/{pageMax}</span> 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"
213 <button onClick={() => { 290 onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
214 if (pageNumber !== pageMax) { 291 disabled={pageNumber === 1}
215 setPageNumber(prevPageNumber => prevPageNumber + 1); 292 >
216 const records = document.querySelectorAll(".profileboard-record"); 293
217 records.forEach((r) => { 294 </button>
218 (r as HTMLInputElement).style.height = "44px"; 295 <span className="text-sm text-foreground">{pageNumber}/{pageMax}</span>
219 }); 296 <button
220 } 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"
221 }} 298 onClick={() => setPageNumber(Math.min(pageMax, pageNumber + 1))}
222 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 299 disabled={pageNumber === pageMax}
300 >
301
302 </button>
223 </div> 303 </div>
224 </div> 304 </div>
225 </div>
226 <hr />
227 <div id='profileboard-records'>
228
229 {game === "0"
230 ? (
231
232 user.records.sort((a, b) => a.map_id - b.map_id)
233 .map((r, index) => (
234 305
306 <div>
307 {game === "0" ? (
308 user.records
309 .sort((a, b) => a.map_id - b.map_id)
310 .map((record, index) =>
235 Math.ceil((index + 1) / 20) === pageNumber ? ( 311 Math.ceil((index + 1) / 20) === pageNumber ? (
236 <button className="profileboard-record" key={index}> 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]">
237 {r.scores.map((e, i) => (<> 313 <Link to={`/maps/${record.map_id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
238 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 314 {record.map_name}
239 315 </Link>
240 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 316 <span className="flex place-items-center h-11">{record.scores[0]?.score_count || 'N/A'}</span>
241 317 <span className={`flex place-items-center h-11 ${record.scores[0]?.score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}>
242 <span style={{ display: "grid" }}>{e.score_count}</span> 318 {record.scores[0]?.score_count - record.map_wr_count > 0
243 319 ? `+${record.scores[0].score_count - record.map_wr_count}`
244 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 320 : '–'}
245 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 321 </span>
246 <span> </span> 322 <span className="flex place-items-center h-11">{record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
247 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 323 <span className="flex place-items-center h-11"></span>
248 <span>{e.date.split("T")[0]}</span> 324 <span className="flex place-items-center h-11 font-semibold">#{record.placement}</span>
249 <span style={{ flexDirection: "row-reverse" }}> 325 <span className="flex place-items-center h-11">{record.scores[0]?.date.split("T")[0] || 'N/A'}</span>
250 326 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
251 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 327 <button
252 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 328 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
253 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 329 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0]?.demo_id}`)}
254 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 330 title="Demo Info"
255 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 331 >
256 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 332 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
257 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 333 </button>
258 } 334 <button
259 }><img src={HistoryIcon} alt="history" /></button> : ""} 335 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
260 336 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0]?.demo_id}`}
261 </span> 337 title="Download Demo"
262 </>))} 338 >
263 339 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
264 </button> 340 </button>
265 ) : "" 341 {record.scores.length > 1 && (
266 ))) : maps ? 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">
267 343 <img src={HistoryIcon} alt="History" className="w-4 h-4" />
268 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 344 </button>
269 .map((r, index) => { 345 )}
270 if (Math.ceil((index + 1) / 20) === pageNumber) { 346 </div>
271 let record = user.records.find((e) => e.map_id === r.id); 347 </div>
272 return record === undefined ? ( 348 ) : null
273 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 349 )
274 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 350 ) : (
275 <span style={{ display: "grid" }}>N/A</span> 351 maps
276 <span style={{ display: "grid" }}>N/A</span> 352 ?.filter(map => !map.is_disabled)
277 <span>N/A</span> 353 .sort((a, b) => a.id - b.id)
278 <span> </span> 354 .map((map, index) => {
279 <span>N/A</span> 355 if (Math.ceil((index + 1) / 20) !== pageNumber) return null;
280 <span>N/A</span> 356
281 <span style={{ flexDirection: "row-reverse" }}></span> 357 const record = user.records.find(r => r.map_id === map.id);
282 </button> 358
283 ) : ( 359 return (
284 <button className="profileboard-record" key={index}> 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' : ''}`}>
285 {record.scores.map((e, i) => (<> 361 <Link to={`/maps/${map.id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
286 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 362 {map.name}
287 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 363 </Link>
288 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 364 <span className="flex place-items-center h-11">{record?.scores[0]?.score_count || 'N/A'}</span>
289 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</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]' : ''}`}>
290 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> 366 {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0
291 <span> </span> 367 ? `+${record.scores[0].score_count - record.map_wr_count}`
292 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 368 : '–'}
293 <span>{record!.scores[i].date.split("T")[0]}</span> 369 </span>
294 <span style={{ flexDirection: "row-reverse" }}> 370 <span className="flex place-items-center h-11">{record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
295 371 <span className="flex place-items-center h-11"></span>
296 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 372 <span className="flex place-items-center h-11 font-semibold">{record ? `#${record.placement}` : 'N/A'}</span>
297 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 373 <span className="flex place-items-center h-11">{record?.scores[0]?.date.split("T")[0] || 'N/A'}</span>
298 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 374 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
299 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 375 {record?.scores[0] && (
300 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 376 <>
301 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 377 <button
302 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 378 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
303 } 379 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0].demo_id}`)}
304 }><img src={HistoryIcon} alt="history" /></button> : ""} 380 title="Demo Info"
305 381 >
306 </span> 382 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
307 </>))} 383 </button>
308 </button> 384 <button
309 385 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
310 ) 386 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0].demo_id}`}
311 } else { return null } 387 title="Download Demo"
312 }) : (<>{console.warn(maps)}</>)} 388 >
313 </div> 389 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
314 </section> 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 )}
315 </main> 406 </main>
316 ); 407 );
317}; 408};