aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/About.tsx54
-rw-r--r--frontend/src/pages/Games.tsx61
-rw-r--r--frontend/src/pages/Homepage.tsx39
-rw-r--r--frontend/src/pages/Maplist.tsx165
-rw-r--r--frontend/src/pages/Maps.tsx129
-rw-r--r--frontend/src/pages/Profile.tsx669
-rw-r--r--frontend/src/pages/Rankings.tsx319
-rw-r--r--frontend/src/pages/Rules.tsx56
-rw-r--r--frontend/src/pages/User.tsx599
9 files changed, 1405 insertions, 686 deletions
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index a8b7826..a5d34f6 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -5,36 +5,34 @@ import { Helmet } from 'react-helmet';
5import '@css/About.css'; 5import '@css/About.css';
6 6
7const About: React.FC = () => { 7const About: React.FC = () => {
8 const [aboutText, setAboutText] = React.useState<string>('');
8 9
9 const [aboutText, setAboutText] = React.useState<string>(""); 10 React.useEffect(() => {
11 const fetchReadme = async () => {
12 try {
13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md'
15 );
16 if (!response.ok) {
17 throw new Error('Failed to fetch README');
18 }
19 const readmeText = await response.text();
20 setAboutText(readmeText);
21 } catch (error) {
22 console.error('Error fetching README:', error);
23 }
24 };
25 fetchReadme();
26 }, []);
10 27
11 React.useEffect(() => { 28 return (
12 const fetchReadme = async () => { 29 <div id="about">
13 try { 30 <Helmet>
14 const response = await fetch( 31 <title>LPHUB | About</title>
15 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' 32 </Helmet>
16 ); 33 <ReactMarkdown>{aboutText}</ReactMarkdown>
17 if (!response.ok) { 34 </div>
18 throw new Error('Failed to fetch README'); 35 );
19 }
20 const readmeText = await response.text();
21 setAboutText(readmeText);
22 } catch (error) {
23 console.error('Error fetching README:', error);
24 }
25 };
26 fetchReadme();
27 }, []);
28
29
30 return (
31 <div id="about">
32 <Helmet>
33 <title>LPHUB | About</title>
34 </Helmet>
35 <ReactMarkdown>{aboutText}</ReactMarkdown>
36 </div>
37 );
38}; 36};
39 37
40export default About; 38export default About;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index 15cc891..ae0a2d6 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -3,44 +3,45 @@ import { 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" 6import '@css/Maps.css';
7 7
8interface GamesProps { 8interface GamesProps {
9 games: Game[]; 9 games: Game[];
10} 10}
11 11
12const Games: React.FC<GamesProps> = ({ games }) => { 12const Games: React.FC<GamesProps> = ({ games }) => {
13 const _page_load = () => {
14 const loaders = document.querySelectorAll('.loader');
15 loaders.forEach(loader => {
16 (loader as HTMLElement).style.display = 'none';
17 });
18 };
13 19
14 const _page_load = () => { 20 React.useEffect(() => {
15 const loaders = document.querySelectorAll(".loader"); 21 document
16 loaders.forEach((loader) => { 22 .querySelectorAll('.games-page-item-body')
17 (loader as HTMLElement).style.display = "none"; 23 .forEach((game, index) => {
18 }); 24 game.innerHTML = '';
19 } 25 });
26 _page_load();
27 }, []);
20 28
21 React.useEffect(() => { 29 return (
22 document.querySelectorAll(".games-page-item-body").forEach((game, index) => { 30 <div className="games-page">
23 game.innerHTML = ""; 31 <Helmet>
24 }); 32 <title>LPHUB | Games</title>
25 _page_load(); 33 </Helmet>
26 }, []); 34 <section>
27 35 <div className="games-page-content">
28 return ( 36 <div className="games-page-item-content">
29 <div className='games-page'> 37 {games.map((game, index) => (
30 <Helmet> 38 <GameEntry game={game} key={index} />
31 <title>LPHUB | Games</title> 39 ))}
32 </Helmet> 40 </div>
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> 41 </div>
43 ); 42 </section>
43 </div>
44 );
44}; 45};
45 46
46export default Games; 47export default Games;
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 4f46af5..859af52 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -2,21 +2,30 @@ import 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>
7 <main> 7 <Helmet>
8 <Helmet> 8 <title>LPHUB | Homepage</title>
9 <title>LPHUB | Homepage</title> 9 </Helmet>
10 </Helmet> 10 <section>
11 <section> 11 <p />
12 <p /> 12 <h1>Welcome to Least Portals Hub!</h1>
13 <h1>Welcome to Least Portals Hub!</h1> 13 <p>
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>
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>
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 04938cf..5138964 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -1,11 +1,11 @@
1import React, { useEffect } from "react"; 1import 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"; 5import '@css/Maplist.css';
6import { API } from "@api/Api"; 6import { API } from '@api/Api';
7import { Game } from "@customTypes/Game"; 7import { Game } from '@customTypes/Game';
8import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 8import { GameChapter, GamesChapters } from '@customTypes/Chapters';
9 9
10const Maplist: React.FC = () => { 10const Maplist: React.FC = () => {
11 const [game, setGame] = React.useState<Game | null>(null); 11 const [game, setGame] = React.useState<Game | null>(null);
@@ -19,41 +19,41 @@ const Maplist: React.FC = () => {
19 const [curChapter, setCurChapter] = React.useState<GameChapter>(); 19 const [curChapter, setCurChapter] = React.useState<GameChapter>();
20 const [numChapters, setNumChapters] = React.useState<number>(0); 20 const [numChapters, setNumChapters] = React.useState<number>(0);
21 21
22 const [dropdownActive, setDropdownActive] = React.useState("none"); 22 const [dropdownActive, setDropdownActive] = React.useState('none');
23 23
24 const params = useParams<{ id: string, chapter: string }>(); 24 const params = useParams<{ id: string; chapter: string }>();
25 const location = useLocation(); 25 const location = useLocation();
26 const navigate = useNavigate(); 26 const navigate = useNavigate();
27 27
28 function _update_currently_selected(catNum2: number) { 28 function _update_currently_selected(catNum2: number) {
29 setCurrentlySelected(catNum2); 29 setCurrentlySelected(catNum2);
30 navigate("/games/" + game?.id + "?cat=" + catNum2); 30 navigate('/games/' + game?.id + '?cat=' + catNum2);
31 setHasClicked(true); 31 setHasClicked(true);
32 } 32 }
33 33
34 const _fetch_chapters = async (chapter_id: string) => { 34 const _fetch_chapters = async (chapter_id: string) => {
35 const chapters = await API.get_chapters(chapter_id); 35 const chapters = await API.get_chapters(chapter_id);
36 setCurChapter(chapters); 36 setCurChapter(chapters);
37 } 37 };
38 38
39 const _handle_dropdown_click = () => { 39 const _handle_dropdown_click = () => {
40 if (dropdownActive == "none") { 40 if (dropdownActive == 'none') {
41 setDropdownActive("block"); 41 setDropdownActive('block');
42 } else { 42 } else {
43 setDropdownActive("none"); 43 setDropdownActive('none');
44 } 44 }
45 } 45 };
46 46
47 // im sorry but im too lazy to fix this right now 47 // im sorry but im too lazy to fix this right now
48 useEffect(() => { 48 useEffect(() => {
49 // gameID 49 // gameID
50 const gameId = parseFloat(params.id || ""); 50 const gameId = parseFloat(params.id || '');
51 setId(gameId); 51 setId(gameId);
52 52
53 // location query params 53 // location query params
54 const queryParams = new URLSearchParams(location.search); 54 const queryParams = new URLSearchParams(location.search);
55 if (queryParams.get("chapter")) { 55 if (queryParams.get('chapter')) {
56 let cat = parseFloat(queryParams.get("chapter") || ""); 56 let cat = parseFloat(queryParams.get('chapter') || '');
57 if (gameId == 2) { 57 if (gameId == 2) {
58 cat += 10; 58 cat += 10;
59 } 59 }
@@ -62,7 +62,7 @@ const Maplist: React.FC = () => {
62 62
63 const _fetch_game = async () => { 63 const _fetch_game = async () => {
64 const games = await API.get_games(); 64 const games = await API.get_games();
65 const foundGame = games.find((game) => game.id === gameId); 65 const foundGame = games.find(game => game.id === gameId);
66 // console.log(foundGame) 66 // console.log(foundGame)
67 if (foundGame) { 67 if (foundGame) {
68 setGame(foundGame); 68 setGame(foundGame);
@@ -74,7 +74,7 @@ const Maplist: React.FC = () => {
74 const games_chapters = await API.get_games_chapters(gameId.toString()); 74 const games_chapters = await API.get_games_chapters(gameId.toString());
75 setGameChapters(games_chapters); 75 setGameChapters(games_chapters);
76 setNumChapters(games_chapters.chapters.length); 76 setNumChapters(games_chapters.chapters.length);
77 } 77 };
78 78
79 setLoad(true); 79 setLoad(true);
80 _fetch_game(); 80 _fetch_game();
@@ -83,21 +83,19 @@ const Maplist: React.FC = () => {
83 83
84 useEffect(() => { 84 useEffect(() => {
85 const queryParams = new URLSearchParams(location.search); 85 const queryParams = new URLSearchParams(location.search);
86 if (gameChapters != undefined && !queryParams.get("chapter")) { 86 if (gameChapters != undefined && !queryParams.get('chapter')) {
87 _fetch_chapters(gameChapters!.chapters[0].id.toString()); 87 _fetch_chapters(gameChapters!.chapters[0].id.toString());
88 } 88 }
89 }, [gameChapters]) 89 }, [gameChapters]);
90
91
92 90
93 return ( 91 return (
94 <main> 92 <main>
95 <Helmet> 93 <Helmet>
96 <title>LPHUB | Maplist</title> 94 <title>LPHUB | Maplist</title>
97 </Helmet> 95 </Helmet>
98 <section style={{ marginTop: "20px" }}> 96 <section style={{ marginTop: '20px' }}>
99 <Link to="/games"> 97 <Link to="/games">
100 <button className="nav-button" style={{ borderRadius: "20px" }}> 98 <button className="nav-button" style={{ borderRadius: '20px' }}>
101 <i className="triangle"></i> 99 <i className="triangle"></i>
102 <span>Games List</span> 100 <span>Games List</span>
103 </button> 101 </button>
@@ -117,7 +115,7 @@ const Maplist: React.FC = () => {
117 <h2 className="portal-count"> 115 <h2 className="portal-count">
118 { 116 {
119 game?.category_portals.find( 117 game?.category_portals.find(
120 (obj) => obj.category.id === catNum + 1 118 obj => obj.category.id === catNum + 1
121 )?.portal_count 119 )?.portal_count
122 } 120 }
123 </h2> 121 </h2>
@@ -125,7 +123,19 @@ const Maplist: React.FC = () => {
125 </div> 123 </div>
126 <div className="game-header-categories"> 124 <div className="game-header-categories">
127 {game?.category_portals.map((cat, index) => ( 125 {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) }}> 126 <button
127 key={index}
128 className={
129 currentlySelected == cat.category.id ||
130 (cat.category.id - 1 == catNum && !hasClicked)
131 ? 'game-cat-button selected'
132 : 'game-cat-button'
133 }
134 onClick={() => {
135 setCatNum(cat.category.id - 1);
136 _update_currently_selected(cat.category.id);
137 }}
138 >
129 <span>{cat.category.name}</span> 139 <span>{cat.category.name}</span>
130 </button> 140 </button>
131 ))} 141 ))}
@@ -136,45 +146,88 @@ const Maplist: React.FC = () => {
136 <div> 146 <div>
137 <section className="chapter-select-container"> 147 <section className="chapter-select-container">
138 <div> 148 <div>
139 <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> 149 <span
150 style={{
151 fontSize: '18px',
152 transform: 'translateY(5px)',
153 display: 'block',
154 marginTop: '10px',
155 }}
156 >
157 {curChapter?.chapter.name.split(' - ')[0]}
158 </span>
140 </div> 159 </div>
141 <div onClick={_handle_dropdown_click} className="dropdown"> 160 <div onClick={_handle_dropdown_click} className="dropdown">
142 <span>{curChapter?.chapter.name.split(" - ")[1]}</span> 161 <span>{curChapter?.chapter.name.split(' - ')[1]}</span>
143 <i className="triangle"></i> 162 <i className="triangle"></i>
144 </div> 163 </div>
145 <div className="dropdown-elements" style={{ display: dropdownActive }}> 164 <div
165 className="dropdown-elements"
166 style={{ display: dropdownActive }}
167 >
146 {gameChapters?.chapters.map((chapter, i) => { 168 {gameChapters?.chapters.map((chapter, i) => {
147 return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div> 169 return (
148 }) 170 <div
149 171 className="dropdown-element"
150 } 172 onClick={() => {
173 _fetch_chapters(chapter.id.toString());
174 _handle_dropdown_click();
175 }}
176 >
177 {chapter.name}
178 </div>
179 );
180 })}
151 </div> 181 </div>
152 </section> 182 </section>
153 <section className="maplist"> 183 <section className="maplist">
154 {curChapter?.maps.map((map, i) => { 184 {curChapter?.maps.map((map, i) => {
155 return <div className="maplist-entry"> 185 return (
156 <Link to={`/maps/${map.id}`}> 186 <div className="maplist-entry">
157 <span>{map.name}</span> 187 <Link to={`/maps/${map.id}`}>
158 <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}> 188 <span>{map.name}</span>
159 <div className="blur map"> 189 <div
160 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( 190 className="map-entry-image"
161 (obj) => obj.category.id === catNum + 1 191 style={{ backgroundImage: `url(${map.image})` }}
162 )?.portal_count}</span> 192 >
163 <span>portals</span> 193 <div className="blur map">
194 <span>
195 {map.is_disabled
196 ? map.category_portals[0].portal_count
197 : map.category_portals.find(
198 obj => obj.category.id === catNum + 1
199 )?.portal_count}
200 </span>
201 <span>portals</span>
202 </div>
164 </div> 203 </div>
165 </div> 204 <div className="difficulty-bar">
166 <div className="difficulty-bar"> 205 {/* <span>Difficulty:</span> */}
167 {/* <span>Difficulty:</span> */} 206 <div
168 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}> 207 className={
169 <div className="difficulty-point"></div> 208 map.difficulty == 0
170 <div className="difficulty-point"></div> 209 ? 'one'
171 <div className="difficulty-point"></div> 210 : map.difficulty == 1
172 <div className="difficulty-point"></div> 211 ? 'two'
173 <div className="difficulty-point"></div> 212 : map.difficulty == 2
213 ? 'three'
214 : map.difficulty == 3
215 ? 'four'
216 : map.difficulty == 4
217 ? 'five'
218 : 'one'
219 }
220 >
221 <div className="difficulty-point"></div>
222 <div className="difficulty-point"></div>
223 <div className="difficulty-point"></div>
224 <div className="difficulty-point"></div>
225 <div className="difficulty-point"></div>
226 </div>
174 </div> 227 </div>
175 </div> 228 </Link>
176 </Link> 229 </div>
177 </div> 230 );
178 })} 231 })}
179 </section> 232 </section>
180 </div> 233 </div>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index fb13563..51a2020 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -9,26 +9,31 @@ import 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"; 12import '@css/Maps.css';
13 13
14interface MapProps { 14interface MapProps {
15 token?: string; 15 token?: string;
16 isModerator: boolean; 16 isModerator: boolean;
17}; 17}
18 18
19const Maps: React.FC<MapProps> = ({ token, isModerator }) => { 19const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
20
21 const [selectedRun, setSelectedRun] = React.useState<number>(0); 20 const [selectedRun, setSelectedRun] = React.useState<number>(0);
22 21
23 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); 22 const [mapSummaryData, setMapSummaryData] = React.useState<
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); 23 MapSummary | undefined
25 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined); 24 >(undefined);
25 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<
26 MapLeaderboard | undefined
27 >(undefined);
28 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<
29 MapDiscussions | undefined
30 >(undefined);
26 31
27 const [navState, setNavState] = React.useState<number>(0); 32 const [navState, setNavState] = React.useState<number>(0);
28 33
29 const location = useLocation(); 34 const location = useLocation();
30 35
31 const mapID = location.pathname.split("/")[2]; 36 const mapID = location.pathname.split('/')[2];
32 37
33 const _fetch_map_summary = async () => { 38 const _fetch_map_summary = async () => {
34 const mapSummary = await API.get_map_summary(mapID); 39 const mapSummary = await API.get_map_summary(mapID);
@@ -36,7 +41,7 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
36 }; 41 };
37 42
38 const _fetch_map_leaderboards = async () => { 43 const _fetch_map_leaderboards = async () => {
39 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); 44 const mapLeaderboards = await API.get_map_leaderboard(mapID, '1');
40 setMapLeaderboardData(mapLeaderboards); 45 setMapLeaderboardData(mapLeaderboards);
41 }; 46 };
42 47
@@ -56,19 +61,36 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
56 return ( 61 return (
57 <> 62 <>
58 <main> 63 <main>
59 <section id='section1' className='summary1'> 64 <section id="section1" className="summary1">
60 <div> 65 <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> 66 <Link to="/games">
67 <button
68 className="nav-button"
69 style={{ borderRadius: '20px 20px 20px 20px' }}
70 >
71 <i className="triangle"></i>
72 <span>Games List</span>
73 </button>
74 </Link>
62 </div> 75 </div>
63 </section> 76 </section>
64 77
65 <section id='section2' className='summary1'> 78 <section id="section2" className="summary1">
66 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> 79 <button className="nav-button">
67 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 80 <img src={PortalIcon} alt="" />
68 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> 81 <span>Summary</span>
82 </button>
83 <button className="nav-button">
84 <img src={FlagIcon} alt="" />
85 <span>Leaderboards</span>
86 </button>
87 <button className="nav-button">
88 <img src={ChatIcon} alt="" />
89 <span>Discussions</span>
90 </button>
69 </section> 91 </section>
70 92
71 <section id='section6' className='summary2' /> 93 <section id="section6" className="summary2" />
72 </main> 94 </main>
73 </> 95 </>
74 ); 96 );
@@ -80,29 +102,80 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
80 <title>LPHUB | {mapSummaryData.map.map_name}</title> 102 <title>LPHUB | {mapSummaryData.map.map_name}</title>
81 <meta name="description" content={mapSummaryData.map.map_name} /> 103 <meta name="description" content={mapSummaryData.map.map_name} />
82 </Helmet> 104 </Helmet>
83 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 105 {isModerator && (
84 106 <ModMenu
85 <div id='background-image'> 107 token={token}
108 data={mapSummaryData}
109 selectedRun={selectedRun}
110 mapID={mapID}
111 />
112 )}
113
114 <div id="background-image">
86 <img src={mapSummaryData.map.image} alt="" /> 115 <img src={mapSummaryData.map.image} alt="" />
87 </div> 116 </div>
88 <main> 117 <main>
89 <section id='section1' className='summary1'> 118 <section id="section1" className="summary1">
90 <div> 119 <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> 120 <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> 121 <button
93 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 122 className="nav-button"
123 style={{ borderRadius: '20px 0px 0px 20px' }}
124 >
125 <i className="triangle"></i>
126 <span>Games List</span>
127 </button>
128 </Link>
129 <Link
130 to={`/games/${mapSummaryData.map.is_coop ? '2' : '1'}?chapter=${mapSummaryData.map.chapter_name.split(' ')[1]}`}
131 >
132 <button
133 className="nav-button"
134 style={{ borderRadius: '0px 20px 20px 0px', marginLeft: '2px' }}
135 >
136 <i className="triangle"></i>
137 <span>{mapSummaryData.map.chapter_name}</span>
138 </button>
139 </Link>
140 <br />
141 <span>
142 <b>{mapSummaryData.map.map_name}</b>
143 </span>
94 </div> 144 </div>
95 </section> 145 </section>
96 146
97 <section id='section2' className='summary1'> 147 <section id="section2" className="summary1">
98 <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> 148 <button className="nav-button" onClick={() => setNavState(0)}>
99 <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 149 <img src={PortalIcon} alt="" />
100 <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> 150 <span>Summary</span>
151 </button>
152 <button className="nav-button" onClick={() => setNavState(1)}>
153 <img src={FlagIcon} alt="" />
154 <span>Leaderboards</span>
155 </button>
156 <button className="nav-button" onClick={() => setNavState(2)}>
157 <img src={ChatIcon} alt="" />
158 <span>Discussions</span>
159 </button>
101 </section> 160 </section>
102 161
103 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 162 {navState === 0 && (
163 <Summary
164 selectedRun={selectedRun}
165 setSelectedRun={setSelectedRun}
166 data={mapSummaryData}
167 />
168 )}
104 {navState === 1 && <Leaderboards mapID={mapID} />} 169 {navState === 1 && <Leaderboards mapID={mapID} />}
105 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 170 {navState === 2 && (
171 <Discussions
172 data={mapDiscussionsData}
173 token={token}
174 isModerator={isModerator}
175 mapID={mapID}
176 onRefresh={() => _fetch_map_discussions()}
177 />
178 )}
106 </main> 179 </main>
107 </> 180 </>
108 ); 181 );
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 48233bf..7e3d603 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -2,16 +2,28 @@ import 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 {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16 DeleteIcon,
17} from '@images/Images';
6import { UserProfile } from '@customTypes/Profile'; 18import { UserProfile } from '@customTypes/Profile';
7import { Game, GameChapters } from '@customTypes/Game'; 19import { Game, GameChapters } from '@customTypes/Game';
8import { Map } from '@customTypes/Map'; 20import { Map } from '@customTypes/Map';
9import { ticks_to_time } from '@utils/Time'; 21import { ticks_to_time } from '@utils/Time';
10import "@css/Profile.css"; 22import '@css/Profile.css';
11import { API } from '@api/Api'; 23import { API } from '@api/Api';
12import useConfirm from '@hooks/UseConfirm'; 24import useConfirm from '@hooks/UseConfirm';
13import useMessage from '@hooks/UseMessage'; 25import useMessage from '@hooks/UseMessage';
14import useMessageLoad from "@hooks/UseMessageLoad"; 26import useMessageLoad from '@hooks/UseMessageLoad';
15 27
16interface ProfileProps { 28interface ProfileProps {
17 profile?: UserProfile; 29 profile?: UserProfile;
@@ -20,17 +32,25 @@ interface ProfileProps {
20 onDeleteRecord: () => void; 32 onDeleteRecord: () => void;
21} 33}
22 34
23const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { 35const Profile: React.FC<ProfileProps> = ({
36 profile,
37 token,
38 gameData,
39 onDeleteRecord,
40}) => {
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 41 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { message, MessageDialogComponent } = useMessage(); 42 const { message, MessageDialogComponent } = useMessage();
26 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 43 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
44 useMessageLoad();
27 const [navState, setNavState] = React.useState(0); 45 const [navState, setNavState] = React.useState(0);
28 const [pageNumber, setPageNumber] = React.useState(1); 46 const [pageNumber, setPageNumber] = React.useState(1);
29 const [pageMax, setPageMax] = React.useState(0); 47 const [pageMax, setPageMax] = React.useState(0);
30 48
31 const [game, setGame] = React.useState("0") 49 const [game, setGame] = React.useState('0');
32 const [chapter, setChapter] = React.useState("0") 50 const [chapter, setChapter] = React.useState('0');
33 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 51 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
52 null
53 );
34 const [maps, setMaps] = React.useState<Map[]>([]); 54 const [maps, setMaps] = React.useState<Map[]>([]);
35 55
36 const navigate = useNavigate(); 56 const navigate = useNavigate();
@@ -42,17 +62,17 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
42 }; 62 };
43 63
44 const _get_game_chapters = async () => { 64 const _get_game_chapters = async () => {
45 if (game && game !== "0") { 65 if (game && game !== '0') {
46 const gameChapters = await API.get_games_chapters(game); 66 const gameChapters = await API.get_games_chapters(game);
47 setChapterData(gameChapters); 67 setChapterData(gameChapters);
48 } else if (game && game === "0") { 68 } else if (game && game === '0') {
49 setPageMax(Math.ceil(profile!.records.length / 20)); 69 setPageMax(Math.ceil(profile!.records.length / 20));
50 setPageNumber(1); 70 setPageNumber(1);
51 } 71 }
52 }; 72 };
53 73
54 const _get_game_maps = async () => { 74 const _get_game_maps = async () => {
55 if (chapter === "0") { 75 if (chapter === '0') {
56 const gameMaps = await API.get_game_maps(game); 76 const gameMaps = await API.get_game_maps(game);
57 setMaps(gameMaps); 77 setMaps(gameMaps);
58 setPageMax(Math.ceil(gameMaps.length / 20)); 78 setPageMax(Math.ceil(gameMaps.length / 20));
@@ -66,28 +86,31 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
66 }; 86 };
67 87
68 const _delete_submission = async (map_id: number, record_id: number) => { 88 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?"); 89 const userConfirmed = await confirm(
90 'Delete Record',
91 'Are you sure you want to delete this record?'
92 );
70 93
71 if (!userConfirmed) { 94 if (!userConfirmed) {
72 return; 95 return;
73 } 96 }
74 97
75 messageLoad("Deleting..."); 98 messageLoad('Deleting...');
76 99
77 const api_success = await API.delete_map_record(token!, map_id, record_id); 100 const api_success = await API.delete_map_record(token!, map_id, record_id);
78 messageLoadClose(); 101 messageLoadClose();
79 if (api_success) { 102 if (api_success) {
80 await message("Delete Record", "Successfully deleted record."); 103 await message('Delete Record', 'Successfully deleted record.');
81 onDeleteRecord(); 104 onDeleteRecord();
82 } else { 105 } else {
83 await message("Delete Record", "Could not delete record."); 106 await message('Delete Record', 'Could not delete record.');
84 } 107 }
85 }; 108 };
86 109
87 React.useEffect(() => { 110 React.useEffect(() => {
88 if (!profile) { 111 if (!profile) {
89 navigate("/"); 112 navigate('/');
90 }; 113 }
91 }, [profile]); 114 }, [profile]);
92 115
93 React.useEffect(() => { 116 React.useEffect(() => {
@@ -97,16 +120,14 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
97 }, [profile, game]); 120 }, [profile, game]);
98 121
99 React.useEffect(() => { 122 React.useEffect(() => {
100 if (profile && game !== "0") { 123 if (profile && game !== '0') {
101 _get_game_maps(); 124 _get_game_maps();
102 } 125 }
103 }, [profile, game, chapter, chapterData]) 126 }, [profile, game, chapter, chapterData]);
104 127
105 if (!profile) { 128 if (!profile) {
106 return ( 129 return <></>;
107 <></> 130 }
108 );
109 };
110 131
111 return ( 132 return (
112 <div> 133 <div>
@@ -119,230 +140,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
119 {ConfirmDialogComponent} 140 {ConfirmDialogComponent}
120 141
121 <main> 142 <main>
122 <section id='section1' className='profile'> 143 <section id="section1" className="profile">
123 144 {profile.profile ? (
124 {profile.profile 145 <div id="profile-image" onClick={_update_profile}>
125 ? ( 146 <img src={profile.avatar_link} alt="profile-image"></img>
126 <div id='profile-image' onClick={_update_profile}> 147 <span>Refresh</span>
127 <img src={profile.avatar_link} alt="profile-image"></img> 148 </div>
128 <span>Refresh</span> 149 ) : (
129 </div> 150 <div>
130 ) : ( 151 <img src={profile.avatar_link} alt="profile-image"></img>
131 <div> 152 </div>
132 <img src={profile.avatar_link} alt="profile-image"></img> 153 )}
133 </div>
134 )}
135 154
136 <div id='profile-top'> 155 <div id="profile-top">
137 <div> 156 <div>
138 <div>{profile.user_name}</div> 157 <div>{profile.user_name}</div>
139 <div> 158 <div>
140 {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} 159 {profile.country_code === 'XX' ? (
160 ''
161 ) : (
162 <img
163 src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`}
164 alt={profile.country_code}
165 />
166 )}
141 </div> 167 </div>
142 <div> 168 <div>
143 {profile.titles.map(e => ( 169 {profile.titles.map(e => (
144 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 170 <span
171 className="titles"
172 style={{ backgroundColor: `#${e.color}` }}
173 >
145 {e.name} 174 {e.name}
146 </span> 175 </span>
147 ))} 176 ))}
148 </div> 177 </div>
149 </div> 178 </div>
150 <div> 179 <div>
151 {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 180 {profile.links.steam === '-' ? (
152 {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 181 ''
153 {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 182 ) : (
154 {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 183 <a href={profile.links.steam}>
184 <img src={SteamIcon} alt="Steam" />
185 </a>
186 )}
187 {profile.links.twitch === '-' ? (
188 ''
189 ) : (
190 <a href={profile.links.twitch}>
191 <img src={TwitchIcon} alt="Twitch" />
192 </a>
193 )}
194 {profile.links.youtube === '-' ? (
195 ''
196 ) : (
197 <a href={profile.links.youtube}>
198 <img src={YouTubeIcon} alt="Youtube" />
199 </a>
200 )}
201 {profile.links.p2sr === '-' ? (
202 ''
203 ) : (
204 <a href={profile.links.p2sr}>
205 <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} />
206 </a>
207 )}
155 </div> 208 </div>
156
157 </div> 209 </div>
158 <div id='profile-bottom'> 210 <div id="profile-bottom">
159 <div> 211 <div>
160 <span>Overall</span> 212 <span>Overall</span>
161 <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} 213 <span>
162 <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> 214 {profile.rankings.overall.rank === 0
215 ? 'N/A '
216 : '#' + profile.rankings.overall.rank + ' '}
217 <span>
218 ({profile.rankings.overall.completion_count}/
219 {profile.rankings.overall.completion_total})
220 </span>
163 </span> 221 </span>
164 </div> 222 </div>
165 <div> 223 <div>
166 <span>Singleplayer</span> 224 <span>Singleplayer</span>
167 <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} 225 <span>
168 <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> 226 {profile.rankings.singleplayer.rank === 0
227 ? 'N/A '
228 : '#' + profile.rankings.singleplayer.rank + ' '}
229 <span>
230 ({profile.rankings.singleplayer.completion_count}/
231 {profile.rankings.singleplayer.completion_total})
232 </span>
169 </span> 233 </span>
170 </div> 234 </div>
171 <div> 235 <div>
172 <span>Cooperative</span> 236 <span>Cooperative</span>
173 <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} 237 <span>
174 <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> 238 {profile.rankings.cooperative.rank === 0
239 ? 'N/A '
240 : '#' + profile.rankings.cooperative.rank + ' '}
241 <span>
242 ({profile.rankings.cooperative.completion_count}/
243 {profile.rankings.cooperative.completion_total})
244 </span>
175 </span> 245 </span>
176 </div> 246 </div>
177 </div> 247 </div>
178 </section> 248 </section>
179 249
180 250 <section id="section2" className="profile">
181 <section id='section2' className='profile'> 251 <button onClick={() => setNavState(0)}>
182 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 252 <img src={FlagIcon} alt="" />
183 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 253 &nbsp;Player Records
254 </button>
255 <button onClick={() => setNavState(1)}>
256 <img src={StatisticsIcon} alt="" />
257 &nbsp;Statistics
258 </button>
184 </section> 259 </section>
185 260
186 261 <section id="section3" className="profile1">
187 262 <div id="profileboard-nav">
188 263 {gameData === null ? (
189 264 <select>error</select>
190 <section id='section3' className='profile1'> 265 ) : (
191 <div id='profileboard-nav'> 266 <select
192 {gameData === null ? <select>error</select> : 267 id="select-game"
193
194 <select id='select-game'
195 onChange={() => { 268 onChange={() => {
196 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 269 setGame(
197 setChapter("0"); 270 (document.querySelector('#select-game') as HTMLInputElement)
198 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 271 .value
272 );
273 setChapter('0');
274 const chapterSelect = document.querySelector(
275 '#select-chapter'
276 ) as HTMLSelectElement;
199 if (chapterSelect) { 277 if (chapterSelect) {
200 chapterSelect.value = "0"; 278 chapterSelect.value = '0';
201 } 279 }
202 }}> 280 }}
203 <option value={0} key={0}>All Scores</option> 281 >
282 <option value={0} key={0}>
283 All Scores
284 </option>
204 {gameData.map((e, i) => ( 285 {gameData.map((e, i) => (
205 <option value={e.id} key={i + 1}>{e.name}</option> 286 <option value={e.id} key={i + 1}>
206 ))}</select> 287 {e.name}
207 } 288 </option>
289 ))}
290 </select>
291 )}
208 292
209 {game === "0" ? 293 {game === '0' ? (
210 <select disabled> 294 <select disabled>
211 <option>All Chapters</option> 295 <option>All Chapters</option>
212 </select> 296 </select>
213 : chapterData === null ? <select></select> : 297 ) : chapterData === null ? (
214 298 <select></select>
215 <select id='select-chapter' 299 ) : (
216 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 300 <select
217 <option value="0" key="0">All Chapters</option> 301 id="select-chapter"
218 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 302 onChange={() =>
219 <option value={e.id} key={i + 1}>{e.name}</option> 303 setChapter(
220 ))}</select> 304 (
221 } 305 document.querySelector(
306 '#select-chapter'
307 ) as HTMLInputElement
308 ).value
309 )
310 }
311 >
312 <option value="0" key="0">
313 All Chapters
314 </option>
315 {chapterData.chapters
316 .filter(e => e.is_disabled === false)
317 .map((e, i) => (
318 <option value={e.id} key={i + 1}>
319 {e.name}
320 </option>
321 ))}
322 </select>
323 )}
222 </div> 324 </div>
223 <div id='profileboard-top'> 325 <div id="profileboard-top">
224 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 326 <span>
225 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 327 <span>Map Name</span>
226 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 328 <img src={SortIcon} alt="" />
227 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 329 </span>
330 <span style={{ justifyContent: 'center' }}>
331 <span>Portals</span>
332 <img src={SortIcon} alt="" />
333 </span>
334 <span style={{ justifyContent: 'center' }}>
335 <span>WRΔ </span>
336 <img src={SortIcon} alt="" />
337 </span>
338 <span style={{ justifyContent: 'center' }}>
339 <span>Time</span>
340 <img src={SortIcon} alt="" />
341 </span>
228 <span> </span> 342 <span> </span>
229 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 343 <span>
230 <span><span>Date</span><img src={SortIcon} alt="" /></span> 344 <span>Rank</span>
231 <div id='page-number'> 345 <img src={SortIcon} alt="" />
346 </span>
347 <span>
348 <span>Date</span>
349 <img src={SortIcon} alt="" />
350 </span>
351 <div id="page-number">
232 <div> 352 <div>
233 <button onClick={() => { 353 <button
234 if (pageNumber !== 1) { 354 onClick={() => {
235 setPageNumber(prevPageNumber => prevPageNumber - 1); 355 if (pageNumber !== 1) {
236 const records = document.querySelectorAll(".profileboard-record"); 356 setPageNumber(prevPageNumber => prevPageNumber - 1);
237 records.forEach((r) => { 357 const records = document.querySelectorAll(
238 (r as HTMLInputElement).style.height = "44px"; 358 '.profileboard-record'
239 }); 359 );
240 } 360 records.forEach(r => {
241 }} 361 (r as HTMLInputElement).style.height = '44px';
242 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 362 });
243 <span>{pageNumber}/{pageMax}</span> 363 }
244 <button onClick={() => { 364 }}
245 if (pageNumber !== pageMax) { 365 >
246 setPageNumber(prevPageNumber => prevPageNumber + 1); 366 <i
247 const records = document.querySelectorAll(".profileboard-record"); 367 className="triangle"
248 records.forEach((r) => { 368 style={{ position: 'relative', left: '-5px' }}
249 (r as HTMLInputElement).style.height = "44px"; 369 ></i>{' '}
250 }); 370 </button>
251 } 371 <span>
252 }} 372 {pageNumber}/{pageMax}
253 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 373 </span>
374 <button
375 onClick={() => {
376 if (pageNumber !== pageMax) {
377 setPageNumber(prevPageNumber => prevPageNumber + 1);
378 const records = document.querySelectorAll(
379 '.profileboard-record'
380 );
381 records.forEach(r => {
382 (r as HTMLInputElement).style.height = '44px';
383 });
384 }
385 }}
386 >
387 <i
388 className="triangle"
389 style={{
390 position: 'relative',
391 left: '5px',
392 transform: 'rotate(180deg)',
393 }}
394 ></i>{' '}
395 </button>
254 </div> 396 </div>
255 </div> 397 </div>
256 </div> 398 </div>
257 <hr /> 399 <hr />
258 <div id='profileboard-records'> 400 <div id="profileboard-records">
259 401 {game === '0' ? (
260 {game === "0" 402 profile.records
261 ? ( 403 .sort((a, b) => a.map_id - b.map_id)
262 404 .map((r, index) =>
263 profile.records.sort((a, b) => a.map_id - b.map_id) 405 Math.ceil((index + 1) / 20) === pageNumber ? (
264 .map((r, index) => ( 406 <button className="profileboard-record" key={index}>
265 407 {r.scores.map((e, i) => (
266 Math.ceil((index + 1) / 20) === pageNumber ? ( 408 <>
267 <button className="profileboard-record" key={index}> 409 {i !== 0 ? (
268 {r.scores.map((e, i) => (<> 410 <hr style={{ gridColumn: '1 / span 8' }} />
269 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 411 ) : (
270 412 ''
271 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 413 )}
272 414
273 <span style={{ display: "grid" }}>{e.score_count}</span> 415 <Link to={`/maps/${r.map_id}`}>
274 416 <span>{r.map_name}</span>
275 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 417 </Link>
276 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 418
277 <span> </span> 419 <span style={{ display: 'grid' }}>
278 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 420 {e.score_count}
279 <span>{e.date.split("T")[0]}</span>
280 <span style={{ flexDirection: "row-reverse" }}>
281
282 <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
283 <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button>
284 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
285 {i === 0 && r.scores.length > 1 ? <button onClick={() => {
286 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
287 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
288 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` :
289 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px"
290 }
291 }><img src={HistoryIcon} alt="history" /></button> : ""}
292
293 </span> 421 </span>
294 </>))}
295
296 </button>
297 ) : ""
298 ))) : maps ?
299 422
300 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 423 <span style={{ display: 'grid' }}>
301 .map((r, index) => { 424 {e.score_count - r.map_wr_count > 0
302 if (Math.ceil((index + 1) / 20) === pageNumber) { 425 ? `+${e.score_count - r.map_wr_count}`
303 let record = profile.records.find((e) => e.map_id === r.id); 426 : `-`}
304 return record === undefined ? ( 427 </span>
305 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 428 <span style={{ display: 'grid' }}>
306 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 429 {ticks_to_time(e.score_time)}
307 <span style={{ display: "grid" }}>N/A</span> 430 </span>
308 <span style={{ display: "grid" }}>N/A</span>
309 <span>N/A</span>
310 <span> </span> 431 <span> </span>
311 <span>N/A</span> 432 {i === 0 ? (
312 <span>N/A</span> 433 <span>#{r.placement}</span>
313 <span style={{ flexDirection: "row-reverse" }}></span> 434 ) : (
314 </button>
315 ) : (
316 <button className="profileboard-record" key={index}>
317 {record.scores.map((e, i) => (<>
318 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
319 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link>
320 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span>
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>
322 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
323 <span> </span> 435 <span> </span>
324 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 436 )}
325 <span>{record!.scores[i].date.split("T")[0]}</span> 437 <span>{e.date.split('T')[0]}</span>
326 <span style={{ flexDirection: "row-reverse" }}> 438 <span style={{ flexDirection: 'row-reverse' }}>
327 439 <button
328 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 440 style={{ marginRight: '10px' }}
329 <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> 441 onClick={() => {
330 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 442 message(
331 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 443 'Demo Information',
332 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 444 `Demo ID: ${e.demo_id}`
333 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 445 );
334 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 446 }}
335 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 447 >
448 <img src={ThreedotIcon} alt="demo_id" />
449 </button>
450 <button
451 onClick={() => {
452 _delete_submission(r.map_id, e.record_id);
453 }}
454 >
455 <img src={DeleteIcon}></img>
456 </button>
457 <button
458 onClick={() =>
459 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
336 } 460 }
337 }><img src={HistoryIcon} alt="history" /></button> : ""} 461 >
338 462 <img src={DownloadIcon} alt="download" />
463 </button>
464 {i === 0 && r.scores.length > 1 ? (
465 <button
466 onClick={() => {
467 (
468 document.querySelectorAll(
469 '.profileboard-record'
470 )[index % 20] as HTMLInputElement
471 ).style.height === '44px' ||
472 (
473 document.querySelectorAll(
474 '.profileboard-record'
475 )[index % 20] as HTMLInputElement
476 ).style.height === ''
477 ? ((
478 document.querySelectorAll(
479 '.profileboard-record'
480 )[index % 20] as HTMLInputElement
481 ).style.height =
482 `${r.scores.length * 46}px`)
483 : ((
484 document.querySelectorAll(
485 '.profileboard-record'
486 )[index % 20] as HTMLInputElement
487 ).style.height = '44px');
488 }}
489 >
490 <img src={HistoryIcon} alt="history" />
491 </button>
492 ) : (
493 ''
494 )}
495 </span>
496 </>
497 ))}
498 </button>
499 ) : (
500 ''
501 )
502 )
503 ) : maps ? (
504 maps
505 .filter(e => e.is_disabled === false)
506 .sort((a, b) => a.id - b.id)
507 .map((r, index) => {
508 if (Math.ceil((index + 1) / 20) === pageNumber) {
509 let record = profile.records.find(e => e.map_id === r.id);
510 return record === undefined ? (
511 <button
512 className="profileboard-record"
513 key={index}
514 style={{ backgroundColor: '#1b1b20' }}
515 >
516 <Link to={`/maps/${r.id}`}>
517 <span>{r.name}</span>
518 </Link>
519 <span style={{ display: 'grid' }}>N/A</span>
520 <span style={{ display: 'grid' }}>N/A</span>
521 <span>N/A</span>
522 <span> </span>
523 <span>N/A</span>
524 <span>N/A</span>
525 <span style={{ flexDirection: 'row-reverse' }}></span>
526 </button>
527 ) : (
528 <button className="profileboard-record" key={index}>
529 {record.scores.map((e, i) => (
530 <>
531 {i !== 0 ? (
532 <hr style={{ gridColumn: '1 / span 8' }} />
533 ) : (
534 ''
535 )}
536 <Link to={`/maps/${r.id}`}>
537 <span>{r.name}</span>
538 </Link>
539 <span style={{ display: 'grid' }}>
540 {record!.scores[i].score_count}
339 </span> 541 </span>
340 </>))} 542 <span style={{ display: 'grid' }}>
341 </button> 543 {record!.scores[i].score_count -
342 544 record!.map_wr_count >
343 ) 545 0
344 } else { return null } 546 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
345 }) : (<>{console.warn(maps)}</>)} 547 : `-`}
548 </span>
549 <span style={{ display: 'grid' }}>
550 {ticks_to_time(record!.scores[i].score_time)}
551 </span>
552 <span> </span>
553 {i === 0 ? (
554 <span>#{record!.placement}</span>
555 ) : (
556 <span> </span>
557 )}
558 <span>{record!.scores[i].date.split('T')[0]}</span>
559 <span style={{ flexDirection: 'row-reverse' }}>
560 <button
561 onClick={() => {
562 message(
563 'Demo Information',
564 `Demo ID: ${e.demo_id}`
565 );
566 }}
567 >
568 <img src={ThreedotIcon} alt="demo_id" />
569 </button>
570 <button
571 onClick={() => {
572 _delete_submission(r.id, e.record_id);
573 }}
574 >
575 <img src={DeleteIcon}></img>
576 </button>
577 <button
578 onClick={() =>
579 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
580 }
581 >
582 <img src={DownloadIcon} alt="download" />
583 </button>
584 {i === 0 && record!.scores.length > 1 ? (
585 <button
586 onClick={() => {
587 (
588 document.querySelectorAll(
589 '.profileboard-record'
590 )[index % 20] as HTMLInputElement
591 ).style.height === '44px' ||
592 (
593 document.querySelectorAll(
594 '.profileboard-record'
595 )[index % 20] as HTMLInputElement
596 ).style.height === ''
597 ? ((
598 document.querySelectorAll(
599 '.profileboard-record'
600 )[index % 20] as HTMLInputElement
601 ).style.height =
602 `${record!.scores.length * 46}px`)
603 : ((
604 document.querySelectorAll(
605 '.profileboard-record'
606 )[index % 20] as HTMLInputElement
607 ).style.height = '44px');
608 }}
609 >
610 <img src={HistoryIcon} alt="history" />
611 </button>
612 ) : (
613 ''
614 )}
615 </span>
616 </>
617 ))}
618 </button>
619 );
620 } else {
621 return null;
622 }
623 })
624 ) : (
625 <>{console.warn(maps)}</>
626 )}
346 </div> 627 </div>
347 </section> 628 </section>
348 </main> 629 </main>
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index 71aa427..885638d 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -1,147 +1,200 @@
1import React, { useEffect } from "react"; 1import 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 {
6import { API } from "@api/Api"; 6 Ranking,
7 SteamRanking,
8 RankingType,
9 SteamRankingType,
10} from '@customTypes/Ranking';
11import { API } from '@api/Api';
7 12
8import "@css/Rankings.css"; 13import '@css/Rankings.css';
9 14
10const Rankings: React.FC = () => { 15const Rankings: React.FC = () => {
11 const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); 16 const [leaderboardData, setLeaderboardData] = React.useState<
12 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); 17 Ranking | SteamRanking
13 enum LeaderboardTypes { 18 >();
14 official, 19 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<
15 unofficial 20 RankingType[] | SteamRankingType[]
21 >();
22 enum LeaderboardTypes {
23 official,
24 unofficial,
25 }
26 const [currentRankingType, setCurrentRankingType] =
27 React.useState<LeaderboardTypes>(LeaderboardTypes.official);
28
29 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false);
30
31 enum RankingCategories {
32 rankings_overall,
33 rankings_multiplayer,
34 rankings_singleplayer,
35 }
36 const [currentLeaderboardType, setCurrentLeaderboardType] =
37 React.useState<RankingCategories>(RankingCategories.rankings_singleplayer);
38 const [load, setLoad] = React.useState<boolean>(false);
39
40 const _fetch_rankings = async () => {
41 setLeaderboardLoad(false);
42 const rankings = await API.get_official_rankings();
43 setLeaderboardData(rankings);
44 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
45 setCurrentLeaderboard(rankings.rankings_singleplayer);
46 } else if (
47 currentLeaderboardType == RankingCategories.rankings_multiplayer
48 ) {
49 setCurrentLeaderboard(rankings.rankings_multiplayer);
50 } else {
51 setCurrentLeaderboard(rankings.rankings_overall);
16 } 52 }
17 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); 53 setLoad(true);
18 54 setLeaderboardLoad(true);
19 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); 55 };
20 56
21 enum RankingCategories { 57 const __dev_fetch_unofficial_rankings = async () => {
22 rankings_overall, 58 try {
23 rankings_multiplayer, 59 setLeaderboardLoad(false);
24 rankings_singleplayer 60 const rankings = await API.get_unofficial_rankings();
25 } 61 setLeaderboardData(rankings);
26 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); 62 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
27 const [load, setLoad] = React.useState<boolean>(false); 63 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
28 64 setCurrentLeaderboard(rankings.rankings_singleplayer);
29 const _fetch_rankings = async () => { 65 } else if (
30 setLeaderboardLoad(false); 66 currentLeaderboardType == RankingCategories.rankings_multiplayer
31 const rankings = await API.get_official_rankings(); 67 ) {
32 setLeaderboardData(rankings); 68 setCurrentLeaderboard(rankings.rankings_multiplayer);
33 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 69 } else {
34 setCurrentLeaderboard(rankings.rankings_singleplayer) 70 setCurrentLeaderboard(rankings.rankings_overall);
35 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 71 }
36 setCurrentLeaderboard(rankings.rankings_multiplayer) 72 setLeaderboardLoad(true);
37 } else { 73 } catch (e) {
38 setCurrentLeaderboard(rankings.rankings_overall) 74 console.log(e);
39 }
40 setLoad(true);
41 setLeaderboardLoad(true);
42 } 75 }
43 76 };
44 const __dev_fetch_unofficial_rankings = async () => { 77
45 try { 78 const _set_current_leaderboard = (ranking_cat: RankingCategories) => {
46 setLeaderboardLoad(false); 79 if (ranking_cat == RankingCategories.rankings_singleplayer) {
47 const rankings = await API.get_unofficial_rankings(); 80 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
48 setLeaderboardData(rankings); 81 } else if (ranking_cat == RankingCategories.rankings_multiplayer) {
49 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 82 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
50 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) 83 } else {
51 setCurrentLeaderboard(rankings.rankings_singleplayer) 84 setCurrentLeaderboard(leaderboardData!.rankings_overall);
52 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) {
53 setCurrentLeaderboard(rankings.rankings_multiplayer)
54 } else {
55 setCurrentLeaderboard(rankings.rankings_overall)
56 }
57 setLeaderboardLoad(true);
58 } catch (e) {
59 console.log(e)
60 }
61 } 85 }
62 86
63 const _set_current_leaderboard = (ranking_cat: RankingCategories) => { 87 setCurrentLeaderboardType(ranking_cat);
64 if (ranking_cat == RankingCategories.rankings_singleplayer) { 88 };
65 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
66 } else if (ranking_cat == RankingCategories.rankings_multiplayer) {
67 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
68 } else {
69 setCurrentLeaderboard(leaderboardData!.rankings_overall);
70 }
71 89
72 setCurrentLeaderboardType(ranking_cat); 90 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => {
91 if (leaderboard_type == LeaderboardTypes.official) {
92 _fetch_rankings();
93 } else {
73 } 94 }
95 };
74 96
75 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { 97 useEffect(() => {
76 if (leaderboard_type == LeaderboardTypes.official) { 98 _fetch_rankings();
77 _fetch_rankings(); 99 if (load) {
78 } else { 100 _set_current_leaderboard(RankingCategories.rankings_singleplayer);
79
80 }
81 } 101 }
82 102 }, [load]);
83 useEffect(() => { 103
84 _fetch_rankings(); 104 return (
85 if (load) { 105 <main>
86 _set_current_leaderboard(RankingCategories.rankings_singleplayer); 106 <Helmet>
87 } 107 <title>LPHUB | Rankings</title>
88 }, [load]) 108 </Helmet>
89 109 <section className="nav-container nav-1">
90 return ( 110 <div>
91 <main> 111 <button
92 <Helmet> 112 onClick={() => {
93 <title>LPHUB | Rankings</title> 113 _fetch_rankings();
94 </Helmet> 114 setCurrentRankingType(LeaderboardTypes.official);
95 <section className="nav-container nav-1"> 115 }}
96 <div> 116 className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? 'selected' : ''}`}
97 <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> 117 >
98 <span>Official (LPHUB)</span> 118 <span>Official (LPHUB)</span>
99 </button> 119 </button>
100 <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> 120 <button
101 <span>Unofficial (Steam)</span> 121 onClick={() => {
102 </button> 122 __dev_fetch_unofficial_rankings();
103 </div> 123 setCurrentRankingType(LeaderboardTypes.unofficial);
104 </section> 124 }}
105 <section className="nav-container nav-2"> 125 className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? 'selected' : ''}`}
106 <div> 126 >
107 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> 127 <span>Unofficial (Steam)</span>
108 <span>Singleplayer</span> 128 </button>
109 </button> 129 </div>
110 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> 130 </section>
111 <span>Cooperative</span> 131 <section className="nav-container nav-2">
112 </button> 132 <div>
113 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> 133 <button
114 <span>Overall</span> 134 onClick={() =>
115 </button> 135 _set_current_leaderboard(RankingCategories.rankings_singleplayer)
116 </div> 136 }
117 </section> 137 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? 'selected' : ''}`}
118 138 >
119 {load ? 139 <span>Singleplayer</span>
120 <section className="rankings-leaderboard"> 140 </button>
121 <div className="ranks-container"> 141 <button
122 <div className="leaderboard-entry header"> 142 onClick={() =>
123 <span>Rank</span> 143 _set_current_leaderboard(RankingCategories.rankings_multiplayer)
124 <span>Player</span> 144 }
125 <span>Portals</span> 145 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? 'selected' : ''}`}
126 </div> 146 >
127 147 <span>Cooperative</span>
128 <div className="splitter"></div> 148 </button>
129 149 <button
130 {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { 150 onClick={() =>
131 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> 151 _set_current_leaderboard(RankingCategories.rankings_overall)
132 }) 152 }
133 } 153 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? 'selected' : ''}`}
134 154 >
135 {leaderboardLoad ? null : 155 <span>Overall</span>
136 <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> 156 </button>
137 <span className="loader"></span> 157 </div>
138 </div> 158 </section>
139 } 159
140 </div> 160 {load ? (
141 </section> 161 <section className="rankings-leaderboard">
142 : null} 162 <div className="ranks-container">
143 </main> 163 <div className="leaderboard-entry header">
144 ) 164 <span>Rank</span>
145} 165 <span>Player</span>
166 <span>Portals</span>
167 </div>
168
169 <div className="splitter"></div>
170
171 {leaderboardLoad &&
172 currentLeaderboard?.map((curRankingData, i) => {
173 return (
174 <RankingEntry
175 currentLeaderboardType={currentLeaderboardType}
176 curRankingData={curRankingData}
177 key={i}
178 ></RankingEntry>
179 );
180 })}
181
182 {leaderboardLoad ? null : (
183 <div
184 style={{
185 display: 'flex',
186 justifyContent: 'center',
187 margin: '30px 0px',
188 }}
189 >
190 <span className="loader"></span>
191 </div>
192 )}
193 </div>
194 </section>
195 ) : null}
196 </main>
197 );
198};
146 199
147export default Rankings; 200export default Rankings;
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 9f57b7e..7a774bc 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -5,37 +5,35 @@ import { Helmet } from 'react-helmet';
5import '@css/Rules.css'; 5import '@css/Rules.css';
6 6
7const Rules: React.FC = () => { 7const Rules: React.FC = () => {
8 const [rulesText, setRulesText] = React.useState<string>('');
8 9
9 const [rulesText, setRulesText] = React.useState<string>(""); 10 React.useEffect(() => {
11 const fetchRules = async () => {
12 try {
13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md'
15 );
16 if (!response.ok) {
17 throw new Error('Failed to fetch README');
18 }
19 const rulesText = await response.text();
20 setRulesText(rulesText);
21 } catch (error) {
22 console.error('Error fetching Rules:', error);
23 }
24 // setRulesText(rulesText)
25 };
26 fetchRules();
27 }, []);
10 28
11 React.useEffect(() => { 29 return (
12 const fetchRules = async () => { 30 <main>
13 try { 31 <Helmet>
14 const response = await fetch( 32 <title>LPHUB | Rules</title>
15 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' 33 </Helmet>
16 ); 34 <ReactMarkdown>{rulesText}</ReactMarkdown>
17 if (!response.ok) { 35 </main>
18 throw new Error('Failed to fetch README'); 36 );
19 }
20 const rulesText = await response.text();
21 setRulesText(rulesText);
22 } catch (error) {
23 console.error('Error fetching Rules:', error);
24 }
25 // setRulesText(rulesText)
26 };
27 fetchRules();
28 }, []);
29
30
31 return (
32 <main>
33 <Helmet>
34 <title>LPHUB | Rules</title>
35 </Helmet>
36 <ReactMarkdown>{rulesText}</ReactMarkdown>
37 </main>
38 );
39}; 37};
40 38
41export default Rules; 39export default Rules;
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index d43c0c6..b6dfbd3 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -2,13 +2,24 @@ import 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 {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16} from '@images/Images';
6import { UserProfile } from '@customTypes/Profile'; 17import { UserProfile } from '@customTypes/Profile';
7import { Game, GameChapters } from '@customTypes/Game'; 18import { Game, GameChapters } from '@customTypes/Game';
8import { Map } from '@customTypes/Map'; 19import { Map } from '@customTypes/Map';
9import { API } from '@api/Api'; 20import { API } from '@api/Api';
10import { ticks_to_time } from '@utils/Time'; 21import { ticks_to_time } from '@utils/Time';
11import "@css/Profile.css"; 22import '@css/Profile.css';
12import useMessage from '@hooks/UseMessage'; 23import useMessage from '@hooks/UseMessage';
13 24
14interface UserProps { 25interface UserProps {
@@ -18,7 +29,6 @@ interface UserProps {
18} 29}
19 30
20const User: React.FC<UserProps> = ({ token, profile, gameData }) => { 31const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
21
22 const { message, MessageDialogComponent } = useMessage(); 32 const { message, MessageDialogComponent } = useMessage();
23 33
24 const [user, setUser] = React.useState<UserProfile | undefined>(undefined); 34 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
@@ -27,18 +37,20 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
27 const [pageNumber, setPageNumber] = React.useState(1); 37 const [pageNumber, setPageNumber] = React.useState(1);
28 const [pageMax, setPageMax] = React.useState(0); 38 const [pageMax, setPageMax] = React.useState(0);
29 39
30 const [game, setGame] = React.useState("0"); 40 const [game, setGame] = React.useState('0');
31 const [chapter, setChapter] = React.useState("0"); 41 const [chapter, setChapter] = React.useState('0');
32 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 42 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
43 null
44 );
33 const [maps, setMaps] = React.useState<Map[]>([]); 45 const [maps, setMaps] = React.useState<Map[]>([]);
34 46
35 const location = useLocation(); 47 const location = useLocation();
36 const navigate = useNavigate(); 48 const navigate = useNavigate();
37 49
38 const _fetch_user = async () => { 50 const _fetch_user = async () => {
39 const userID = location.pathname.split("/")[2]; 51 const userID = location.pathname.split('/')[2];
40 if (token && profile && profile.profile && profile.steam_id === userID) { 52 if (token && profile && profile.profile && profile.steam_id === userID) {
41 navigate("/profile"); 53 navigate('/profile');
42 return; 54 return;
43 } 55 }
44 const userData = await API.get_user(userID); 56 const userData = await API.get_user(userID);
@@ -46,7 +58,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
46 }; 58 };
47 59
48 const _get_game_chapters = async () => { 60 const _get_game_chapters = async () => {
49 if (game !== "0") { 61 if (game !== '0') {
50 const gameChapters = await API.get_games_chapters(game); 62 const gameChapters = await API.get_games_chapters(game);
51 setChapterData(gameChapters); 63 setChapterData(gameChapters);
52 } else { 64 } else {
@@ -56,7 +68,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
56 }; 68 };
57 69
58 const _get_game_maps = async () => { 70 const _get_game_maps = async () => {
59 if (chapter === "0") { 71 if (chapter === '0') {
60 const gameMaps = await API.get_game_maps(game); 72 const gameMaps = await API.get_game_maps(game);
61 setMaps(gameMaps); 73 setMaps(gameMaps);
62 setPageMax(Math.ceil(gameMaps.length / 20)); 74 setPageMax(Math.ceil(gameMaps.length / 20));
@@ -80,16 +92,14 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
80 }, [user, game, location]); 92 }, [user, game, location]);
81 93
82 React.useEffect(() => { 94 React.useEffect(() => {
83 if (user && game !== "0") { 95 if (user && game !== '0') {
84 _get_game_maps(); 96 _get_game_maps();
85 } 97 }
86 }, [user, game, chapter, location]) 98 }, [user, game, chapter, location]);
87 99
88 if (!user) { 100 if (!user) {
89 return ( 101 return <></>;
90 <></> 102 }
91 );
92 };
93 103
94 return ( 104 return (
95 <main> 105 <main>
@@ -98,218 +108,461 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
98 <meta name="description" content={user.user_name} /> 108 <meta name="description" content={user.user_name} />
99 </Helmet> 109 </Helmet>
100 {MessageDialogComponent} 110 {MessageDialogComponent}
101 <section id='section1' className='profile'> 111 <section id="section1" className="profile">
102 <div> 112 <div>
103 <img src={user.avatar_link} alt="profile-image"></img> 113 <img src={user.avatar_link} alt="profile-image"></img>
104 </div> 114 </div>
105 <div id='profile-top'> 115 <div id="profile-top">
106 <div> 116 <div>
107 <div>{user.user_name}</div> 117 <div>{user.user_name}</div>
108 <div> 118 <div>
109 {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} 119 {user.country_code === 'XX' ? (
120 ''
121 ) : (
122 <img
123 src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`}
124 alt={user.country_code}
125 />
126 )}
110 </div> 127 </div>
111 <div> 128 <div>
112 {user.titles.map(e => ( 129 {user.titles.map(e => (
113 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 130 <span
131 className="titles"
132 style={{ backgroundColor: `#${e.color}` }}
133 >
114 {e.name} 134 {e.name}
115 </span> 135 </span>
116 ))} 136 ))}
117 </div> 137 </div>
118 </div> 138 </div>
119 <div> 139 <div>
120 {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 140 {user.links.steam === '-' ? (
121 {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 141 ''
122 {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 142 ) : (
123 {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 143 <a href={user.links.steam}>
144 <img src={SteamIcon} alt="Steam" />
145 </a>
146 )}
147 {user.links.twitch === '-' ? (
148 ''
149 ) : (
150 <a href={user.links.twitch}>
151 <img src={TwitchIcon} alt="Twitch" />
152 </a>
153 )}
154 {user.links.youtube === '-' ? (
155 ''
156 ) : (
157 <a href={user.links.youtube}>
158 <img src={YouTubeIcon} alt="Youtube" />
159 </a>
160 )}
161 {user.links.p2sr === '-' ? (
162 ''
163 ) : (
164 <a href={user.links.p2sr}>
165 <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} />
166 </a>
167 )}
124 </div> 168 </div>
125
126 </div> 169 </div>
127 <div id='profile-bottom'> 170 <div id="profile-bottom">
128 <div> 171 <div>
129 <span>Overall</span> 172 <span>Overall</span>
130 <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} 173 <span>
131 <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> 174 {user.rankings.overall.rank === 0
175 ? 'N/A '
176 : '#' + user.rankings.overall.rank + ' '}
177 <span>
178 ({user.rankings.overall.completion_count}/
179 {user.rankings.overall.completion_total})
180 </span>
132 </span> 181 </span>
133 </div> 182 </div>
134 <div> 183 <div>
135 <span>Singleplayer</span> 184 <span>Singleplayer</span>
136 <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} 185 <span>
137 <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> 186 {user.rankings.singleplayer.rank === 0
187 ? 'N/A '
188 : '#' + user.rankings.singleplayer.rank + ' '}
189 <span>
190 ({user.rankings.singleplayer.completion_count}/
191 {user.rankings.singleplayer.completion_total})
192 </span>
138 </span> 193 </span>
139 </div> 194 </div>
140 <div> 195 <div>
141 <span>Cooperative</span> 196 <span>Cooperative</span>
142 <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} 197 <span>
143 <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> 198 {user.rankings.cooperative.rank === 0
199 ? 'N/A '
200 : '#' + user.rankings.cooperative.rank + ' '}
201 <span>
202 ({user.rankings.cooperative.completion_count}/
203 {user.rankings.cooperative.completion_total})
204 </span>
144 </span> 205 </span>
145 </div> 206 </div>
146 </div> 207 </div>
147 </section> 208 </section>
148 209
149 210 <section id="section2" className="profile">
150 <section id='section2' className='profile'> 211 <button onClick={() => setNavState(0)}>
151 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 212 <img src={FlagIcon} alt="" />
152 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 213 &nbsp;Player Records
214 </button>
215 <button onClick={() => setNavState(1)}>
216 <img src={StatisticsIcon} alt="" />
217 &nbsp;Statistics
218 </button>
153 </section> 219 </section>
154 220
155 221 <section id="section3" className="profile1">
156 222 <div id="profileboard-nav">
157 223 {gameData === null ? (
158 224 <select>error</select>
159 <section id='section3' className='profile1'> 225 ) : (
160 <div id='profileboard-nav'> 226 <select
161 {gameData === null ? <select>error</select> : 227 id="select-game"
162
163 <select id='select-game'
164 onChange={() => { 228 onChange={() => {
165 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 229 setGame(
166 setChapter("0"); 230 (document.querySelector('#select-game') as HTMLInputElement)
167 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 231 .value
232 );
233 setChapter('0');
234 const chapterSelect = document.querySelector(
235 '#select-chapter'
236 ) as HTMLSelectElement;
168 if (chapterSelect) { 237 if (chapterSelect) {
169 chapterSelect.value = "0"; 238 chapterSelect.value = '0';
170 } 239 }
171 }}> 240 }}
172 <option value={0} key={0}>All Scores</option> 241 >
242 <option value={0} key={0}>
243 All Scores
244 </option>
173 {gameData.map((e, i) => ( 245 {gameData.map((e, i) => (
174 <option value={e.id} key={i + 1}>{e.name}</option> 246 <option value={e.id} key={i + 1}>
175 ))}</select> 247 {e.name}
176 } 248 </option>
249 ))}
250 </select>
251 )}
177 252
178 {game === "0" ? 253 {game === '0' ? (
179 <select disabled> 254 <select disabled>
180 <option>All Chapters</option> 255 <option>All Chapters</option>
181 </select> 256 </select>
182 : chapterData === null ? <select></select> : 257 ) : chapterData === null ? (
183 258 <select></select>
184 <select id='select-chapter' 259 ) : (
185 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 260 <select
186 <option value="0" key="0">All Chapters</option> 261 id="select-chapter"
187 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 262 onChange={() =>
188 <option value={e.id} key={i + 1}>{e.name}</option> 263 setChapter(
189 ))}</select> 264 (
190 } 265 document.querySelector(
266 '#select-chapter'
267 ) as HTMLInputElement
268 ).value
269 )
270 }
271 >
272 <option value="0" key="0">
273 All Chapters
274 </option>
275 {chapterData.chapters
276 .filter(e => e.is_disabled === false)
277 .map((e, i) => (
278 <option value={e.id} key={i + 1}>
279 {e.name}
280 </option>
281 ))}
282 </select>
283 )}
191 </div> 284 </div>
192 <div id='profileboard-top'> 285 <div id="profileboard-top">
193 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 286 <span>
194 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 287 <span>Map Name</span>
195 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 288 <img src={SortIcon} alt="" />
196 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 289 </span>
290 <span style={{ justifyContent: 'center' }}>
291 <span>Portals</span>
292 <img src={SortIcon} alt="" />
293 </span>
294 <span style={{ justifyContent: 'center' }}>
295 <span>WRΔ </span>
296 <img src={SortIcon} alt="" />
297 </span>
298 <span style={{ justifyContent: 'center' }}>
299 <span>Time</span>
300 <img src={SortIcon} alt="" />
301 </span>
197 <span> </span> 302 <span> </span>
198 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 303 <span>
199 <span><span>Date</span><img src={SortIcon} alt="" /></span> 304 <span>Rank</span>
200 <div id='page-number'> 305 <img src={SortIcon} alt="" />
306 </span>
307 <span>
308 <span>Date</span>
309 <img src={SortIcon} alt="" />
310 </span>
311 <div id="page-number">
201 <div> 312 <div>
202 <button onClick={() => { 313 <button
203 if (pageNumber !== 1) { 314 onClick={() => {
204 setPageNumber(prevPageNumber => prevPageNumber - 1); 315 if (pageNumber !== 1) {
205 const records = document.querySelectorAll(".profileboard-record"); 316 setPageNumber(prevPageNumber => prevPageNumber - 1);
206 records.forEach((r) => { 317 const records = document.querySelectorAll(
207 (r as HTMLInputElement).style.height = "44px"; 318 '.profileboard-record'
208 }); 319 );
209 } 320 records.forEach(r => {
210 }} 321 (r as HTMLInputElement).style.height = '44px';
211 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 322 });
212 <span>{pageNumber}/{pageMax}</span> 323 }
213 <button onClick={() => { 324 }}
214 if (pageNumber !== pageMax) { 325 >
215 setPageNumber(prevPageNumber => prevPageNumber + 1); 326 <i
216 const records = document.querySelectorAll(".profileboard-record"); 327 className="triangle"
217 records.forEach((r) => { 328 style={{ position: 'relative', left: '-5px' }}
218 (r as HTMLInputElement).style.height = "44px"; 329 ></i>{' '}
219 }); 330 </button>
220 } 331 <span>
221 }} 332 {pageNumber}/{pageMax}
222 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 333 </span>
334 <button
335 onClick={() => {
336 if (pageNumber !== pageMax) {
337 setPageNumber(prevPageNumber => prevPageNumber + 1);
338 const records = document.querySelectorAll(
339 '.profileboard-record'
340 );
341 records.forEach(r => {
342 (r as HTMLInputElement).style.height = '44px';
343 });
344 }
345 }}
346 >
347 <i
348 className="triangle"
349 style={{
350 position: 'relative',
351 left: '5px',
352 transform: 'rotate(180deg)',
353 }}
354 ></i>{' '}
355 </button>
223 </div> 356 </div>
224 </div> 357 </div>
225 </div> 358 </div>
226 <hr /> 359 <hr />
227 <div id='profileboard-records'> 360 <div id="profileboard-records">
228 361 {game === '0' ? (
229 {game === "0" 362 user.records
230 ? ( 363 .sort((a, b) => a.map_id - b.map_id)
231 364 .map((r, index) =>
232 user.records.sort((a, b) => a.map_id - b.map_id) 365 Math.ceil((index + 1) / 20) === pageNumber ? (
233 .map((r, index) => ( 366 <button className="profileboard-record" key={index}>
234 367 {r.scores.map((e, i) => (
235 Math.ceil((index + 1) / 20) === pageNumber ? ( 368 <>
236 <button className="profileboard-record" key={index}> 369 {i !== 0 ? (
237 {r.scores.map((e, i) => (<> 370 <hr style={{ gridColumn: '1 / span 8' }} />
238 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 371 ) : (
239 372 ''
240 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 373 )}
241 374
242 <span style={{ display: "grid" }}>{e.score_count}</span> 375 <Link to={`/maps/${r.map_id}`}>
243 376 <span>{r.map_name}</span>
244 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 377 </Link>
245 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 378
379 <span style={{ display: 'grid' }}>{e.score_count}</span>
380
381 <span style={{ display: 'grid' }}>
382 {e.score_count - r.map_wr_count > 0
383 ? `+${e.score_count - r.map_wr_count}`
384 : `-`}
385 </span>
386 <span style={{ display: 'grid' }}>
387 {ticks_to_time(e.score_time)}
388 </span>
246 <span> </span> 389 <span> </span>
247 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 390 {i === 0 ? <span>#{r.placement}</span> : <span> </span>}
248 <span>{e.date.split("T")[0]}</span> 391 <span>{e.date.split('T')[0]}</span>
249 <span style={{ flexDirection: "row-reverse" }}> 392 <span style={{ flexDirection: 'row-reverse' }}>
250 393 <button
251 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 394 onClick={() => {
252 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 395 message(
253 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 396 'Demo Information',
254 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 397 `Demo ID: ${e.demo_id}`
255 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 398 );
256 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 399 }}
257 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 400 >
258 } 401 <img src={ThreedotIcon} alt="demo_id" />
259 }><img src={HistoryIcon} alt="history" /></button> : ""} 402 </button>
260 403 <button
404 onClick={() =>
405 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
406 }
407 >
408 <img src={DownloadIcon} alt="download" />
409 </button>
410 {i === 0 && r.scores.length > 1 ? (
411 <button
412 onClick={() => {
413 (
414 document.querySelectorAll(
415 '.profileboard-record'
416 )[index % 20] as HTMLInputElement
417 ).style.height === '44px' ||
418 (
419 document.querySelectorAll(
420 '.profileboard-record'
421 )[index % 20] as HTMLInputElement
422 ).style.height === ''
423 ? ((
424 document.querySelectorAll(
425 '.profileboard-record'
426 )[index % 20] as HTMLInputElement
427 ).style.height =
428 `${r.scores.length * 46}px`)
429 : ((
430 document.querySelectorAll(
431 '.profileboard-record'
432 )[index % 20] as HTMLInputElement
433 ).style.height = '44px');
434 }}
435 >
436 <img src={HistoryIcon} alt="history" />
437 </button>
438 ) : (
439 ''
440 )}
261 </span> 441 </span>
262 </>))} 442 </>
263 443 ))}
444 </button>
445 ) : (
446 ''
447 )
448 )
449 ) : maps ? (
450 maps
451 .filter(e => e.is_disabled === false)
452 .sort((a, b) => a.id - b.id)
453 .map((r, index) => {
454 if (Math.ceil((index + 1) / 20) === pageNumber) {
455 let record = user.records.find(e => e.map_id === r.id);
456 return record === undefined ? (
457 <button
458 className="profileboard-record"
459 key={index}
460 style={{ backgroundColor: '#1b1b20' }}
461 >
462 <Link to={`/maps/${r.id}`}>
463 <span>{r.name}</span>
464 </Link>
465 <span style={{ display: 'grid' }}>N/A</span>
466 <span style={{ display: 'grid' }}>N/A</span>
467 <span>N/A</span>
468 <span> </span>
469 <span>N/A</span>
470 <span>N/A</span>
471 <span style={{ flexDirection: 'row-reverse' }}></span>
264 </button> 472 </button>
265 ) : "" 473 ) : (
266 ))) : maps ? 474 <button className="profileboard-record" key={index}>
267 475 {record.scores.map((e, i) => (
268 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 476 <>
269 .map((r, index) => { 477 {i !== 0 ? (
270 if (Math.ceil((index + 1) / 20) === pageNumber) { 478 <hr style={{ gridColumn: '1 / span 8' }} />
271 let record = user.records.find((e) => e.map_id === r.id); 479 ) : (
272 return record === undefined ? ( 480 ''
273 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 481 )}
274 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 482 <Link to={`/maps/${r.id}`}>
275 <span style={{ display: "grid" }}>N/A</span> 483 <span>{r.name}</span>
276 <span style={{ display: "grid" }}>N/A</span> 484 </Link>
277 <span>N/A</span> 485 <span style={{ display: 'grid' }}>
278 <span> </span> 486 {record!.scores[i].score_count}
279 <span>N/A</span> 487 </span>
280 <span>N/A</span> 488 <span style={{ display: 'grid' }}>
281 <span style={{ flexDirection: "row-reverse" }}></span> 489 {record!.scores[i].score_count -
282 </button> 490 record!.map_wr_count >
283 ) : ( 491 0
284 <button className="profileboard-record" key={index}> 492 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
285 {record.scores.map((e, i) => (<> 493 : `-`}
286 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 494 </span>
287 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 495 <span style={{ display: 'grid' }}>
288 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 496 {ticks_to_time(record!.scores[i].score_time)}
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> 497 </span>
290 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
291 <span> </span> 498 <span> </span>
292 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 499 {i === 0 ? (
293 <span>{record!.scores[i].date.split("T")[0]}</span> 500 <span>#{record!.placement}</span>
294 <span style={{ flexDirection: "row-reverse" }}> 501 ) : (
295 502 <span> </span>
296 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 503 )}
297 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 504 <span>{record!.scores[i].date.split('T')[0]}</span>
298 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 505 <span style={{ flexDirection: 'row-reverse' }}>
299 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 506 <button
300 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 507 onClick={() => {
301 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 508 message(
302 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 509 'Demo Information',
303 } 510 `Demo ID: ${e.demo_id}`
304 }><img src={HistoryIcon} alt="history" /></button> : ""} 511 );
305 512 }}
513 >
514 <img src={ThreedotIcon} alt="demo_id" />
515 </button>
516 <button
517 onClick={() =>
518 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
519 }
520 >
521 <img src={DownloadIcon} alt="download" />
522 </button>
523 {i === 0 && record!.scores.length > 1 ? (
524 <button
525 onClick={() => {
526 (
527 document.querySelectorAll(
528 '.profileboard-record'
529 )[index % 20] as HTMLInputElement
530 ).style.height === '44px' ||
531 (
532 document.querySelectorAll(
533 '.profileboard-record'
534 )[index % 20] as HTMLInputElement
535 ).style.height === ''
536 ? ((
537 document.querySelectorAll(
538 '.profileboard-record'
539 )[index % 20] as HTMLInputElement
540 ).style.height =
541 `${record!.scores.length * 46}px`)
542 : ((
543 document.querySelectorAll(
544 '.profileboard-record'
545 )[index % 20] as HTMLInputElement
546 ).style.height = '44px');
547 }}
548 >
549 <img src={HistoryIcon} alt="history" />
550 </button>
551 ) : (
552 ''
553 )}
306 </span> 554 </span>
307 </>))} 555 </>
308 </button> 556 ))}
309 557 </button>
310 ) 558 );
311 } else { return null } 559 } else {
312 }) : (<>{console.warn(maps)}</>)} 560 return null;
561 }
562 })
563 ) : (
564 <>{console.warn(maps)}</>
565 )}
313 </div> 566 </div>
314 </section> 567 </section>
315 </main> 568 </main>