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