aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.css233
-rw-r--r--frontend/src/App.tsx109
-rw-r--r--frontend/src/api/Api.ts90
-rw-r--r--frontend/src/api/Auth.ts2
-rw-r--r--frontend/src/api/Games.ts16
-rw-r--r--frontend/src/api/Maps.ts158
-rw-r--r--frontend/src/api/Mod.ts98
-rw-r--r--frontend/src/api/User.ts18
-rw-r--r--frontend/src/components/ConfirmDialog.tsx51
-rw-r--r--frontend/src/components/Discussions.tsx327
-rw-r--r--frontend/src/components/GameCategory.tsx32
-rw-r--r--frontend/src/components/GameEntry.tsx38
-rw-r--r--frontend/src/components/Leaderboards.tsx295
-rw-r--r--frontend/src/components/Login.tsx128
-rw-r--r--frontend/src/components/MapEntry.tsx10
-rw-r--r--frontend/src/components/MessageDialog.tsx48
-rw-r--r--frontend/src/components/MessageDialogLoad.tsx46
-rw-r--r--frontend/src/components/ModMenu.tsx461
-rw-r--r--frontend/src/components/RankingEntry.tsx93
-rw-r--r--frontend/src/components/Sidebar.tsx381
-rw-r--r--frontend/src/components/Summary.tsx300
-rw-r--r--frontend/src/components/UploadRunDialog.tsx375
-rw-r--r--frontend/src/hooks/UseConfirm.tsx65
-rw-r--r--frontend/src/hooks/UseMessage.tsx60
-rw-r--r--frontend/src/hooks/UseMessageLoad.tsx55
-rw-r--r--frontend/src/images/Images.tsx48
-rw-r--r--frontend/src/images/svgs/steam.tsx7
-rw-r--r--frontend/src/index.tsx8
-rw-r--r--frontend/src/pages/About.tsx62
-rw-r--r--frontend/src/pages/Games.tsx55
-rw-r--r--frontend/src/pages/Homepage.tsx43
-rw-r--r--frontend/src/pages/Maplist.tsx234
-rw-r--r--frontend/src/pages/Maps.tsx166
-rw-r--r--frontend/src/pages/Profile.tsx672
-rw-r--r--frontend/src/pages/Rankings.tsx320
-rw-r--r--frontend/src/pages/Rules.tsx64
-rw-r--r--frontend/src/pages/User.tsx525
-rw-r--r--frontend/src/types/Chapters.ts18
-rw-r--r--frontend/src/types/Content.ts8
-rw-r--r--frontend/src/types/Game.ts15
-rw-r--r--frontend/src/types/Map.ts36
-rw-r--r--frontend/src/types/MapNames.ts220
-rw-r--r--frontend/src/types/Pagination.ts2
-rw-r--r--frontend/src/types/Profile.ts19
-rw-r--r--frontend/src/types/Ranking.ts38
-rw-r--r--frontend/src/types/Search.ts4
-rw-r--r--frontend/src/utils/Jwt.ts32
-rw-r--r--frontend/src/utils/Time.ts64
48 files changed, 3918 insertions, 2231 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 14a9972..464b759 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,42 +1,76 @@
1main { 1@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Montserrat+Alternates:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 overflow: auto; 2@import "tailwindcss";
3 overflow-x: hidden;
4 position: relative;
5 3
6 width: calc(100% - 380px); 4@theme {
7 height: 100vh; 5 --color-rosewater: #f2d5cf;
8 left: 350px; 6 --color-flamingo: #eebebe;
7 --color-pink: #f4b8e4;
8 --color-mauve: #ca9ee6;
9 --color-red: #e78284;
10 --color-maroon: #ea999c;
11 --color-peach: #ef9f76;
12 --color-yellow: #e5c890;
13 --color-green: #a6d189;
14 --color-teal: #81c8be;
15 --color-sky: #99d1db;
16 --color-sapphire: #85c1dc;
17 --color-blue: #8caaee;
18 --color-lavender: #babbf1;
19 --color-text: #c6d0f5;
20 --color-subtext1: #b5bfe2;
21 --color-subtext0: #a5adce;
22 --color-overlay2: #949cbb;
23 --color-overlay1: #838ba7;
24 --color-overlay0: #737994;
25 --color-surface2: #626880;
26 --color-surface1: #51576d;
27 --color-surface0: #414559;
28 --color-base: #303446;
29 --color-mantle: #292c3c;
30 --color-crust: #232634;
9 31
10 padding-right: 30px; 32 --color-primary: var(--color-mauve);
11 33 --color-secondary: var(--color-blue);
12 font-size: 40px; 34 --color-accent: var(--color-peach);
13 font-family: BarlowSemiCondensed-Regular; 35 --color-background: var(--color-base);
14 color: #cdcfdf; 36 --color-surface: var(--color-surface0);
37 --color-muted: var(--color-overlay0);
38 --color-border: var(--color-surface2);
39 --color-input: var(--color-surface1);
40 --color-foreground: var(--color-text);
41 --color-success: var(--color-green);
42 --color-warning: var(--color-yellow);
43 --color-error: var(--color-red);
44 --color-info: var(--color-blue);
15 45
46 --font-barlow-condensed-regular: 'BarlowCondensed-Regular';
47 --font-barlow-condensed-bold: 'BarlowCondensed-Bold';
48 --font-barlow-semicondensed-regular: 'BarlowSemiCondensed-Regular';
49 --font-barlow-semicondensed-semibold: 'BarlowSemiCondensed-SemiBold';
16} 50}
17 51
52
18a { 53a {
19 color: inherit; 54 color: inherit;
20 width: fit-content;
21} 55}
22 56
23body { 57body {
24 overflow: hidden; 58 overflow: hidden;
25 background-color: #141520; 59 background-color: var(--color-crust);
26 margin: 0; 60 margin: 0;
27} 61}
28 62
29.loader { 63.loader {
30 animation: loader 1.2s ease infinite; 64 animation: loader 1.2s ease infinite;
31 background-size: 400% 300%; 65 background-size: 400% 300%;
32 background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); 66 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
33 user-select: none; 67 user-select: none;
34} 68}
35 69
36.loader-text { 70.loader-text {
37 animation: loader 1.2s ease infinite; 71 animation: loader 1.2s ease infinite;
38 background-size: 400% 300%; 72 background-size: 400% 300%;
39 background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); 73 background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%);
40 user-select: none; 74 user-select: none;
41 color: #00000000; 75 color: #00000000;
42 border-radius: 1000px; 76 border-radius: 1000px;
@@ -76,6 +110,173 @@ body {
76 } 110 }
77} 111}
78 112
113/* Custom Tailwind utilities for Catppuccin Frappe theme */
114@layer utilities {
115 .bg-primary {
116 background-color: var(--color-primary);
117 }
118
119 .bg-secondary {
120 background-color: var(--color-secondary);
121 }
122
123 .bg-accent {
124 background-color: var(--color-accent);
125 }
126
127 .bg-background {
128 background-color: var(--color-background);
129 }
130
131 .bg-surface {
132 background-color: var(--color-surface);
133 }
134
135 .bg-muted {
136 background-color: var(--color-muted);
137 }
138
139 .text-primary {
140 color: var(--color-primary);
141 }
142
143 .text-secondary {
144 color: var(--color-secondary);
145 }
146
147 .text-accent {
148 color: var(--color-accent);
149 }
150
151 .text-foreground {
152 color: var(--color-foreground);
153 }
154
155 .text-muted {
156 color: var(--color-muted);
157 }
158
159 .border-primary {
160 border-color: var(--color-primary);
161 }
162
163 .border-secondary {
164 border-color: var(--color-secondary);
165 }
166
167 .border-muted {
168 border-color: var(--color-border);
169 }
170
171 .hover\:bg-primary:hover {
172 background-color: var(--color-primary);
173 }
174
175 .hover\:bg-secondary:hover {
176 background-color: var(--color-secondary);
177 }
178
179 .hover\:bg-surface:hover {
180 background-color: var(--color-surface);
181 }
182
183 .hover\:text-primary:hover {
184 color: var(--color-primary);
185 }
186
187 .focus\:ring-primary:focus {
188 --tw-ring-color: var(--color-primary);
189 }
190
191 .triangle {
192 width: 0;
193 height: 0;
194 border-left: 5px solid transparent;
195 border-right: 5px solid transparent;
196 border-bottom: 8px solid var(--color-foreground);
197 display: inline-block;
198 }
199
200 .sidebar-button-selected {
201 background-color: var(--color-primary) !important;
202 color: var(--color-background) !important;
203 }
204
205 .sidebar-button-deselected {
206 background-color: var(--color-surface) !important;
207 color: var(--color-foreground) !important;
208 }
209
210 .profileboard-record {
211 background-color: var(--color-surface);
212 border: 1px solid var(--color-border);
213 border-radius: 0.5rem;
214 padding: 0.75rem;
215 margin-bottom: 0.5rem;
216 transition: all 0.2s ease;
217 }
218
219 .profileboard-record:hover {
220 background-color: var(--color-surface1);
221 }
222
223 .difficulty-rating {
224 width: 20px;
225 height: 20px;
226 background-color: var(--color-muted);
227 border-radius: 50%;
228 margin: 0 2px;
229 display: inline-block;
230 }
231
232 .nav-button {
233 background-color: var(--color-surface);
234 color: var(--color-foreground);
235 border: 1px solid var(--color-border);
236 border-radius: 0.5rem;
237 padding: 0.5rem 1rem;
238 transition: all 0.2s ease;
239 display: inline-flex;
240 align-items: center;
241 gap: 0.5rem;
242 text-decoration: none;
243 }
244
245 .nav-button:hover {
246 background-color: var(--color-surface1);
247 }
248
249 .record {
250 background-color: var(--color-surface);
251 border: 1px solid var(--color-border);
252 border-radius: 0.5rem;
253 padding: 0.5rem;
254 margin: 0.25rem;
255 cursor: pointer;
256 transition: all 0.2s ease;
257 }
258
259 .record:hover {
260 background-color: var(--color-surface1);
261 }
262
263 .portal-count {
264 font-size: 3rem;
265 font-weight: bold;
266 color: var(--color-primary);
267 }
268
269 .titles {
270 background-color: var(--color-accent);
271 color: var(--color-background);
272 padding: 0.25rem 0.5rem;
273 border-radius: 1rem;
274 font-size: 0.875rem;
275 margin-right: 0.5rem;
276 display: inline-block;
277 }
278}
279
79@font-face { 280@font-face {
80 font-family: 'BarlowCondensed-Bold'; 281 font-family: 'BarlowCondensed-Bold';
81 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); 282 src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype');
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bdd3adc..fbfa59f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,32 +1,32 @@
1import React from 'react'; 1import React, { useCallback } from "react";
2import { Routes, Route } from "react-router-dom"; 2import { Routes, Route } from "react-router-dom";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import { UserProfile } from '@customTypes/Profile'; 5import { UserProfile } from "@customTypes/Profile";
6import Sidebar from './components/Sidebar'; 6import Sidebar from "./components/Sidebar";
7import "./App.css"; 7import "./App.css";
8 8
9import Profile from '@pages/Profile'; 9import Profile from "@pages/Profile";
10import Games from '@pages/Games'; 10import Games from "@pages/Games";
11import Maps from '@pages/Maps'; 11import Maps from "@pages/Maps";
12import User from '@pages/User'; 12import User from "@pages/User";
13import Homepage from '@pages/Homepage'; 13import Homepage from "@pages/Homepage";
14import UploadRunDialog from './components/UploadRunDialog'; 14import UploadRunDialog from "./components/UploadRunDialog";
15import Rules from '@pages/Rules'; 15import Rules from "@pages/Rules";
16import About from '@pages/About'; 16import About from "@pages/About";
17import { Game } from '@customTypes/Game'; 17import { Game } from "@customTypes/Game";
18import { API } from './api/Api'; 18import { API } from "./api/Api";
19import Maplist from '@pages/Maplist'; 19import Maplist from "@pages/Maplist";
20import Rankings from '@pages/Rankings'; 20import Rankings from "@pages/Rankings";
21import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt'; 21import { get_user_id_from_token, get_user_mod_from_token } from "./utils/Jwt";
22 22
23const App: React.FC = () => { 23const App: React.FC = () => {
24 const [token, setToken] = React.useState<string | undefined>(undefined); 24 const [token, setToken] = React.useState<string | undefined>(undefined);
25 const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); 25 const [profile, setProfile] = React.useState<UserProfile | undefined>(
26 undefined
27 );
26 const [isModerator, setIsModerator] = React.useState<boolean>(false); 28 const [isModerator, setIsModerator] = React.useState<boolean>(false);
27
28 const [games, setGames] = React.useState<Game[]>([]); 29 const [games, setGames] = React.useState<Game[]>([]);
29
30 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false); 30 const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false);
31 31
32 const _fetch_token = async () => { 32 const _fetch_token = async () => {
@@ -39,12 +39,15 @@ const App: React.FC = () => {
39 setGames(games); 39 setGames(games);
40 }; 40 };
41 41
42 const _set_profile = async (user_id?: string) => { 42 const _set_profile = useCallback(
43 if (user_id && token) { 43 async (user_id?: string) => {
44 const user = await API.get_profile(token); 44 if (user_id && token) {
45 setProfile(user); 45 const user = await API.get_profile(token);
46 } 46 setProfile(user);
47 }; 47 }
48 },
49 [token]
50 );
48 51
49 React.useEffect(() => { 52 React.useEffect(() => {
50 if (token === undefined) { 53 if (token === undefined) {
@@ -52,15 +55,15 @@ const App: React.FC = () => {
52 setIsModerator(false); 55 setIsModerator(false);
53 } else { 56 } else {
54 setProfile({} as UserProfile); // placeholder before we call actual user profile 57 setProfile({} as UserProfile); // placeholder before we call actual user profile
55 _set_profile(get_user_id_from_token(token)) 58 _set_profile(get_user_id_from_token(token));
56 const modStatus = get_user_mod_from_token(token) 59 const modStatus = get_user_mod_from_token(token);
57 if (modStatus) { 60 if (modStatus) {
58 setIsModerator(true); 61 setIsModerator(true);
59 } else { 62 } else {
60 setIsModerator(false); 63 setIsModerator(false);
61 } 64 }
62 } 65 }
63 }, [token]); 66 }, [token, _set_profile]);
64 67
65 React.useEffect(() => { 68 React.useEffect(() => {
66 _fetch_token(); 69 _fetch_token();
@@ -73,23 +76,49 @@ const App: React.FC = () => {
73 <title>LPHUB</title> 76 <title>LPHUB</title>
74 <meta name="description" content="Least Portals Hub" /> 77 <meta name="description" content="Least Portals Hub" />
75 </Helmet> 78 </Helmet>
76 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { 79 <UploadRunDialog
77 setUploadRunDialog(false); 80 token={token}
78 if (updateProfile) { 81 open={uploadRunDialog}
79 _set_profile(get_user_id_from_token(token)); 82 onClose={updateProfile => {
80 } 83 setUploadRunDialog(false);
81 }} games={games} /> 84 if (updateProfile) {
82 <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> 85 _set_profile(get_user_id_from_token(token));
86 }
87 }}
88 games={games}
89 />
90 <Sidebar
91 setToken={setToken}
92 profile={profile}
93 setProfile={setProfile}
94 onUploadRun={() => setUploadRunDialog(true)}
95 />
83 <Routes> 96 <Routes>
84 <Route path="/" element={<Homepage />} /> 97 <Route path="/" element={<Homepage />} />
85 <Route path="/profile" element={<Profile profile={profile} token={token} gameData={games} onDeleteRecord={() => _set_profile(get_user_id_from_token(token))} />} /> 98 <Route
86 <Route path="/users/*" element={<User profile={profile} token={token} gameData={games} />} /> 99 path="/profile"
100 element={
101 <Profile
102 profile={profile}
103 token={token}
104 gameData={games}
105 onDeleteRecord={() => _set_profile(get_user_id_from_token(token))}
106 />
107 }
108 />
109 <Route
110 path="/users/*"
111 element={<User profile={profile} token={token} gameData={games} />}
112 />
87 <Route path="/games" element={<Games games={games} />} /> 113 <Route path="/games" element={<Games games={games} />} />
88 <Route path='/games/:id' element={<Maplist />}></Route> 114 <Route path="/games/:id" element={<Maplist />}></Route>
89 <Route path="/maps/*" element={<Maps token={token} isModerator={isModerator} />} /> 115 <Route
116 path="/maps/*"
117 element={<Maps token={token} isModerator={isModerator} />}
118 />
90 <Route path="/rules" element={<Rules />} /> 119 <Route path="/rules" element={<Rules />} />
91 <Route path="/about" element={<About />} /> 120 <Route path="/about" element={<About />} />
92 <Route path='/rankings' element={<Rankings />}></Route> 121 <Route path="/rankings" element={<Rankings />}></Route>
93 <Route path="*" element={"404"} /> 122 <Route path="*" element={"404"} />
94 </Routes> 123 </Routes>
95 </> 124 </>
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts
index 862e688..b782d17 100644
--- a/frontend/src/api/Api.ts
+++ b/frontend/src/api/Api.ts
@@ -1,18 +1,39 @@
1import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '@customTypes/Content'; 1import { MapDiscussionContent, ModMenuContent } from "@customTypes/Content";
2import { delete_token, get_token } from '@api/Auth'; 2import { delete_token, get_token } from "@api/Auth";
3import { get_user, get_profile, post_profile } from '@api/User'; 3import { get_user, get_profile, post_profile } from "@api/User";
4import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from '@api/Games'; 4import {
5import { get_official_rankings, get_unofficial_rankings } from '@api/Rankings'; 5 get_games,
6import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from '@api/Maps'; 6 get_chapters,
7import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from '@api/Mod'; 7 get_games_chapters,
8import { UploadRunContent } from '@customTypes/Content'; 8 get_game_maps,
9 get_search,
10} from "@api/Games";
11import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings";
12import {
13 get_map_summary,
14 get_map_leaderboard,
15 get_map_discussions,
16 get_map_discussion,
17 post_map_discussion,
18 post_map_discussion_comment,
19 delete_map_discussion,
20 post_record,
21 delete_map_record,
22} from "@api/Maps";
23import {
24 delete_map_summary,
25 post_map_summary,
26 put_map_image,
27 put_map_summary,
28} from "@api/Mod";
29import { UploadRunContent } from "@customTypes/Content";
9 30
10// add new api call function entries here 31// add new api call function entries here
11// example usage: API.get_games(); 32// example usage: API.get_games();
12export const API = { 33export const API = {
13 // Auth 34 // Auth
14 get_token: () => get_token(), 35 get_token: () => get_token(),
15 36
16 delete_token: () => delete_token(), 37 delete_token: () => delete_token(),
17 // User 38 // User
18 get_user: (user_id: string) => get_user(user_id), 39 get_user: (user_id: string) => get_user(user_id),
@@ -29,28 +50,49 @@ export const API = {
29 get_unofficial_rankings: () => get_unofficial_rankings(), 50 get_unofficial_rankings: () => get_unofficial_rankings(),
30 // Maps 51 // Maps
31 get_map_summary: (map_id: string) => get_map_summary(map_id), 52 get_map_summary: (map_id: string) => get_map_summary(map_id),
32 get_map_leaderboard: (map_id: string, page: string) => get_map_leaderboard(map_id, page), 53 get_map_leaderboard: (map_id: string, page: string) =>
54 get_map_leaderboard(map_id, page),
33 get_map_discussions: (map_id: string) => get_map_discussions(map_id), 55 get_map_discussions: (map_id: string) => get_map_discussions(map_id),
34 get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), 56 get_map_discussion: (map_id: string, discussion_id: number) =>
57 get_map_discussion(map_id, discussion_id),
35 58
36 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), 59 post_map_discussion: (
37 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), 60 token: string,
38 post_record: (token: string, run: UploadRunContent, map_id: number) => post_record(token, run, map_id), 61 map_id: string,
62 content: MapDiscussionContent
63 ) => post_map_discussion(token, map_id, content),
64 post_map_discussion_comment: (
65 token: string,
66 map_id: string,
67 discussion_id: number,
68 comment: string
69 ) => post_map_discussion_comment(token, map_id, discussion_id, comment),
70 post_record: (token: string, run: UploadRunContent, map_id: number) =>
71 post_record(token, run, map_id),
39 72
40 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), 73 delete_map_discussion: (
74 token: string,
75 map_id: string,
76 discussion_id: number
77 ) => delete_map_discussion(token, map_id, discussion_id),
41 78
42 delete_map_record: (token: string, map_id: number, record_id: number) => delete_map_record(token, map_id, record_id), 79 delete_map_record: (token: string, map_id: number, record_id: number) =>
80 delete_map_record(token, map_id, record_id),
43 // Mod 81 // Mod
44 post_map_summary: (token: string, map_id: string, content: ModMenuContent) => post_map_summary(token, map_id, content), 82 post_map_summary: (token: string, map_id: string, content: ModMenuContent) =>
45 83 post_map_summary(token, map_id, content),
46 put_map_image: (token: string, map_id: string, image: string) => put_map_image(token, map_id, image), 84
47 put_map_summary: (token: string, map_id: string, content: ModMenuContent) => put_map_summary(token, map_id, content), 85 put_map_image: (token: string, map_id: string, image: string) =>
48 86 put_map_image(token, map_id, image),
49 delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), 87 put_map_summary: (token: string, map_id: string, content: ModMenuContent) =>
88 put_map_summary(token, map_id, content),
89
90 delete_map_summary: (token: string, map_id: string, route_id: number) =>
91 delete_map_summary(token, map_id, route_id),
50}; 92};
51 93
52const BASE_API_URL: string = "/api/v1/" 94const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/"
53 95
54export function url(path: string): string { 96export function url(path: string): string {
55 return BASE_API_URL + path; 97 return BASE_API_URL + path;
56}; 98}
diff --git a/frontend/src/api/Auth.ts b/frontend/src/api/Auth.ts
index 875c7e5..98c6d36 100644
--- a/frontend/src/api/Auth.ts
+++ b/frontend/src/api/Auth.ts
@@ -2,7 +2,7 @@ import axios from "axios";
2import { url } from "@api/Api"; 2import { url } from "@api/Api";
3 3
4export const get_token = async (): Promise<string | undefined> => { 4export const get_token = async (): Promise<string | undefined> => {
5 const response = await axios.get(url(`token`)) 5 const response = await axios.get(url(`token`));
6 if (!response.data.success) { 6 if (!response.data.success) {
7 return undefined; 7 return undefined;
8 } 8 }
diff --git a/frontend/src/api/Games.ts b/frontend/src/api/Games.ts
index 72bb4b3..b739f80 100644
--- a/frontend/src/api/Games.ts
+++ b/frontend/src/api/Games.ts
@@ -6,26 +6,30 @@ import { Map } from "@customTypes/Map";
6import { Search } from "@customTypes/Search"; 6import { Search } from "@customTypes/Search";
7 7
8export const get_games = async (): Promise<Game[]> => { 8export const get_games = async (): Promise<Game[]> => {
9 const response = await axios.get(url(`games`)) 9 const response = await axios.get(url(`games`));
10 return response.data.data; 10 return response.data.data;
11}; 11};
12 12
13export const get_chapters = async (chapter_id: string): Promise<GameChapter> => { 13export const get_chapters = async (
14 chapter_id: string
15): Promise<GameChapter> => {
14 const response = await axios.get(url(`chapters/${chapter_id}`)); 16 const response = await axios.get(url(`chapters/${chapter_id}`));
15 return response.data.data; 17 return response.data.data;
16} 18};
17 19
18export const get_games_chapters = async (game_id: string): Promise<GamesChapters> => { 20export const get_games_chapters = async (
21 game_id: string
22): Promise<GamesChapters> => {
19 const response = await axios.get(url(`games/${game_id}`)); 23 const response = await axios.get(url(`games/${game_id}`));
20 return response.data.data; 24 return response.data.data;
21}; 25};
22 26
23export const get_game_maps = async (game_id: string): Promise<Map[]> => { 27export const get_game_maps = async (game_id: string): Promise<Map[]> => {
24 const response = await axios.get(url(`games/${game_id}/maps`)) 28 const response = await axios.get(url(`games/${game_id}/maps`));
25 return response.data.data.maps; 29 return response.data.data.maps;
26}; 30};
27 31
28export const get_search = async (q: string): Promise<Search> => { 32export const get_search = async (q: string): Promise<Search> => {
29 const response = await axios.get(url(`search?q=${q}`)) 33 const response = await axios.get(url(`search?q=${q}`));
30 return response.data.data; 34 return response.data.data;
31}; 35};
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts
index aa967ce..2485941 100644
--- a/frontend/src/api/Maps.ts
+++ b/frontend/src/api/Maps.ts
@@ -1,15 +1,25 @@
1import axios from "axios"; 1import axios from "axios";
2import { url } from "@api/Api"; 2import { url } from "@api/Api";
3import { MapDiscussionContent, UploadRunContent } from "@customTypes/Content"; 3import { MapDiscussionContent, UploadRunContent } from "@customTypes/Content";
4import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "@customTypes/Map"; 4import {
5 MapSummary,
6 MapLeaderboard,
7 MapDiscussions,
8 MapDiscussion,
9} from "@customTypes/Map";
5 10
6export const get_map_summary = async (map_id: string): Promise<MapSummary> => { 11export const get_map_summary = async (map_id: string): Promise<MapSummary> => {
7 const response = await axios.get(url(`maps/${map_id}/summary`)) 12 const response = await axios.get(url(`maps/${map_id}/summary`));
8 return response.data.data; 13 return response.data.data;
9}; 14};
10 15
11export const get_map_leaderboard = async (map_id: string, page: string): Promise<MapLeaderboard | undefined> => { 16export const get_map_leaderboard = async (
12 const response = await axios.get(url(`maps/${map_id}/leaderboards?page=${page}`)); 17 map_id: string,
18 page: string
19): Promise<MapLeaderboard | undefined> => {
20 const response = await axios.get(
21 url(`maps/${map_id}/leaderboards?page=${page}`)
22 );
13 if (!response.data.success) { 23 if (!response.data.success) {
14 return undefined; 24 return undefined;
15 } 25 }
@@ -17,15 +27,17 @@ export const get_map_leaderboard = async (map_id: string, page: string): Promise
17 // map the kind of leaderboard 27 // map the kind of leaderboard
18 data.records = data.records.map((record: any) => { 28 data.records = data.records.map((record: any) => {
19 if (record.host && record.partner) { 29 if (record.host && record.partner) {
20 return { ...record, kind: 'multiplayer' }; 30 return { ...record, kind: "multiplayer" };
21 } else { 31 } else {
22 return { ...record, kind: 'singleplayer' }; 32 return { ...record, kind: "singleplayer" };
23 } 33 }
24 }); 34 });
25 return data; 35 return data;
26}; 36};
27 37
28export const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => { 38export const get_map_discussions = async (
39 map_id: string
40): Promise<MapDiscussions | undefined> => {
29 const response = await axios.get(url(`maps/${map_id}/discussions`)); 41 const response = await axios.get(url(`maps/${map_id}/discussions`));
30 if (!response.data.data.discussions) { 42 if (!response.data.data.discussions) {
31 return undefined; 43 return undefined;
@@ -33,74 +45,122 @@ export const get_map_discussions = async (map_id: string): Promise<MapDiscussion
33 return response.data.data; 45 return response.data.data;
34}; 46};
35 47
36export const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => { 48export const get_map_discussion = async (
37 const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`)); 49 map_id: string,
50 discussion_id: number
51): Promise<MapDiscussion | undefined> => {
52 const response = await axios.get(
53 url(`maps/${map_id}/discussions/${discussion_id}`)
54 );
38 if (!response.data.data.discussion) { 55 if (!response.data.data.discussion) {
39 return undefined; 56 return undefined;
40 } 57 }
41 return response.data.data; 58 return response.data.data;
42}; 59};
43 60
44export const post_map_discussion = async (token: string, map_id: string, content: MapDiscussionContent): Promise<boolean> => { 61export const post_map_discussion = async (
45 const response = await axios.post(url(`maps/${map_id}/discussions`), { 62 token: string,
46 "title": content.title, 63 map_id: string,
47 "content": content.content, 64 content: MapDiscussionContent
48 }, { 65): Promise<boolean> => {
49 headers: { 66 const response = await axios.post(
50 "Authorization": token, 67 url(`maps/${map_id}/discussions`),
68 {
69 title: content.title,
70 content: content.content,
71 },
72 {
73 headers: {
74 Authorization: token,
75 },
51 } 76 }
52 }); 77 );
53 return response.data.success; 78 return response.data.success;
54}; 79};
55 80
56export const post_map_discussion_comment = async (token: string, map_id: string, discussion_id: number, comment: string): Promise<boolean> => { 81export const post_map_discussion_comment = async (
57 const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), { 82 token: string,
58 "comment": comment, 83 map_id: string,
59 }, { 84 discussion_id: number,
60 headers: { 85 comment: string
61 "Authorization": token, 86): Promise<boolean> => {
87 const response = await axios.post(
88 url(`maps/${map_id}/discussions/${discussion_id}`),
89 {
90 comment: comment,
91 },
92 {
93 headers: {
94 Authorization: token,
95 },
62 } 96 }
63 }); 97 );
64 return response.data.success; 98 return response.data.success;
65}; 99};
66 100
67export const delete_map_discussion = async (token: string, map_id: string, discussion_id: number): Promise<boolean> => { 101export const delete_map_discussion = async (
68 const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`), { 102 token: string,
69 headers: { 103 map_id: string,
70 "Authorization": token, 104 discussion_id: number
105): Promise<boolean> => {
106 const response = await axios.delete(
107 url(`maps/${map_id}/discussions/${discussion_id}`),
108 {
109 headers: {
110 Authorization: token,
111 },
71 } 112 }
72 }); 113 );
73 return response.data.success; 114 return response.data.success;
74}; 115};
75 116
76export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => { 117export const post_record = async (
118 token: string,
119 run: UploadRunContent,
120 map_id: number
121): Promise<[boolean, string]> => {
77 if (run.partner_demo) { 122 if (run.partner_demo) {
78 const response = await axios.postForm(url(`maps/${map_id}/record`), { 123 const response = await axios.postForm(
79 "host_demo": run.host_demo, 124 url(`maps/${map_id}/record`),
80 "partner_demo": run.partner_demo, 125 {
81 }, { 126 host_demo: run.host_demo,
82 headers: { 127 partner_demo: run.partner_demo,
83 "Authorization": token, 128 },
129 {
130 headers: {
131 Authorization: token,
132 },
84 } 133 }
85 }); 134 );
86 return [response.data.success, response.data.message]; 135 return [response.data.success, response.data.message];
87 } else { 136 } else {
88 const response = await axios.postForm(url(`maps/${map_id}/record`), { 137 const response = await axios.postForm(
89 "host_demo": run.host_demo, 138 url(`maps/${map_id}/record`),
90 }, { 139 {
91 headers: { 140 host_demo: run.host_demo,
92 "Authorization": token, 141 },
142 {
143 headers: {
144 Authorization: token,
145 },
93 } 146 }
94 }); 147 );
95 return [response.data.success, response.data.message]; 148 return [response.data.success, response.data.message];
96 } 149 }
97} 150};
98 151
99export const delete_map_record = async (token: string, map_id: number, record_id: number): Promise<boolean> => { 152export const delete_map_record = async (
100 const response = await axios.delete(url(`maps/${map_id}/record/${record_id}`), { 153 token: string,
101 headers: { 154 map_id: number,
102 "Authorization": token, 155 record_id: number
156): Promise<boolean> => {
157 const response = await axios.delete(
158 url(`maps/${map_id}/record/${record_id}`),
159 {
160 headers: {
161 Authorization: token,
162 },
103 } 163 }
104 }); 164 );
105 return response.data.success; 165 return response.data.success;
106}; 166};
diff --git a/frontend/src/api/Mod.ts b/frontend/src/api/Mod.ts
index 1511f8b..d682f1a 100644
--- a/frontend/src/api/Mod.ts
+++ b/frontend/src/api/Mod.ts
@@ -2,57 +2,85 @@ import axios from "axios";
2import { url } from "@api/Api"; 2import { url } from "@api/Api";
3import { ModMenuContent } from "@customTypes/Content"; 3import { ModMenuContent } from "@customTypes/Content";
4 4
5export const put_map_image = async (token: string, map_id: string, image: string): Promise<boolean> => { 5export const put_map_image = async (
6 const response = await axios.put(url(`maps/${map_id}/image`), { 6 token: string,
7 "image": image, 7 map_id: string,
8 }, { 8 image: string
9 headers: { 9): Promise<boolean> => {
10 "Authorization": token, 10 const response = await axios.put(
11 url(`maps/${map_id}/image`),
12 {
13 image: image,
14 },
15 {
16 headers: {
17 Authorization: token,
18 },
11 } 19 }
12 }); 20 );
13 return response.data.success; 21 return response.data.success;
14}; 22};
15 23
16export const post_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { 24export const post_map_summary = async (
17 const response = await axios.post(url(`maps/${map_id}/summary`), { 25 token: string,
18 "category_id": content.category_id, 26 map_id: string,
19 "user_name": content.name, 27 content: ModMenuContent
20 "score_count": content.score, 28): Promise<boolean> => {
21 "record_date": content.date, 29 const response = await axios.post(
22 "showcase": content.showcase, 30 url(`maps/${map_id}/summary`),
23 "description": content.description, 31 {
24 }, { 32 category_id: content.category_id,
25 headers: { 33 user_name: content.name,
26 "Authorization": token, 34 score_count: content.score,
35 record_date: content.date,
36 showcase: content.showcase,
37 description: content.description,
38 },
39 {
40 headers: {
41 Authorization: token,
42 },
27 } 43 }
28 }); 44 );
29 return response.data.success; 45 return response.data.success;
30}; 46};
31 47
32export const put_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { 48export const put_map_summary = async (
33 const response = await axios.put(url(`maps/${map_id}/summary`), { 49 token: string,
34 "route_id": content.id, 50 map_id: string,
35 "user_name": content.name, 51 content: ModMenuContent
36 "score_count": content.score, 52): Promise<boolean> => {
37 "record_date": content.date, 53 const response = await axios.put(
38 "showcase": content.showcase, 54 url(`maps/${map_id}/summary`),
39 "description": content.description, 55 {
40 }, { 56 route_id: content.id,
41 headers: { 57 user_name: content.name,
42 "Authorization": token, 58 score_count: content.score,
59 record_date: content.date,
60 showcase: content.showcase,
61 description: content.description,
62 },
63 {
64 headers: {
65 Authorization: token,
66 },
43 } 67 }
44 }); 68 );
45 return response.data.success; 69 return response.data.success;
46}; 70};
47 71
48export const delete_map_summary = async (token: string, map_id: string, route_id: number): Promise<boolean> => { 72export const delete_map_summary = async (
73 token: string,
74 map_id: string,
75 route_id: number
76): Promise<boolean> => {
49 const response = await axios.delete(url(`maps/${map_id}/summary`), { 77 const response = await axios.delete(url(`maps/${map_id}/summary`), {
50 data: { 78 data: {
51 "route_id": route_id, 79 route_id: route_id,
52 }, 80 },
53 headers: { 81 headers: {
54 "Authorization": token, 82 Authorization: token,
55 } 83 },
56 }); 84 });
57 return response.data.success; 85 return response.data.success;
58}; 86};
diff --git a/frontend/src/api/User.ts b/frontend/src/api/User.ts
index 88da0f2..004aa22 100644
--- a/frontend/src/api/User.ts
+++ b/frontend/src/api/User.ts
@@ -10,16 +10,20 @@ export const get_user = async (user_id: string): Promise<UserProfile> => {
10export const get_profile = async (token: string): Promise<UserProfile> => { 10export const get_profile = async (token: string): Promise<UserProfile> => {
11 const response = await axios.get(url(`profile`), { 11 const response = await axios.get(url(`profile`), {
12 headers: { 12 headers: {
13 "Authorization": token, 13 Authorization: token,
14 } 14 },
15 }); 15 });
16 return response.data.data; 16 return response.data.data;
17}; 17};
18 18
19export const post_profile = async (token: string) => { 19export const post_profile = async (token: string): Promise<void> => {
20 const _ = await axios.post(url(`profile`), {}, { 20 await axios.post(
21 headers: { 21 url(`profile`),
22 "Authorization": token, 22 {},
23 {
24 headers: {
25 Authorization: token,
26 },
23 } 27 }
24 }); 28 );
25}; 29};
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index 44a653b..8f2ce7a 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,31 +1,34 @@
1import React from 'react'; 1import React from "react";
2
3import "@css/Dialog.css"
4 2
5interface ConfirmDialogProps { 3interface ConfirmDialogProps {
6 title: string; 4 title: string;
7 subtitle: string; 5 subtitle: string;
8 onConfirm: () => void; 6 onConfirm: () => void;
9 onCancel: () => void; 7 onCancel: () => void;
10}; 8}
11 9
12const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { 10const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
13 return ( 11 title,
14 <div className='dimmer'> 12 subtitle,
15 <div className='dialog'> 13 onConfirm,
16 <div className='dialog-element dialog-header'> 14 onCancel,
17 <span>{title}</span> 15}) => {
18 </div> 16 return (
19 <div className='dialog-element dialog-description'> 17 <div className="fixed w-[200%] h-full bg-black bg-opacity-50 z-[4]">
20 <span>{subtitle}</span> 18 <div className="fixed z-[4] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-surface rounded-3xl overflow-hidden min-w-[350px] border border-border animate-[dialog_in_0.2s_cubic-bezier(0.075,0.82,0.165,1.1)] text-foreground font-[--font-barlow-semicondensed-regular]">
21 </div> 19 <div className="p-2 text-2xl bg-mantle">
22 <div className='dialog-element dialog-btns-container'> 20 <span>{title}</span>
23 <button onClick={onCancel}>Cancel</button> 21 </div>
24 <button onClick={onConfirm}>Confirm</button> 22 <div className="p-2">
25 </div> 23 <span>{subtitle}</span>
26 </div> 24 </div>
25 <div className="p-2 flex justify-end border-t-2 border-border bg-mantle">
26 <button className="mr-2 px-4 py-2 bg-muted text-foreground rounded hover:bg-overlay1 transition-colors" onClick={onCancel}>Cancel</button>
27 <button className="px-4 py-2 bg-primary text-background rounded hover:bg-mauve transition-colors" onClick={onConfirm}>Confirm</button>
27 </div> 28 </div>
28 ) 29 </div>
30 </div>
31 );
29}; 32};
30 33
31export default ConfirmDialog; 34export default ConfirmDialog;
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx
index 17ae586..7aa8901 100644
--- a/frontend/src/components/Discussions.tsx
+++ b/frontend/src/components/Discussions.tsx
@@ -1,34 +1,48 @@
1import React from 'react'; 1import React from "react";
2 2
3import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '@customTypes/Map'; 3import {
4import { MapDiscussionCommentContent, MapDiscussionContent } from '@customTypes/Content'; 4 MapDiscussion,
5import { time_ago } from '@utils/Time'; 5 MapDiscussions,
6import { API } from '@api/Api'; 6 MapDiscussionsDetail,
7import "@css/Maps.css" 7} from "@customTypes/Map";
8import { Link } from 'react-router-dom'; 8import { MapDiscussionContent } from "@customTypes/Content";
9import useConfirm from '@hooks/UseConfirm'; 9import { time_ago } from "@utils/Time";
10import { API } from "@api/Api";
11import "@css/Maps.css";
12import { Link } from "react-router-dom";
13import useConfirm from "@hooks/UseConfirm";
10 14
11interface DiscussionsProps { 15interface DiscussionsProps {
12 token?: string 16 token?: string;
13 data?: MapDiscussions; 17 data?: MapDiscussions;
14 isModerator: boolean; 18 isModerator: boolean;
15 mapID: string; 19 mapID: string;
16 onRefresh: () => void; 20 onRefresh: () => void;
17} 21}
18 22
19const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, mapID, onRefresh }) => { 23const Discussions: React.FC<DiscussionsProps> = ({
20 24 token,
25 data,
26 isModerator,
27 mapID,
28 onRefresh,
29}) => {
21 const { confirm, ConfirmDialogComponent } = useConfirm(); 30 const { confirm, ConfirmDialogComponent } = useConfirm();
22 31
23 const [discussionThread, setDiscussionThread] = React.useState<MapDiscussion | undefined>(undefined); 32 const [discussionThread, setDiscussionThread] = React.useState<
33 MapDiscussion | undefined
34 >(undefined);
24 const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); 35 const [discussionSearch, setDiscussionSearch] = React.useState<string>("");
25 36
26 const [createDiscussion, setCreateDiscussion] = React.useState<boolean>(false); 37 const [createDiscussion, setCreateDiscussion] =
27 const [createDiscussionContent, setCreateDiscussionContent] = React.useState<MapDiscussionContent>({ 38 React.useState<boolean>(false);
28 title: "", 39 const [createDiscussionContent, setCreateDiscussionContent] =
29 content: "", 40 React.useState<MapDiscussionContent>({
30 }); 41 title: "",
31 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState<string>(""); 42 content: "",
43 });
44 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] =
45 React.useState<string>("");
32 46
33 const _open_map_discussion = async (discussion_id: number) => { 47 const _open_map_discussion = async (discussion_id: number) => {
34 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); 48 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id);
@@ -45,13 +59,23 @@ const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, map
45 59
46 const _create_map_discussion_comment = async (discussion_id: number) => { 60 const _create_map_discussion_comment = async (discussion_id: number) => {
47 if (token) { 61 if (token) {
48 await API.post_map_discussion_comment(token, mapID, discussion_id, createDiscussionCommentContent); 62 await API.post_map_discussion_comment(
63 token,
64 mapID,
65 discussion_id,
66 createDiscussionCommentContent
67 );
49 await _open_map_discussion(discussion_id); 68 await _open_map_discussion(discussion_id);
50 } 69 }
51 }; 70 };
52 71
53 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { 72 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => {
54 if (await confirm("Delete Map Discussion", `Are you sure you want to remove post: ${discussion.title}?`)) { 73 if (
74 await confirm(
75 "Delete Map Discussion",
76 `Are you sure you want to remove post: ${discussion.title}?`
77 )
78 ) {
55 if (token) { 79 if (token) {
56 await API.delete_map_discussion(token, mapID, discussion.id); 80 await API.delete_map_discussion(token, mapID, discussion.id);
57 onRefresh(); 81 onRefresh();
@@ -60,107 +84,186 @@ const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, map
60 }; 84 };
61 85
62 return ( 86 return (
63 <section id='section7' className='summary3'> 87 <section id="section7" className="summary3">
64 {ConfirmDialogComponent} 88 {ConfirmDialogComponent}
65 <div id='discussion-search'> 89 <div id="discussion-search">
66 <input type="text" value={discussionSearch} placeholder={"Search for posts..."} onChange={(e) => setDiscussionSearch(e.target.value)} /> 90 <input
67 <div><button onClick={() => setCreateDiscussion(true)}>New Post</button></div> 91 type="text"
92 value={discussionSearch}
93 placeholder={"Search for posts..."}
94 onChange={e => setDiscussionSearch(e.target.value)}
95 />
96 <div>
97 <button onClick={() => setCreateDiscussion(true)}>New Post</button>
98 </div>
68 </div> 99 </div>
69 100
70 { // janky ternary operators here, could divide them to more components? 101 {
71 createDiscussion ? 102 // janky ternary operators here, could divide them to more components?
72 ( 103 createDiscussion ? (
73 <div id='discussion-create'> 104 <div id="discussion-create">
74 <span>Create Post</span> 105 <span>Create Post</span>
75 <button onClick={() => setCreateDiscussion(false)}>X</button> 106 <button onClick={() => setCreateDiscussion(false)}>X</button>
76 <div style={{ gridColumn: "1 / span 2" }}> 107 <div style={{ gridColumn: "1 / span 2" }}>
77 <input id='discussion-create-title' placeholder='Title...' onChange={(e) => setCreateDiscussionContent({ 108 <input
78 ...createDiscussionContent, 109 id="discussion-create-title"
79 title: e.target.value, 110 placeholder="Title..."
80 })} /> 111 onChange={e =>
81 <input id='discussion-create-content' placeholder='Enter the content...' onChange={(e) => setCreateDiscussionContent({ 112 setCreateDiscussionContent({
82 ...createDiscussionContent, 113 ...createDiscussionContent,
83 content: e.target.value, 114 title: e.target.value,
84 })} /> 115 })
85 </div> 116 }
86 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> 117 />
87 <button id='discussion-create-button' onClick={() => _create_map_discussion()}>Post</button> 118 <input
88 </div> 119 id="discussion-create-content"
120 placeholder="Enter the content..."
121 onChange={e =>
122 setCreateDiscussionContent({
123 ...createDiscussionContent,
124 content: e.target.value,
125 })
126 }
127 />
128 </div>
129 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}>
130 <button
131 id="discussion-create-button"
132 onClick={() => _create_map_discussion()}
133 >
134 Post
135 </button>
136 </div>
137 </div>
138 ) : discussionThread ? (
139 <div id="discussion-thread">
140 <div>
141 <span>{discussionThread.discussion.title}</span>
142 <button onClick={() => setDiscussionThread(undefined)}>X</button>
89 </div> 143 </div>
90 )
91 :
92 discussionThread ?
93 (
94 <div id='discussion-thread'>
95 <div>
96 <span>{discussionThread.discussion.title}</span>
97 <button onClick={() => setDiscussionThread(undefined)}>X</button>
98 </div>
99 144
100 <div> 145 <div>
101 <Link to={`/users/${discussionThread.discussion.creator.steam_id}`}> 146 <Link
102 <img src={discussionThread.discussion.creator.avatar_link} alt="" /> 147 to={`/users/${discussionThread.discussion.creator.steam_id}`}
103 </Link> 148 >
104 <div> 149 <img
105 <span>{discussionThread.discussion.creator.user_name}</span> 150 src={discussionThread.discussion.creator.avatar_link}
106 <span>{time_ago(new Date(discussionThread.discussion.created_at.replace("T", " ").replace("Z", "")))}</span> 151 alt=""
107 <span>{discussionThread.discussion.content}</span> 152 />
108 </div> 153 </Link>
109 {discussionThread.discussion.comments ? 154 <div>
110 discussionThread.discussion.comments.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) 155 <span>{discussionThread.discussion.creator.user_name}</span>
111 .map(e => ( 156 <span>
112 <> 157 {time_ago(
113 <Link to={`/users/${e.user.steam_id}`}> 158 new Date(
114 <img src={e.user.avatar_link} alt="" /> 159 discussionThread.discussion.created_at
115 </Link> 160 .replace("T", " ")
116 <div> 161 .replace("Z", "")
117 <span>{e.user.user_name}</span> 162 )
118 <span>{time_ago(new Date(e.date.replace("T", " ").replace("Z", "")))}</span> 163 )}
119 <span>{e.comment}</span> 164 </span>
120 </div> 165 <span>{discussionThread.discussion.content}</span>
121 </> 166 </div>
122 )) : "" 167 {discussionThread.discussion.comments
123 } 168 ? discussionThread.discussion.comments
124 </div> 169 .sort(
125 <div id='discussion-send'> 170 (a, b) =>
126 <input type="text" value={createDiscussionCommentContent} placeholder={"Message"} 171 new Date(a.date).getTime() - new Date(b.date).getTime()
127 onKeyDown={(e) => e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)} 172 )
128 onChange={(e) => setCreateDiscussionCommentContent(e.target.value)} /> 173 .map(e => (
129 <div><button onClick={() => { 174 <>
175 <Link to={`/users/${e.user.steam_id}`}>
176 <img src={e.user.avatar_link} alt="" />
177 </Link>
178 <div>
179 <span>{e.user.user_name}</span>
180 <span>
181 {time_ago(
182 new Date(
183 e.date.replace("T", " ").replace("Z", "")
184 )
185 )}
186 </span>
187 <span>{e.comment}</span>
188 </div>
189 </>
190 ))
191 : ""}
192 </div>
193 <div id="discussion-send">
194 <input
195 type="text"
196 value={createDiscussionCommentContent}
197 placeholder={"Message"}
198 onKeyDown={e =>
199 e.key === "Enter" &&
200 _create_map_discussion_comment(discussionThread.discussion.id)
201 }
202 onChange={e =>
203 setCreateDiscussionCommentContent(e.target.value)
204 }
205 />
206 <div>
207 <button
208 onClick={() => {
130 if (createDiscussionCommentContent !== "") { 209 if (createDiscussionCommentContent !== "") {
131 _create_map_discussion_comment(discussionThread.discussion.id); 210 _create_map_discussion_comment(
211 discussionThread.discussion.id
212 );
132 setCreateDiscussionCommentContent(""); 213 setCreateDiscussionCommentContent("");
133 } 214 }
134 }}>Send</button></div> 215 }}
135 </div> 216 >
136 217 Send
218 </button>
137 </div> 219 </div>
138 ) 220 </div>
139 : 221 </div>
140 ( 222 ) : data ? (
141 data ? 223 <>
142 (<> 224 {data.discussions
143 {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) 225 .filter(f => f.title.includes(discussionSearch))
144 .map((e, i) => ( 226 .sort(
145 <div id='discussion-post'> 227 (a, b) =>
146 <button key={e.id} onClick={() => _open_map_discussion(e.id)}> 228 new Date(b.updated_at).getTime() -
147 <span>{e.title}</span> 229 new Date(a.updated_at).getTime()
148 {isModerator ? 230 )
149 <button onClick={(m) => { 231 .map((e, i) => (
150 m.stopPropagation(); 232 <div id="discussion-post">
151 _delete_map_discussion(e); 233 <button key={e.id} onClick={() => _open_map_discussion(e.id)}>
152 }}>Delete Post</button> 234 <span>{e.title}</span>
153 : <span></span> 235 {isModerator ? (
154 } 236 <button
155 <span><b>{e.creator.user_name}:</b> {e.content}</span> 237 onClick={m => {
156 <span>Last Updated: {time_ago(new Date(e.updated_at.replace("T", " ").replace("Z", "")))}</span> 238 m.stopPropagation();
157 </button> 239 _delete_map_discussion(e);
158 </div> 240 }}
159 ))} 241 >
160 </>) 242 Delete Post
161 : 243 </button>
162 (<span style={{ textAlign: "center", display: "block" }}>No Discussions...</span>) 244 ) : (
163 ) 245 <span></span>
246 )}
247 <span>
248 <b>{e.creator.user_name}:</b> {e.content}
249 </span>
250 <span>
251 Last Updated:{" "}
252 {time_ago(
253 new Date(
254 e.updated_at.replace("T", " ").replace("Z", "")
255 )
256 )}
257 </span>
258 </button>
259 </div>
260 ))}
261 </>
262 ) : (
263 <span style={{ textAlign: "center", display: "block" }}>
264 No Discussions...
265 </span>
266 )
164 } 267 }
165 </section> 268 </section>
166 ); 269 );
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index d8879ef..b18c9d9 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -1,24 +1,24 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from '@customTypes/Game'; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css"
6 5
7interface GameCategoryProps { 6interface GameCategoryProps {
8 game: Game; 7 game: Game;
9 cat: GameCategoryPortals; 8 cat: GameCategoryPortals;
10} 9}
11 10
12const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { 11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
13 return ( 12 return (
14 <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> 13 <Link
15 <div> 14 className="bg-surface text-center w-full h-[100px] rounded-3xl text-foreground m-3 hover:bg-surface1 transition-colors flex flex-col justify-between p-4"
16 <span className='games-page-item-body-item-title'>{cat.category.name}</span> 15 to={"/games/" + game.id + "?cat=" + cat.category.id}
17 <br /> 16 >
18 <span className='games-page-item-body-item-num'>{cat.portal_count}</span> 17 <p className="text-3xl font-semibold">{cat.category.name}</p>
19 </div> 18 <br />
20 </Link> 19 <p className="font-bold text-4xl">{cat.portal_count}</p>
21 ) 20 </Link>
22} 21 );
22};
23 23
24export default GameCategory; 24export default GameCategory;
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index 3bd2842..f8fd179 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -1,10 +1,9 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from '@customTypes/Game'; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css"
6 5
7import GameCategory from '@components/GameCategory'; 6import GameCategory from "@components/GameCategory";
8 7
9interface GameEntryProps { 8interface GameEntryProps {
10 game: Game; 9 game: Game;
@@ -18,17 +17,28 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 }, [game.category_portals]); 17 }, [game.category_portals]);
19 18
20 return ( 19 return (
21 <Link to={"/games/" + game.id}><div className='games-page-item'> 20 <Link to={"/games/" + game.id} className="w-full">
22 <div className='games-page-item-header'> 21 <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6">
23 <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> 22 <div className="w-full h-1/2 bg-cover overflow-hidden relative">
24 <span><b>{game.name}</b></span> 23 <div
24 style={{ backgroundImage: `url(${game.image})` }}
25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover"
26 ></div>
27 <span className="absolute inset-0 flex justify-center items-center">
28 <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b>
29 </span>
30 </div>
31 <div className="flex justify-center items-center h-1/2">
32 <div className="flex flex-row justify-between w-full">
33 {catInfo.map((cat, index) => {
34 return (
35 <GameCategory key={index} cat={cat} game={game} />
36 );
37 })}
38 </div>
39 </div>
25 </div> 40 </div>
26 <div id={game.id as any as string} className='games-page-item-body'> 41 </Link>
27 {catInfo.map((cat, index) => {
28 return <GameCategory cat={cat} game={game} key={index}></GameCategory>
29 })}
30 </div>
31 </div></Link>
32 ); 42 );
33}; 43};
34 44
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index fb614fa..1de9b08 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -1,15 +1,15 @@
1import React from 'react'; 1import React, { useCallback } from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { DownloadIcon, ThreedotIcon } from '@images/Images'; 4import { DownloadIcon, ThreedotIcon } from "@images/Images";
5import { MapLeaderboard } from '@customTypes/Map'; 5import { MapLeaderboard } from "@customTypes/Map";
6import { ticks_to_time, time_ago } from '@utils/Time'; 6import { ticks_to_time, time_ago } from "@utils/Time";
7import { API } from "@api/Api"; 7import { API } from "@api/Api";
8import useMessage from "@hooks/UseMessage"; 8import useMessage from "@hooks/UseMessage";
9import "@css/Maps.css" 9import "@css/Maps.css";
10 10
11interface LeaderboardsProps { 11interface LeaderboardsProps {
12 mapID: string; 12 mapID: string;
13} 13}
14 14
15const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { 15const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
@@ -17,109 +17,228 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
17 const [data, setData] = React.useState<MapLeaderboard | undefined>(undefined); 17 const [data, setData] = React.useState<MapLeaderboard | undefined>(undefined);
18 const [pageNumber, setPageNumber] = React.useState<number>(1); 18 const [pageNumber, setPageNumber] = React.useState<number>(1);
19 19
20 const _fetch_map_leaderboards = async () => { 20 const _fetch_map_leaderboards = useCallback(async () => {
21 const mapLeaderboards = await API.get_map_leaderboard(mapID, pageNumber.toString()); 21 const mapLeaderboards = await API.get_map_leaderboard(
22 mapID,
23 pageNumber.toString()
24 );
22 setData(mapLeaderboards); 25 setData(mapLeaderboards);
23 }; 26 }, [mapID, pageNumber]);
24 27
25 const { message, MessageDialogComponent } = useMessage(); 28 const { message, MessageDialogComponent } = useMessage();
26 29
27 React.useEffect(() => { 30 React.useEffect(() => {
28 _fetch_map_leaderboards(); 31 _fetch_map_leaderboards();
29 console.log(data); 32 console.log(data);
30 }, [pageNumber, navigate]) 33 }, [pageNumber, navigate, _fetch_map_leaderboards, data]);
31 34
32 if (!data) { 35 if (!data) {
33 return ( 36 return (
34 <section id='section6' className='summary2'> 37 <section id="section6" className="summary2">
35 <h1 style={{ textAlign: "center" }}>Map is not available for competitive boards.</h1> 38 <h1 style={{ textAlign: "center" }}>
39 Loading...
40 </h1>
36 </section> 41 </section>
37 ); 42 );
38 }; 43 }
39 44
40 if (data.records.length === 0) { 45 if (data.records.length === 0) {
41 return ( 46 return (
42 <section id='section6' className='summary2'> 47 <section id="section6" className="summary2">
43 <h1 style={{ textAlign: "center" }}>No records found.</h1> 48 <h1 style={{ textAlign: "center" }}>No records found.</h1>
44 </section> 49 </section>
45 ); 50 );
46 }; 51 }
47 52
48 return ( 53 return (
49 <div> 54 <div className="text-foreground">
50 {MessageDialogComponent} 55 {MessageDialogComponent}
51 <section id='section6' className='summary2'> 56 <section id="section6" className="summary2">
52 57 <div
53 <div id='leaderboard-top' 58 id="leaderboard-top"
54 style={data.map.is_coop ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }} 59 style={
55 > 60 data.map.is_coop
56 <span>Place</span> 61 ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" }
57 62 : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }
58 {data.map.is_coop ? ( 63 }
59 <div id='runner'> 64 >
60 <span>Blue</span> 65 <span>Place</span>
61 <span>Orange</span> 66
62 </div> 67 {data.map.is_coop ? (
63 ) : ( 68 <div id="runner">
64 <span>Runner</span> 69 <span>Blue</span>
65 )} 70 <span>Orange</span>
66 71 </div>
67 <span>Portals</span> 72 ) : (
68 <span>Time</span> 73 <span>Runner</span>
69 <span>Date</span> 74 )}
70 <div id='page-number'> 75
71 <div> 76 <span>Portals</span>
72 77 <span>Time</span>
73 <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} 78 <span>Date</span>
74 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 79 <div id="page-number">
75 <span>{data.pagination.current_page}/{data.pagination.total_pages}</span> 80 <div>
76 <button onClick={() => pageNumber === data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} 81 <button
77 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 82 onClick={() =>
83 pageNumber === 1
84 ? null
85 : setPageNumber(prevPageNumber => prevPageNumber - 1)
86 }
87 >
88 <i
89 className="triangle"
90 style={{ position: "relative", left: "-5px" }}
91 ></i>{" "}
92 </button>
93 <span>
94 {data.pagination.current_page}/{data.pagination.total_pages}
95 </span>
96 <button
97 onClick={() =>
98 pageNumber === data.pagination.total_pages
99 ? null
100 : setPageNumber(prevPageNumber => prevPageNumber + 1)
101 }
102 >
103 <i
104 className="triangle"
105 style={{
106 position: "relative",
107 left: "5px",
108 transform: "rotate(180deg)",
109 }}
110 ></i>{" "}
111 </button>
112 </div>
78 </div> 113 </div>
79 </div> 114 </div>
80 </div> 115 <hr />
81 <hr /> 116 <div id="leaderboard-records">
82 <div id='leaderboard-records'> 117 {data.records.map((r, index) => (
83 {data.records.map((r, index) => ( 118 <span
84 <span className='leaderboard-record' key={index} 119 className="leaderboard-record"
85 style={data.map.is_coop ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }} 120 key={index}
86 > 121 style={
87 <span>{r.placement}</span> 122 data.map.is_coop
88 <span> </span> 123 ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" }
89 {r.kind === "multiplayer" ? ( 124 : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }
90 <div> 125 }
91 <Link to={`/users/${r.host.steam_id}`}><span><img src={r.host.avatar_link} alt='' /> &nbsp; {r.host.user_name}</span></Link> 126 >
92 <Link to={`/users/${r.partner.steam_id}`}><span><img src={r.partner.avatar_link} alt='' /> &nbsp; {r.partner.user_name}</span></Link> 127 <span>{r.placement}</span>
93 </div> 128 <span> </span>
94 ) : r.kind === "singleplayer" && ( 129 {r.kind === "multiplayer" ? (
95 <div> 130 <div>
96 <Link to={`/users/${r.user.steam_id}`}><span><img src={r.user.avatar_link} alt='' /> &nbsp; {r.user.user_name}</span></Link> 131 <Link to={`/users/${r.host.steam_id}`}>
97 </div> 132 <span>
98 )} 133 <img src={r.host.avatar_link} alt="" /> &nbsp;{" "}
99 134 {r.host.user_name}
100 <span>{r.score_count}</span> 135 </span>
101 <span> </span> 136 </Link>
102 <span className='hover-popup' popup-text={(r.score_time) + " ticks"}>{ticks_to_time(r.score_time)}</span> 137 <Link to={`/users/${r.partner.steam_id}`}>
103 <span className='hover-popup' popup-text={r.record_date.replace("T", ' ').split(".")[0]}>{time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))}</span> 138 <span>
104 139 <img src={r.partner.avatar_link} alt="" /> &nbsp;{" "}
105 {r.kind === "multiplayer" ? ( 140 {r.partner.user_name}
106 <span> 141 </span>
107 <button onClick={() => { message("Demo Information", `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 142 </Link>
108 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button> 143 </div>
109 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button> 144 ) : (
145 r.kind === "singleplayer" && (
146 <div>
147 <Link to={`/users/${r.user.steam_id}`}>
148 <span>
149 <img src={r.user.avatar_link} alt="" /> &nbsp;{" "}
150 {r.user.user_name}
151 </span>
152 </Link>
153 </div>
154 )
155 )}
156
157 <span>{r.score_count}</span>
158 <span> </span>
159 <span
160 className="hover-popup"
161 popup-text={r.score_time + " ticks"}
162 >
163 {ticks_to_time(r.score_time)}
110 </span> 164 </span>
111 ) : r.kind === "singleplayer" && ( 165 <span
112 166 className="hover-popup"
113 <span> 167 popup-text={r.record_date.replace("T", " ").split(".")[0]}
114 <button onClick={() => { message("Demo Information", `Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 168 >
115 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 169 {time_ago(
170 new Date(r.record_date.replace("T", " ").replace("Z", ""))
171 )}
116 </span> 172 </span>
117 )} 173
118 </span> 174 {r.kind === "multiplayer" ? (
119 ))} 175 <span>
120 </div> 176 <button
121 </section> 177 onClick={() => {
122 </div> 178 message(
179 "Demo Information",
180 `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`
181 );
182 }}
183 >
184 <img src={ThreedotIcon} alt="demo_id" />
185 </button>
186 <button
187 onClick={() =>
188 (window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`)
189 }
190 >
191 <img
192 src={DownloadIcon}
193 alt="download"
194 style={{
195 filter:
196 "hue-rotate(160deg) contrast(60%) saturate(1000%)",
197 }}
198 className="w-6 h-6 mx-4"
199 />
200 </button>
201 <button
202 onClick={() =>
203 (window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`)
204 }
205 >
206 <img
207 src={DownloadIcon}
208 alt="download"
209 style={{
210 filter:
211 "hue-rotate(300deg) contrast(60%) saturate(1000%)",
212 }}
213 className="w-6 h-6"
214 />
215 </button>
216 </span>
217 ) : (
218 r.kind === "singleplayer" && (
219 <span>
220 <button
221 onClick={() => {
222 message("Demo Information", `Demo ID: ${r.demo_id}`);
223 }}
224 >
225 <img src={ThreedotIcon} alt="demo_id" />
226 </button>
227 <button
228 onClick={() =>
229 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`)
230 }
231 >
232 <img src={DownloadIcon} alt="download" className="w-6 h-6 mr-4" />
233 </button>
234 </span>
235 )
236 )}
237 </span>
238 ))}
239 </div>
240 </section>
241 </div>
123 ); 242 );
124}; 243};
125 244
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index f1628b2..ba85aeb 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -1,19 +1,18 @@
1import React from 'react'; 1import React from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { ExitIcon, UserIcon, LoginIcon } from '@images/Images'; 4import { ExitIcon, UserIcon, LoginIcon } from "../images/Images";
5import { UserProfile } from '@customTypes/Profile'; 5import { UserProfile } from "@customTypes/Profile";
6import { API } from '@api/Api'; 6import { API } from "@api/Api";
7import "@css/Login.css";
8 7
9interface LoginProps { 8interface LoginProps {
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 9 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 10 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 11 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
13}; 12 isOpen: boolean;
14 13}
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
16 14
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile, isOpen }) => {
17 const navigate = useNavigate(); 16 const navigate = useNavigate();
18 17
19 const _login = () => { 18 const _login = () => {
@@ -29,52 +28,71 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
29 28
30 return ( 29 return (
31 <> 30 <>
32 {profile 31 {profile ? (
33 ? 32 <>
34 ( 33 {profile.profile ? (
35 <> 34 <>
36 {profile.profile ? 35 <Link to="/profile" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
37 ( 36 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
38 <> 37 <img
39 <Link to="/profile" tabIndex={-1} className='login'> 38 className="rounded-[50px]"
40 <button className='sidebar-button'> 39 src={profile.avatar_link}
41 <img className="avatar-img" src={profile.avatar_link} alt="" /> 40 alt=""
42 <span>{profile.user_name}</span> 41 />
43 </button> 42 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">{profile.user_name}</span>
44 <button className='logout-button' onClick={_logout}> 43 </button>
45 <img src={ExitIcon} alt="" /><span /> 44 <button className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent" onClick={_logout}>
46 </button> 45 <img src={ExitIcon} alt="" />
47 </Link> 46 <span />
48 </> 47 </button>
49 ) 48 </Link>
50 : 49 </>
51 ( 50 ) : (
52 <> 51 <>
53 <Link to="/" tabIndex={-1} className='login'> 52 <Link to="/" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
54 <button className='sidebar-button'> 53 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
55 <img className="avatar-img" src={profile.avatar_link} alt="" /> 54 <img
56 <span>Loading Profile...</span> 55 className="rounded-[50px]"
57 </button> 56 src={profile.avatar_link}
58 <button disabled className='logout-button' onClick={_logout}> 57 alt=""
59 <img src={ExitIcon} alt="" /><span /> 58 />
60 </button> 59 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">Loading Profile...</span>
61 </Link> 60 </button>
62 </> 61 <button disabled className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent hidden" onClick={_logout}>
63 ) 62 <img src={ExitIcon} alt="" />
64 } 63 <span />
65 </> 64 </button>
66 ) 65 </Link>
67 : 66 </>
68 ( 67 )}
69 <Link to="/api/v1/login" tabIndex={-1} className='login' > 68 </>
70 <button className='sidebar-button' onClick={_login}> 69 ) : (
71 <img className="avatar-img" src={UserIcon} alt="" /> 70 <Link to="/api/v1/login" tabIndex={-1}>
72 <span> 71 <button
73 <img src={LoginIcon} alt="Sign in through Steam" /> 72 className={`${
74 </span> 73 isOpen
75 </button> 74 ? "grid grid-cols-[50px_auto] place-items-start pl-[11px]"
76 </Link> 75 : "flex items-center justify-center"
77 )} 76 } text-left bg-inherit cursor-pointer border-none w-[310px] h-16 rounded-[20px] py-[0.3em] px-0 transition-all duration-300 ${isOpen ? "text-white" : "text-gray-400"}`}
77 onClick={_login}
78 >
79 <span className={`font-[--font-barlow-semicondensed-regular] text-lg h-12 leading-7 transition-opacity duration-100 ${isOpen ? " overflow-hidden" : ""}`}>
80 {isOpen ? (
81 <div className="bg-neutral-800 p-2 rounded-lg w-64 flex flex-row items-center justifyt-start gap-2 font-semibold">
82 <LoginIcon />
83 <span>
84 Login with Steam
85 </span>
86 </div>
87 ) : (
88 <div className="bg-neutral-800 p-2 rounded-lg w-">
89 <LoginIcon />
90 </div>
91 )}
92 </span>
93 </button>
94 </Link>
95 )}
78 </> 96 </>
79 ); 97 );
80}; 98};
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx
index 0f494ad..985e806 100644
--- a/frontend/src/components/MapEntry.tsx
+++ b/frontend/src/components/MapEntry.tsx
@@ -1,12 +1,8 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4const MapEntry: React.FC = () => { 4const MapEntry: React.FC = () => {
5 return ( 5 return <div></div>;
6 <div> 6};
7
8 </div>
9 )
10}
11 7
12export default MapEntry; 8export default MapEntry;
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx
index 5c85189..fcf4d8d 100644
--- a/frontend/src/components/MessageDialog.tsx
+++ b/frontend/src/components/MessageDialog.tsx
@@ -1,29 +1,33 @@
1import React from 'react'; 1import React from "react";
2 2
3import "@css/Dialog.css" 3import "@css/Dialog.css";
4 4
5interface MessageDialogProps { 5interface MessageDialogProps {
6 title: string; 6 title: string;
7 subtitle: string; 7 subtitle: string;
8 onClose: () => void; 8 onClose: () => void;
9}; 9}
10 10
11const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { 11const MessageDialog: React.FC<MessageDialogProps> = ({
12 return ( 12 title,
13 <div className='dimmer'> 13 subtitle,
14 <div className='dialog'> 14 onClose,
15 <div className='dialog-element dialog-header'> 15}) => {
16 <span>{title}</span> 16 return (
17 </div> 17 <div className="dimmer">
18 <div className='dialog-element dialog-description'> 18 <div className="dialog">
19 <span>{subtitle}</span> 19 <div className="dialog-element dialog-header">
20 </div> 20 <span>{title}</span>
21 <div className='dialog-element dialog-btns-container'>
22 <button onClick={onClose}>Close</button>
23 </div>
24 </div>
25 </div> 21 </div>
26 ) 22 <div className="dialog-element dialog-description">
27} 23 <span>{subtitle}</span>
24 </div>
25 <div className="dialog-element dialog-btns-container">
26 <button onClick={onClose}>Close</button>
27 </div>
28 </div>
29 </div>
30 );
31};
28 32
29export default MessageDialog; 33export default MessageDialog;
diff --git a/frontend/src/components/MessageDialogLoad.tsx b/frontend/src/components/MessageDialogLoad.tsx
index 966e064..64cdd29 100644
--- a/frontend/src/components/MessageDialogLoad.tsx
+++ b/frontend/src/components/MessageDialogLoad.tsx
@@ -1,29 +1,31 @@
1import React from 'react'; 1import React from "react";
2 2
3import "@css/Dialog.css" 3import "@css/Dialog.css";
4 4
5interface MessageDialogLoadProps { 5interface MessageDialogLoadProps {
6 title: string; 6 title: string;
7 onClose: () => void; 7 onClose: () => void;
8}; 8}
9 9
10const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { 10const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({
11 return ( 11 title,
12 <div className='dimmer'> 12 onClose,
13 <div className='dialog'> 13}) => {
14 <div className='dialog-element dialog-header'> 14 return (
15 <span>{title}</span> 15 <div className="dimmer">
16 </div> 16 <div className="dialog">
17 <div className='dialog-element dialog-description'> 17 <div className="dialog-element dialog-header">
18 <div style={{display: "flex", justifyContent: "center"}}> 18 <span>{title}</span>
19 <span className="loader"></span>
20 </div>
21 </div>
22 <div className='dialog-element dialog-btns-container'>
23 </div>
24 </div>
25 </div> 19 </div>
26 ) 20 <div className="dialog-element dialog-description">
27} 21 <div style={{ display: "flex", justifyContent: "center" }}>
22 <span className="loader"></span>
23 </div>
24 </div>
25 <div className="dialog-element dialog-btns-container"></div>
26 </div>
27 </div>
28 );
29};
28 30
29export default MessageDialogLoad; 31export default MessageDialogLoad;
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx
index 925b8a8..a0d7eb7 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -1,12 +1,11 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { useNavigate } from 'react-router-dom'; 3import { useNavigate } from "react-router-dom";
4 4
5import { MapSummary } from '@customTypes/Map'; 5import { MapSummary } from "@customTypes/Map";
6import { ModMenuContent } from '@customTypes/Content'; 6import { ModMenuContent } from "@customTypes/Content";
7import { API } from '@api/Api'; 7import { API } from "@api/Api";
8import "@css/ModMenu.css" 8import useConfirm from "@hooks/UseConfirm";
9import useConfirm from '@hooks/UseConfirm';
10 9
11interface ModMenuProps { 10interface ModMenuProps {
12 token?: string; 11 token?: string;
@@ -15,8 +14,12 @@ interface ModMenuProps {
15 mapID: string; 14 mapID: string;
16} 15}
17 16
18const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) => { 17const ModMenu: React.FC<ModMenuProps> = ({
19 18 token,
19 data,
20 selectedRun,
21 mapID,
22}) => {
20 const { confirm, ConfirmDialogComponent } = useConfirm(); 23 const { confirm, ConfirmDialogComponent } = useConfirm();
21 24
22 const [menu, setMenu] = React.useState<number>(0); 25 const [menu, setMenu] = React.useState<number>(0);
@@ -55,74 +58,99 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
55 width *= 320 / height; 58 width *= 320 / height;
56 height = 320; 59 height = 320;
57 } 60 }
58 const canvas = document.createElement('canvas'); 61 const canvas = document.createElement("canvas");
59 canvas.width = width; 62 canvas.width = width;
60 canvas.height = height; 63 canvas.height = height;
61 canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); 64 canvas.getContext("2d")!.drawImage(img, 0, 0, width, height);
62 resolve(canvas.toDataURL(file.type, 0.6)); 65 resolve(canvas.toDataURL(file.type, 0.6));
63 }; 66 };
64 } 67 }
65 }; 68 };
66 }); 69 });
67 }; 70 }
68 71
69 const _edit_map_summary_image = async () => { 72 const _edit_map_summary_image = async () => {
70 if (await confirm("Edit Map Summary Image", "Are you sure you want to submit this to the database?")) { 73 if (
74 await confirm(
75 "Edit Map Summary Image",
76 "Are you sure you want to submit this to the database?"
77 )
78 ) {
71 if (token) { 79 if (token) {
72 const success = await API.put_map_image(token, mapID, image); 80 const success = await API.put_map_image(token, mapID, image);
73 if (success) { 81 if (success) {
74 navigate(0); 82 navigate(0);
75 } else { 83 } else {
76 alert("Error. Check logs.") 84 alert("Error. Check logs.");
77 } 85 }
78 } 86 }
79 } 87 }
80 }; 88 };
81 89
82 const _edit_map_summary_route = async () => { 90 const _edit_map_summary_route = async () => {
83 if (await confirm("Edit Map Summary Route", "Are you sure you want to submit this to the database?")) { 91 if (
92 await confirm(
93 "Edit Map Summary Route",
94 "Are you sure you want to submit this to the database?"
95 )
96 ) {
84 if (token) { 97 if (token) {
85 routeContent.date += "T00:00:00Z"; 98 routeContent.date += "T00:00:00Z";
86 const success = await API.put_map_summary(token, mapID, routeContent); 99 const success = await API.put_map_summary(token, mapID, routeContent);
87 if (success) { 100 if (success) {
88 navigate(0); 101 navigate(0);
89 } else { 102 } else {
90 alert("Error. Check logs.") 103 alert("Error. Check logs.");
91 } 104 }
92 } 105 }
93 } 106 }
94 }; 107 };
95 108
96 const _create_map_summary_route = async () => { 109 const _create_map_summary_route = async () => {
97 if (await confirm("Create Map Summary Route", "Are you sure you want to submit this to the database?")) { 110 if (
111 await confirm(
112 "Create Map Summary Route",
113 "Are you sure you want to submit this to the database?"
114 )
115 ) {
98 if (token) { 116 if (token) {
99 routeContent.date += "T00:00:00Z"; 117 routeContent.date += "T00:00:00Z";
100 const success = await API.post_map_summary(token, mapID, routeContent); 118 const success = await API.post_map_summary(token, mapID, routeContent);
101 if (success) { 119 if (success) {
102 navigate(0); 120 navigate(0);
103 } else { 121 } else {
104 alert("Error. Check logs.") 122 alert("Error. Check logs.");
105 } 123 }
106 } 124 }
107 } 125 }
108 }; 126 };
109 127
110 const _delete_map_summary_route = async () => { 128 const _delete_map_summary_route = async () => {
111 if (await confirm("Delete Map Summary Route", `Are you sure you want to submit this to the database?\n 129 if (
112 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}`)) { 130 await confirm(
131 "Delete Map Summary Route",
132 `Are you sure you want to submit this to the database?\n
133 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}`
134 )
135 ) {
113 if (token) { 136 if (token) {
114 const success = await API.delete_map_summary(token, mapID, data.summary.routes[selectedRun].route_id); 137 const success = await API.delete_map_summary(
138 token,
139 mapID,
140 data.summary.routes[selectedRun].route_id
141 );
115 if (success) { 142 if (success) {
116 navigate(0); 143 navigate(0);
117 } else { 144 } else {
118 alert("Error. Check logs.") 145 alert("Error. Check logs.");
119 } 146 }
120 } 147 }
121 } 148 }
122 }; 149 };
123 150
124 React.useEffect(() => { 151 React.useEffect(() => {
125 if (menu === 3) { // add route 152 if (menu === 3) {
153 // add route
126 setRouteContent({ 154 setRouteContent({
127 id: 0, 155 id: 0,
128 name: "", 156 name: "",
@@ -134,7 +162,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
134 }); 162 });
135 setMd("No description available."); 163 setMd("No description available.");
136 } 164 }
137 if (menu === 2) { // edit route 165 if (menu === 2) {
166 // edit route
138 setRouteContent({ 167 setRouteContent({
139 id: data.summary.routes[selectedRun].route_id, 168 id: data.summary.routes[selectedRun].route_id,
140 name: data.summary.routes[selectedRun].history.runner_name, 169 name: data.summary.routes[selectedRun].history.runner_name,
@@ -146,207 +175,335 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
146 }); 175 });
147 setMd(data.summary.routes[selectedRun].description); 176 setMd(data.summary.routes[selectedRun].description);
148 } 177 }
149 }, [menu]); 178 }, [menu, data.summary.routes, selectedRun]);
150 179
151 React.useEffect(() => { 180 React.useEffect(() => {
152 const modview = document.querySelector("div#modview") as HTMLElement 181 const modview = document.querySelector("div#modview") as HTMLElement;
153 if (modview) { 182 if (modview) {
154 showButton ? modview.style.transform = "translateY(-68%)" 183 showButton
155 : modview.style.transform = "translateY(0%)" 184 ? (modview.style.transform = "translateY(-68%)")
185 : (modview.style.transform = "translateY(0%)");
156 } 186 }
157 187
158 const modview_block = document.querySelector("#modview_block") as HTMLElement 188 const modview_block = document.querySelector(
189 "#modview_block"
190 ) as HTMLElement;
159 if (modview_block) { 191 if (modview_block) {
160 showButton ? modview_block.style.display = "none" : modview_block.style.display = "block" 192 showButton
193 ? (modview_block.style.display = "none")
194 : (modview_block.style.display = "block");
161 } 195 }
162 }, [showButton]) 196 }, [showButton]);
163 197
164 return ( 198 return (
165 <> 199 <>
166 {ConfirmDialogComponent} 200 {ConfirmDialogComponent}
167 <div id="modview_block" /> 201 <div id="modview_block" />
168 <div id='modview'> 202 <div id="modview">
169 <div> 203 <div>
170 <button onClick={() => setMenu(1)}>Edit Image</button> 204 <button onClick={() => setMenu(1)}>Edit Image</button>
171 <button onClick={() => setMenu(2)}>Edit Selected Route</button> 205 <button onClick={() => setMenu(2)}>Edit Selected Route</button>
172 <button onClick={() => setMenu(3)}>Add New Route</button> 206 <button onClick={() => setMenu(3)}>Add New Route</button>
173 <button onClick={() => _delete_map_summary_route()}>Delete Selected Route</button> 207 <button onClick={() => _delete_map_summary_route()}>
208 Delete Selected Route
209 </button>
174 </div> 210 </div>
175 <div> 211 <div>
176 {showButton ? ( 212 {showButton ? (
177 <button onClick={() => setShowButton(false)}>Show</button> 213 <button onClick={() => setShowButton(false)}>Show</button>
178 ) : ( 214 ) : (
179 <button onClick={() => { setShowButton(true); setMenu(0); }}>Hide</button> 215 <button
216 onClick={() => {
217 setShowButton(true);
218 setMenu(0);
219 }}
220 >
221 Hide
222 </button>
180 )} 223 )}
181 </div> 224 </div>
182 </div><div id='modview-menu'> 225 </div>
183 {// Edit Image 226 <div id="modview-menu">
227 {
228 // Edit Image
184 menu === 1 && ( 229 menu === 1 && (
185 <div id='modview-menu-image'> 230 <div id="modview-menu-image">
186 <div> 231 <div>
187 <span>Current Image:</span> 232 <span>Current Image:</span>
188 <img src={data.map.image} alt="missing" /> 233 <img src={data.map.image} alt="missing" />
189 </div> 234 </div>
190 235
191 <div> 236 <div>
192 <span>New Image: 237 <span>
193 <input type="file" accept='image/*' onChange={e => { 238 New Image:
194 if (e.target.files) { 239 <input
195 compressImage(e.target.files[0]) 240 type="file"
196 .then(d => setImage(d)); 241 accept="image/*"
197 } 242 onChange={e => {
198 }} /></span> 243 if (e.target.files) {
199 {image ? (<button onClick={() => _edit_map_summary_image()}>upload</button>) : <span></span>} 244 compressImage(e.target.files[0]).then(d => setImage(d));
200 <img src={image} alt="" id='modview-menu-image-file' /> 245 }
201 246 }}
247 />
248 </span>
249 {image ? (
250 <button onClick={() => _edit_map_summary_image()}>
251 upload
252 </button>
253 ) : (
254 <span></span>
255 )}
256 <img src={image} alt="" id="modview-menu-image-file" />
202 </div> 257 </div>
203 </div> 258 </div>
204 )} 259 )
260 }
205 261
206 {// Edit Route 262 {
263 // Edit Route
207 menu === 2 && ( 264 menu === 2 && (
208 <div id='modview-menu-edit'> 265 <div id="modview-menu-edit">
209 <div id='modview-route-id'> 266 <div id="modview-route-id">
210 <span>Route ID:</span> 267 <span>Route ID:</span>
211 <input type="number" value={routeContent.id} disabled /> 268 <input type="number" value={routeContent.id} disabled />
212 </div> 269 </div>
213 <div id='modview-route-name'> 270 <div id="modview-route-name">
214 <span>Runner Name:</span> 271 <span>Runner Name:</span>
215 <input type="text" value={routeContent.name} onChange={(e) => { 272 <input
216 setRouteContent({ 273 type="text"
217 ...routeContent, 274 value={routeContent.name}
218 name: e.target.value, 275 onChange={e => {
219 }); 276 setRouteContent({
220 }} /> 277 ...routeContent,
278 name: e.target.value,
279 });
280 }}
281 />
221 </div> 282 </div>
222 <div id='modview-route-score'> 283 <div id="modview-route-score">
223 <span>Score:</span> 284 <span>Score:</span>
224 <input type="number" value={routeContent.score} onChange={(e) => { 285 <input
225 setRouteContent({ 286 type="number"
226 ...routeContent, 287 value={routeContent.score}
227 score: parseInt(e.target.value), 288 onChange={e => {
228 }); 289 setRouteContent({
229 }} /> 290 ...routeContent,
291 score: parseInt(e.target.value),
292 });
293 }}
294 />
230 </div> 295 </div>
231 <div id='modview-route-date'> 296 <div id="modview-route-date">
232 <span>Date:</span> 297 <span>Date:</span>
233 <input type="date" value={routeContent.date} onChange={(e) => { 298 <input
234 setRouteContent({ 299 type="date"
235 ...routeContent, 300 value={routeContent.date}
236 date: e.target.value, 301 onChange={e => {
237 }); 302 setRouteContent({
238 }} /> 303 ...routeContent,
304 date: e.target.value,
305 });
306 }}
307 />
239 </div> 308 </div>
240 <div id='modview-route-showcase'> 309 <div id="modview-route-showcase">
241 <span>Showcase Video:</span> 310 <span>Showcase Video:</span>
242 <input type="text" value={routeContent.showcase} onChange={(e) => { 311 <input
243 setRouteContent({ 312 type="text"
244 ...routeContent, 313 value={routeContent.showcase}
245 showcase: e.target.value, 314 onChange={e => {
246 }); 315 setRouteContent({
247 }} /> 316 ...routeContent,
317 showcase: e.target.value,
318 });
319 }}
320 />
248 </div> 321 </div>
249 <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> 322 <div
323 id="modview-route-description"
324 style={{ height: "180px", gridColumn: "1 / span 5" }}
325 >
250 <span>Description:</span> 326 <span>Description:</span>
251 <textarea value={routeContent.description} onChange={(e) => { 327 <textarea
252 setRouteContent({ 328 value={routeContent.description}
253 ...routeContent, 329 onChange={e => {
254 description: e.target.value, 330 setRouteContent({
255 }); 331 ...routeContent,
256 setMd(routeContent.description); 332 description: e.target.value,
257 }} /> 333 });
334 setMd(routeContent.description);
335 }}
336 />
258 </div> 337 </div>
259 <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_edit_map_summary_route}>Apply</button> 338 <button
339 style={{ gridColumn: "2 / span 3", height: "40px" }}
340 onClick={_edit_map_summary_route}
341 >
342 Apply
343 </button>
260 344
261 <div id='modview-md'> 345 <div id="modview-md">
262 <span>Markdown Preview</span> 346 <span>Markdown Preview</span>
263 <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>Documentation</a></span> 347 <span>
264 <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>Demo</a></span> 348 <a
349 href="https://commonmark.org/help/"
350 rel="noreferrer"
351 target="_blank"
352 >
353 Documentation
354 </a>
355 </span>
356 <span>
357 <a
358 href="https://remarkjs.github.io/react-markdown/"
359 rel="noreferrer"
360 target="_blank"
361 >
362 Demo
363 </a>
364 </span>
265 <p> 365 <p>
266 <ReactMarkdown>{md} 366 <ReactMarkdown>{md}</ReactMarkdown>
267 </ReactMarkdown>
268 </p> 367 </p>
269 </div> 368 </div>
270 </div> 369 </div>
271 )} 370 )
371 }
272 372
273 {// Add Route 373 {
374 // Add Route
274 menu === 3 && ( 375 menu === 3 && (
275 <div id='modview-menu-add'> 376 <div id="modview-menu-add">
276 <div id='modview-route-category'> 377 <div id="modview-route-category">
277 <span>Category:</span> 378 <span>Category:</span>
278 <select onChange={(e) => { 379 <select
279 setRouteContent({ 380 onChange={e => {
280 ...routeContent, 381 setRouteContent({
281 category_id: parseInt(e.target.value), 382 ...routeContent,
282 }); 383 category_id: parseInt(e.target.value),
283 }}> 384 });
284 <option value="1" key="1">CM</option> 385 }}
285 <option value="2" key="2">No SLA</option> 386 >
286 {data.map.game_name === "Portal 2 - Cooperative" ? "" : ( 387 <option value="1" key="1">
287 <option value="3" key="3">Inbounds SLA</option>)} 388 CM
288 <option value="4" key="4">Any%</option> 389 </option>
390 <option value="2" key="2">
391 No SLA
392 </option>
393 {data.map.game_name === "Portal 2 - Cooperative" ? (
394 ""
395 ) : (
396 <option value="3" key="3">
397 Inbounds SLA
398 </option>
399 )}
400 <option value="4" key="4">
401 Any%
402 </option>
289 </select> 403 </select>
290 </div> 404 </div>
291 <div id='modview-route-name'> 405 <div id="modview-route-name">
292 <span>Runner Name:</span> 406 <span>Runner Name:</span>
293 <input type="text" value={routeContent.name} onChange={(e) => { 407 <input
294 setRouteContent({ 408 type="text"
295 ...routeContent, 409 value={routeContent.name}
296 name: e.target.value, 410 onChange={e => {
297 }); 411 setRouteContent({
298 }} /> 412 ...routeContent,
413 name: e.target.value,
414 });
415 }}
416 />
299 </div> 417 </div>
300 <div id='modview-route-score'> 418 <div id="modview-route-score">
301 <span>Score:</span> 419 <span>Score:</span>
302 <input type="number" value={routeContent.score} onChange={(e) => { 420 <input
303 setRouteContent({ 421 type="number"
304 ...routeContent, 422 value={routeContent.score}
305 score: parseInt(e.target.value), 423 onChange={e => {
306 }); 424 setRouteContent({
307 }} /> 425 ...routeContent,
426 score: parseInt(e.target.value),
427 });
428 }}
429 />
308 </div> 430 </div>
309 <div id='modview-route-date'> 431 <div id="modview-route-date">
310 <span>Date:</span> 432 <span>Date:</span>
311 <input type="date" value={routeContent.date} onChange={(e) => { 433 <input
312 setRouteContent({ 434 type="date"
313 ...routeContent, 435 value={routeContent.date}
314 date: e.target.value, 436 onChange={e => {
315 }); 437 setRouteContent({
316 }} /> 438 ...routeContent,
439 date: e.target.value,
440 });
441 }}
442 />
317 </div> 443 </div>
318 <div id='modview-route-showcase'> 444 <div id="modview-route-showcase">
319 <span>Showcase Video:</span> 445 <span>Showcase Video:</span>
320 <input type="text" value={routeContent.showcase} onChange={(e) => { 446 <input
321 setRouteContent({ 447 type="text"
322 ...routeContent, 448 value={routeContent.showcase}
323 showcase: e.target.value, 449 onChange={e => {
324 }); 450 setRouteContent({
325 }} /> 451 ...routeContent,
452 showcase: e.target.value,
453 });
454 }}
455 />
326 </div> 456 </div>
327 <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> 457 <div
458 id="modview-route-description"
459 style={{ height: "180px", gridColumn: "1 / span 5" }}
460 >
328 <span>Description:</span> 461 <span>Description:</span>
329 <textarea value={routeContent.description} onChange={(e) => { 462 <textarea
330 setRouteContent({ 463 value={routeContent.description}
331 ...routeContent, 464 onChange={e => {
332 description: e.target.value, 465 setRouteContent({
333 }); 466 ...routeContent,
334 setMd(routeContent.description); 467 description: e.target.value,
335 }} /> 468 });
469 setMd(routeContent.description);
470 }}
471 />
336 </div> 472 </div>
337 <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_create_map_summary_route}>Apply</button> 473 <button
474 style={{ gridColumn: "2 / span 3", height: "40px" }}
475 onClick={_create_map_summary_route}
476 >
477 Apply
478 </button>
338 479
339 <div id='modview-md'> 480 <div id="modview-md">
340 <span>Markdown preview</span> 481 <span>Markdown preview</span>
341 <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>documentation</a></span> 482 <span>
342 <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>demo</a></span> 483 <a
484 href="https://commonmark.org/help/"
485 rel="noreferrer"
486 target="_blank"
487 >
488 documentation
489 </a>
490 </span>
491 <span>
492 <a
493 href="https://remarkjs.github.io/react-markdown/"
494 rel="noreferrer"
495 target="_blank"
496 >
497 demo
498 </a>
499 </span>
343 <p> 500 <p>
344 <ReactMarkdown>{md} 501 <ReactMarkdown>{md}</ReactMarkdown>
345 </ReactMarkdown>
346 </p> 502 </p>
347 </div> 503 </div>
348 </div> 504 </div>
349 )} 505 )
506 }
350 </div> 507 </div>
351 </> 508 </>
352 ); 509 );
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx
index b899965..f28eabf 100644
--- a/frontend/src/components/RankingEntry.tsx
+++ b/frontend/src/components/RankingEntry.tsx
@@ -1,46 +1,65 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3import { RankingType, SteamRanking, SteamRankingType } from '@customTypes/Ranking'; 3import { RankingType, SteamRankingType } from "@customTypes/Ranking";
4 4
5enum RankingCategories { 5enum RankingCategories {
6 rankings_overall, 6 rankings_overall,
7 rankings_multiplayer, 7 rankings_multiplayer,
8 rankings_singleplayer 8 rankings_singleplayer,
9} 9}
10 10
11interface RankingEntryProps { 11interface RankingEntryProps {
12 curRankingData: RankingType | SteamRankingType; 12 curRankingData: RankingType | SteamRankingType;
13 currentLeaderboardType: RankingCategories 13 currentLeaderboardType: RankingCategories;
14};
15
16const RankingEntry: React.FC<RankingEntryProps> = (prop) => {
17 if ("placement" in prop.curRankingData) {
18 return (
19 <div className='leaderboard-entry'>
20 <span>{prop.curRankingData.placement}</span>
21 <div>
22 <Link to={`/users/${prop.curRankingData.user.steam_id}`}>
23 <img src={prop.curRankingData.user.avatar_link}></img>
24 <span>{prop.curRankingData.user.user_name}</span>
25 </Link>
26 </div>
27 <span>{prop.curRankingData.total_score}</span>
28 </div>
29 )
30 } else {
31 return (
32 <div className='leaderboard-entry'>
33 <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_rank : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_rank : prop.curRankingData.overall_rank}</span>
34 <div>
35 <Link to={`/users/${prop.curRankingData.steam_id}`}>
36 <img src={prop.curRankingData.avatar_link}></img>
37 <span>{prop.curRankingData.user_name}</span>
38 </Link>
39 </div>
40 <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span>
41 </div>
42 )
43 }
44} 14}
45 15
16const RankingEntry: React.FC<RankingEntryProps> = prop => {
17 if ("placement" in prop.curRankingData) {
18 return (
19 <div className="leaderboard-entry">
20 <span>{prop.curRankingData.placement}</span>
21 <div>
22 <Link to={`/users/${prop.curRankingData.user.steam_id}`}>
23 <img
24 src={prop.curRankingData.user.avatar_link}
25 alt={`${prop.curRankingData.user.user_name}'s Avatar`}
26 ></img>
27 <span>{prop.curRankingData.user.user_name}</span>
28 </Link>
29 </div>
30 <span>{prop.curRankingData.total_score}</span>
31 </div>
32 );
33 } else {
34 return (
35 <div className="leaderboard-entry">
36 <span>
37 {prop.currentLeaderboardType ===
38 RankingCategories.rankings_singleplayer
39 ? prop.curRankingData.sp_rank
40 : prop.currentLeaderboardType ===
41 RankingCategories.rankings_multiplayer
42 ? prop.curRankingData.mp_rank
43 : prop.curRankingData.overall_rank}
44 </span>
45 <div>
46 <Link to={`/users/${prop.curRankingData.steam_id}`}>
47 <img src={prop.curRankingData.avatar_link}></img>
48 <span>{prop.curRankingData.user_name}</span>
49 </Link>
50 </div>
51 <span>
52 {prop.currentLeaderboardType ===
53 RankingCategories.rankings_singleplayer
54 ? prop.curRankingData.sp_score
55 : prop.currentLeaderboardType ===
56 RankingCategories.rankings_multiplayer
57 ? prop.curRankingData.mp_score
58 : prop.curRankingData.overall_score}
59 </span>
60 </div>
61 );
62 }
63};
64
46export default RankingEntry; 65export default RankingEntry;
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..88a5297 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,99 +1,80 @@
1import React from 'react'; 1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from "react-router-dom";
3 3
4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; 4import {
5import Login from '@components/Login'; 5 BookIcon,
6import { UserProfile } from '@customTypes/Profile'; 6 FlagIcon,
7import { Search } from '@customTypes/Search'; 7 HelpIcon,
8import { API } from '@api/Api'; 8 HomeIcon,
9import "@css/Sidebar.css"; 9 LogoIcon,
10 PortalIcon,
11 SearchIcon,
12 UploadIcon,
13} from "../images/Images";
14import Login from "@components/Login";
15import { UserProfile } from "@customTypes/Profile";
16import { Search } from "@customTypes/Search";
17import { API } from "@api/Api";
10 18
11interface SidebarProps { 19interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 20 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile; 21 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 22 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15 onUploadRun: () => void; 23 onUploadRun: () => void;
16}; 24}
25
26function OpenSidebarIcon(){
27 return (
28 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/></svg>
29 )
30}
17 31
18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { 32function ClosedSidebarIcon(){
33 return (
34<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg> )
35}
19 36
20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 37const Sidebar: React.FC<SidebarProps> = ({
38 setToken,
39 profile,
40 setProfile,
41 onUploadRun,
42}) => {
43 const [searchData, setSearchData] = React.useState<Search | undefined>(
44 undefined
45 );
21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 46 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
22 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
23 49
24 const location = useLocation(); 50 const location = useLocation();
25 const path = location.pathname; 51 const path = location.pathname;
26 52
27 const handle_sidebar_click = (clicked_sidebar_idx: number) => { 53 const sidebarRef = useRef<HTMLDivElement>(null);
28 const btn = document.querySelectorAll("button.sidebar-button"); 54 const searchbarRef = useRef<HTMLInputElement>(null);
29 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } 55 const uploadRunRef = useRef<HTMLButtonElement>(null);
30 // clusterfuck 56 const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
31 btn.forEach((e, i) => {
32 btn[i].classList.remove("sidebar-button-selected")
33 btn[i].classList.add("sidebar-button-deselected")
34 })
35 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected")
36 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected")
37 };
38 57
39 const _handle_sidebar_hide = () => { 58 const _handle_sidebar_toggle = useCallback(() => {
40 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> 59 if (!sidebarRef.current) return;
41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement>
42 const side = document.querySelector("#sidebar-list") as HTMLElement;
43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement;
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement;
46 60
47 if (isSidebarOpen) { 61 if (isSidebarOpen) {
48 if (profile) {
49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
50 login.style.opacity = "1"
51 uploadRunBtn.style.width = "310px"
52 uploadRunBtn.style.padding = "0.4em 0 0 11px"
53 uploadRunSpan.style.opacity = "0"
54 setTimeout(() => {
55 uploadRunSpan.style.opacity = "1"
56 }, 100)
57 }
58 setSidebarOpen(false); 62 setSidebarOpen(false);
59 side.style.width = "320px"
60 btn.forEach((e, i) => {
61 e.style.width = "310px"
62 e.style.padding = "0.4em 0 0 11px"
63 setTimeout(() => {
64 span[i].style.opacity = "1"
65 }, 100)
66 });
67 side.style.zIndex = "2"
68 } else { 63 } else {
69 if (profile) {
70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
71 login.style.opacity = "0"
72 uploadRunBtn.style.width = "40px"
73 uploadRunBtn.style.padding = "0.4em 0 0 5px"
74 uploadRunSpan.style.opacity = "0"
75 }
76 setSidebarOpen(true); 64 setSidebarOpen(true);
77 side.style.width = "40px"; 65 searchbarRef.current?.focus();
78 searchbar.focus();
79 btn.forEach((e, i) => {
80 e.style.width = "40px"
81 e.style.padding = "0.4em 0 0 5px"
82 span[i].style.opacity = "0"
83 })
84 setTimeout(() => {
85 side.style.zIndex = "0"
86 }, 300);
87 } 66 }
88 }; 67 }, [isSidebarOpen]);
89 68
90 const _handle_sidebar_lock = () => { 69 const handle_sidebar_click = useCallback(
91 if (!isSidebarLocked) { 70 (clicked_sidebar_idx: number) => {
92 _handle_sidebar_hide() 71 setSelectedButtonIndex(clicked_sidebar_idx);
93 setIsSidebarLocked(true); 72 if (isSidebarOpen) {
94 setTimeout(() => setIsSidebarLocked(false), 300); 73 setSidebarOpen(false);
95 } 74 }
96 }; 75 },
76 [isSidebarOpen]
77 );
97 78
98 const _handle_search_change = async (q: string) => { 79 const _handle_search_change = async (q: string) => {
99 const searchResponse = await API.get_search(q); 80 const searchResponse = await API.get_search(q);
@@ -101,100 +82,200 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
101 }; 82 };
102 83
103 React.useEffect(() => { 84 React.useEffect(() => {
104 if (path === "/") { handle_sidebar_click(1) } 85 if (path === "/") {
105 else if (path.includes("games")) { handle_sidebar_click(2) } 86 setSelectedButtonIndex(1);
106 else if (path.includes("rankings")) { handle_sidebar_click(3) } 87 } else if (path.includes("games")) {
107 // else if (path.includes("news")) { handle_sidebar_click(4) } 88 setSelectedButtonIndex(2);
108 // else if (path.includes("scorelog")) { handle_sidebar_click(5) } 89 } else if (path.includes("rankings")) {
109 else if (path.includes("profile")) { handle_sidebar_click(4) } 90 setSelectedButtonIndex(3);
110 else if (path.includes("rules")) { handle_sidebar_click(5) } 91 } else if (path.includes("profile")) {
111 else if (path.includes("about")) { handle_sidebar_click(6) } 92 setSelectedButtonIndex(4);
93 } else if (path.includes("rules")) {
94 setSelectedButtonIndex(5);
95 } else if (path.includes("about")) {
96 setSelectedButtonIndex(6);
97 }
112 }, [path]); 98 }, [path]);
113 99
114 return ( 100 const getButtonClasses = (buttonIndex: number) => {
115 <div id='sidebar'> 101 const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1";
116 <Link to="/" tabIndex={-1}> 102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
117 <div id='logo'> {/* logo */} 103
118 <img src={LogoIcon} alt="" height={"80px"} /> 104 return `${baseClasses} ${selectedClasses}`;
119 <div id='logo-text'> 105 };
120 <span><b>PORTAL 2</b></span><br />
121 <span>Least Portals Hub</span>
122 </div>
123 </div>
124 </Link>
125 <div id='sidebar-list'> {/* List */}
126 <div id='sidebar-toplist'> {/* Top */}
127
128 <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
129
130 <span></span>
131 106
132 <Link to="/" tabIndex={-1}> 107 const iconClasses = "w-6 h-6 flex-shrink-0";
133 <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
134 </Link>
135 108
136 <Link to="/games" tabIndex={-1}> 109 return (
137 <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> 110 <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${
138 </Link> 111 isSidebarOpen ? 'w-80' : 'w-20'
112 }`}>
113 <div className="flex items-center h-20 px-4 border-b border-border">
114 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
115 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
116 {isSidebarOpen && (
117 <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden">
118 <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate">
119 PORTAL 2
120 </div>
121 <div className="text-sm leading-4 truncate">
122 Least Portals Hub
123 </div>
124 </div>
125 )}
126 </Link>
127
128 <button
129 onClick={_handle_sidebar_toggle}
130 className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground"
131 title={isSidebarOpen ? "Close sidebar" : "Open sidebar"}
132 >
133 {isSidebarOpen ? <ClosedSidebarIcon /> : <OpenSidebarIcon />}
134 </button>
135 </div>
139 136
140 <Link to="/rankings" tabIndex={-1}> 137 {/* Sidebar Content */}
141 <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> 138 <div
142 </Link> 139 ref={sidebarRef}
140 className="flex flex-col h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden"
141 >
142 {isSidebarOpen && (
143 <div className="p-4 border-b border-border min-w-0">
144 <div className="flex items-center gap-3 mb-3">
145 <img src={SearchIcon} alt="Search" className={iconClasses} />
146 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span>
147 </div>
148
149 <div className="min-w-0">
150 <input
151 ref={searchbarRef}
152 type="text"
153 id="searchbar"
154 placeholder="Search for map or a player..."
155 onChange={e => _handle_search_change(e.target.value)}
156 className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0"
157 />
143 158
144 {/* <Link to="/news" tabIndex={-1}> 159 {searchData && (
145 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> 160 <div className="mt-2 max-h-40 overflow-y-auto min-w-0">
146 </Link> */} 161 {searchData?.maps.map((q, index) => (
162 <Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}>
163 <span className="block text-xs text-subtext1 truncate">{q.game}</span>
164 <span className="block text-xs text-subtext1 truncate">{q.chapter}</span>
165 <span className="block text-sm text-foreground truncate">{q.map}</span>
166 </Link>
167 ))}
168 {searchData?.players.map((q, index) => (
169 <Link
170 to={
171 profile && q.steam_id === profile.steam_id
172 ? `/profile`
173 : `/users/${q.steam_id}`
174 }
175 className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0"
176 key={index}
177 >
178 <img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" />
179 <span className="text-sm text-foreground truncate">
180 {q.user_name}
181 </span>
182 </Link>
183 ))}
184 </div>
185 )}
186 </div>
187 </div>
188 )}
147 189
148 {/* <Link to="/scorelog" tabIndex={-1}> 190 <div className="flex-1 p-4 min-w-0">
149 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button> 191 <nav className="space-y-2">
150 </Link> */} 192 {[
193 {
194 to: "/",
195 refIndex: 1,
196 icon: HomeIcon,
197 alt: "Home",
198 label: "Home Page",
199 },
200 {
201 to: "/games",
202 refIndex: 2,
203 icon: PortalIcon,
204 alt: "Games",
205 label: "Games",
206 },
207 {
208 to: "/rankings",
209 refIndex: 3,
210 icon: FlagIcon,
211 alt: "Rankings",
212 label: "Rankings",
213 },
214 ].map(({ to, refIndex, icon, alt, label }) => (
215 <Link to={to} tabIndex={-1} key={refIndex}>
216 <button
217 ref={el => sidebarButtonRefs.current[refIndex] = el}
218 className={getButtonClasses(refIndex)}
219 onClick={() => handle_sidebar_click(refIndex)}
220 >
221 <img src={icon} alt={alt} className={iconClasses} />
222 {isSidebarOpen && (
223 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">
224 {label}
225 </span>
226 )}
227 </button>
228 </Link>
229 ))}
230 </nav>
151 </div> 231 </div>
152 <div id='sidebar-bottomlist'>
153 <span></span>
154 232
155 { 233 {/* Bottom Section */}
156 profile && profile.profile ? 234 <div className="p-4 border-t border-border space-y-2 min-w-0">
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button> 235 {profile && profile.profile && (
158 : 236 <button
159 <span></span> 237 ref={uploadRunRef}
160 } 238 id="upload-run"
239 className={getButtonClasses(-1)}
240 onClick={() => onUploadRun()}
241 >
242 <img src={UploadIcon} alt="Upload" className={iconClasses} />
243 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>}
244 </button>
245 )}
161 246
162 <Login setToken={setToken} profile={profile} setProfile={setProfile} /> 247 <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}>
248 <Login
249 setToken={setToken}
250 profile={profile}
251 setProfile={setProfile}
252 isOpen={isSidebarOpen}
253 />
254 </div>
163 255
164 <Link to="/rules" tabIndex={-1}> 256 <Link to="/rules" tabIndex={-1}>
165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button> 257 <button
258 ref={el => sidebarButtonRefs.current[5] = el}
259 className={getButtonClasses(5)}
260 onClick={() => handle_sidebar_click(5)}
261 >
262 <img src={BookIcon} alt="Rules" className={iconClasses} />
263 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>}
264 </button>
166 </Link> 265 </Link>
167 266
168 <Link to="/about" tabIndex={-1}> 267 <Link to="/about" tabIndex={-1}>
169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button> 268 <button
269 ref={el => sidebarButtonRefs.current[6] = el}
270 className={getButtonClasses(6)}
271 onClick={() => handle_sidebar_click(6)}
272 >
273 <img src={HelpIcon} alt="About" className={iconClasses} />
274 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>}
275 </button>
170 </Link> 276 </Link>
171 </div> 277 </div>
172 </div> 278 </div>
173 <div>
174 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
175
176 <div id='search-data'>
177
178 {searchData?.maps.map((q, index) => (
179 <Link to={`/maps/${q.id}`} className='search-map' key={index}>
180 <span>{q.game}</span>
181 <span>{q.chapter}</span>
182 <span>{q.map}</span>
183 </Link>
184 ))}
185 {searchData?.players.map((q, index) =>
186 (
187 <Link to={
188 profile && q.steam_id === profile.steam_id ? `/profile` :
189 `/users/${q.steam_id}`
190 } className='search-player' key={index}>
191 <img src={q.avatar_link} alt='pfp'></img>
192 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
193 </Link>
194 ))}
195
196 </div>
197 </div>
198 </div> 279 </div>
199 ); 280 );
200}; 281};
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 7da2f1e..ba91f57 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -1,193 +1,267 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3 3
4import { MapSummary } from '@customTypes/Map'; 4import { MapSummary } from "@customTypes/Map";
5import "@css/Maps.css"
6 5
7interface SummaryProps { 6interface SummaryProps {
8 selectedRun: number 7 selectedRun: number;
9 setSelectedRun: (x: number) => void; 8 setSelectedRun: (x: number) => void;
10 data: MapSummary; 9 data: MapSummary;
11} 10}
12 11
13const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { 12const Summary: React.FC<SummaryProps> = ({
14 13 selectedRun,
14 setSelectedRun,
15 data,
16}) => {
15 const [selectedCategory, setSelectedCategory] = React.useState<number>(1); 17 const [selectedCategory, setSelectedCategory] = React.useState<number>(1);
16 const [historySelected, setHistorySelected] = React.useState<boolean>(false); 18 const [historySelected, setHistorySelected] = React.useState<boolean>(false);
17 19
18 function _select_run(idx: number, category_id: number) { 20 const _select_run = React.useCallback(
19 let r = document.querySelectorAll("button.record"); 21 (idx: number, category_id: number) => {
20 r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); 22 let r = document.querySelectorAll("button.record");
21 (r[idx] as HTMLElement).style.backgroundColor = "#161723" 23 r.forEach(e => ((e as HTMLElement).style.backgroundColor = "#2b2e46"));
22 24 (r[idx] as HTMLElement).style.backgroundColor = "#161723";
23 25
24 if (data && data.summary.routes.length !== 0) { 26 if (data && data.summary.routes.length !== 0) {
25 idx += data.summary.routes.filter(e => e.category.id < category_id).length // lethimcook 27 idx += data.summary.routes.filter(
26 setSelectedRun(idx); 28 e => e.category.id < category_id
27 } 29 ).length; // lethimcook
28 }; 30 setSelectedRun(idx);
31 }
32 },
33 [data, setSelectedRun]
34 );
29 35
30 function _get_youtube_id(url: string): string { 36 function _get_youtube_id(url: string): string {
31 const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); 37 const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
32 return (urlArray[2] !== undefined) ? urlArray[2].split(/[^0-9a-z_-]/i)[0] : urlArray[0]; 38 return urlArray[2] !== undefined
33 }; 39 ? urlArray[2].split(/[^0-9a-z_-]/i)[0]
40 : urlArray[0];
41 }
34 42
35 function _category_change() { 43 const _category_change = React.useCallback(() => {
36 const btn = document.querySelectorAll("#section3 #category span button"); 44 const btn = document.querySelectorAll("#section3 #category span button");
37 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 45 btn.forEach(e => {
46 (e as HTMLElement).style.backgroundColor = "#2b2e46";
47 });
38 // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories 48 // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories
39 const idx = selectedCategory === 1 ? 0 : data.map.is_coop ? selectedCategory - 3 : selectedCategory - 1; 49 const idx =
50 selectedCategory === 1
51 ? 0
52 : data.map.is_coop
53 ? selectedCategory - 3
54 : selectedCategory - 1;
40 (btn[idx] as HTMLElement).style.backgroundColor = "#202232"; 55 (btn[idx] as HTMLElement).style.backgroundColor = "#202232";
41 }; 56 }, [selectedCategory, data.map.is_coop]);
42 57
43 function _history_change() { 58 const _history_change = React.useCallback(() => {
44 const btn = document.querySelectorAll("#section3 #history span button"); 59 const btn = document.querySelectorAll("#section3 #history span button");
45 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 60 btn.forEach(e => {
46 (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; 61 (e as HTMLElement).style.backgroundColor = "#2b2e46";
47 }; 62 });
63 (historySelected
64 ? (btn[1] as HTMLElement)
65 : (btn[0] as HTMLElement)
66 ).style.backgroundColor = "#202232";
67 }, [historySelected]);
48 68
49 React.useEffect(() => { 69 React.useEffect(() => {
50 _history_change(); 70 _history_change();
51 }, [historySelected]); 71 }, [historySelected, _history_change]);
52 72
53 React.useEffect(() => { 73 React.useEffect(() => {
54 _category_change(); 74 _category_change();
55 _select_run(0, selectedCategory); 75 _select_run(0, selectedCategory);
56 }, [selectedCategory]); 76 }, [selectedCategory, _category_change, _select_run]);
57 77
58 React.useEffect(() => { 78 React.useEffect(() => {
59 _select_run(0, selectedCategory); 79 _select_run(0, selectedCategory);
60 }, []); 80 }, [_select_run, selectedCategory]);
61 81
62 return ( 82 return (
63 <> 83 <>
64 <section id='section3' className='summary1'> 84 <section id="section3" className="summary1 text-foreground">
65 <div id='category' 85 <div
66 style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> 86 id="category"
67 <img src={data.map.image} alt="" id='category-image'></img> 87 style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}
68 <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> 88 >
69 {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> 89 <img src={data.map.image} alt="" id="category-image"></img>
70 {data.map.is_coop ? // TODO: make this part dynamic 90 <p>
71 ( 91 <span className="portal-count">
72 <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> 92 {data.summary.routes[selectedRun].history.score_count}
73 <button onClick={() => setSelectedCategory(1)}>CM</button> 93 </span>
74 <button onClick={() => setSelectedCategory(4)}>Any%</button> 94 {data.summary.routes[selectedRun].history.score_count === 1
75 <button onClick={() => setSelectedCategory(5)}>All Courses</button> 95 ? ` portal`
76 </span> 96 : ` portals`}
77 ) 97 </p>
78 : 98 {data.map.is_coop ? ( // TODO: make this part dynamic
79 ( 99 <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
80 <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}> 100 <button onClick={() => setSelectedCategory(1)}>CM</button>
81 101 <button onClick={() => setSelectedCategory(4)}>Any%</button>
82 <button onClick={() => setSelectedCategory(1)}>CM</button> 102 <button onClick={() => setSelectedCategory(5)}>
83 <button onClick={() => setSelectedCategory(2)}>NoSLA</button> 103 All Courses
84 <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button> 104 </button>
85 <button onClick={() => setSelectedCategory(4)}>Any%</button> 105 </span>
86 </span> 106 ) : (
87 ) 107 <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}>
88 } 108 <button onClick={() => setSelectedCategory(1)}>CM</button>
89 109 <button onClick={() => setSelectedCategory(2)}>NoSLA</button>
110 <button onClick={() => setSelectedCategory(3)}>
111 Inbounds SLA
112 </button>
113 <button onClick={() => setSelectedCategory(4)}>Any%</button>
114 </span>
115 )}
90 </div> 116 </div>
91 117
92 <div id='history'> 118 <div id="history">
93
94 <div style={{ display: historySelected ? "none" : "block" }}> 119 <div style={{ display: historySelected ? "none" : "block" }}>
95 {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : 120 {data.summary.routes.filter(e => e.category.id === selectedCategory)
121 .length === 0 ? (
122 <h5>There are no records for this map.</h5>
123 ) : (
96 <> 124 <>
97 <div className='record-top'> 125 <div className="record-top">
98 <span>Date</span> 126 <span>Date</span>
99 <span>Record</span> 127 <span>Record</span>
100 <span>First Completion</span> 128 <span>First Completion</span>
101 </div> 129 </div>
102 <hr /> 130 <hr />
103 <div id='records'> 131 <div id="records">
104
105 {data.summary.routes 132 {data.summary.routes
106 .filter(e => e.category.id === selectedCategory) 133 .filter(e => e.category.id === selectedCategory)
107 .map((r, index) => ( 134 .map((r, index) => (
108 <button className='record' key={index} onClick={() => { 135 <button
109 _select_run(index, r.category.id); 136 className="record"
110 }}> 137 key={index}
111 <span>{new Date(r.history.date).toLocaleDateString( 138 onClick={() => {
112 "en-US", { month: 'long', day: 'numeric', year: 'numeric' } 139 _select_run(index, r.category.id);
113 )}</span> 140 }}
141 >
142 <span>
143 {new Date(r.history.date).toLocaleDateString(
144 "en-US",
145 { month: "long", day: "numeric", year: "numeric" }
146 )}
147 </span>
114 <span>{r.history.score_count}</span> 148 <span>{r.history.score_count}</span>
115 <span>{r.history.runner_name}</span> 149 <span>{r.history.runner_name}</span>
116 </button> 150 </button>
117 ))} 151 ))}
118 </div> 152 </div>
119 </> 153 </>
120 } 154 )}
121 </div> 155 </div>
122 156
123 <div style={{ display: historySelected ? "block" : "none" }}> 157 <div style={{ display: historySelected ? "block" : "none" }}>
124 {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : 158 {data.summary.routes.filter(e => e.category.id === selectedCategory)
125 <div id='graph'> 159 .length === 0 ? (
160 <h5>There are no records for this map.</h5>
161 ) : (
162 <div id="graph">
126 {/* <div>{graph(1)}</div> 163 {/* <div>{graph(1)}</div>
127 <div>{graph(2)}</div> 164 <div>{graph(2)}</div>
128 <div>{graph(3)}</div> */} 165 <div>{graph(3)}</div> */}
129 </div> 166 </div>
130 } 167 )}
131 </div> 168 </div>
132 <span> 169 <span>
133 <button onClick={() => setHistorySelected(false)}>List</button> 170 <button onClick={() => setHistorySelected(false)}>List</button>
134 <button onClick={() => setHistorySelected(true)}>Graph</button> 171 <button onClick={() => setHistorySelected(true)}>Graph</button>
135 </span> 172 </span>
136 </div> 173 </div>
137 174 </section>
138 175 <section id="section4" className="summary1">
139 </section > 176 <div id="difficulty">
140 <section id='section4' className='summary1'> 177 <span className="">Difficulty</span>
141 <div id='difficulty'> 178 {data.map.difficulty <= 2 && (
142 <span>Difficulty</span> 179 <span style={{ color: "lime" }}>Very Easy</span>
143 {data.map.difficulty <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)} 180 )}
144 {data.map.difficulty > 2 && data.map.difficulty <= 4 && (<span style={{ color: "green" }}>Easy</span>)} 181 {data.map.difficulty > 2 && data.map.difficulty <= 4 && (
145 {data.map.difficulty > 4 && data.map.difficulty <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)} 182 <span style={{ color: "green" }}>Easy</span>
146 {data.map.difficulty > 6 && data.map.difficulty <= 8 && (<span style={{ color: "orange" }}>Hard</span>)} 183 )}
147 {data.map.difficulty > 8 && data.map.difficulty <= 10 && (<span style={{ color: "red" }}>Very hard</span>)} 184 {data.map.difficulty > 4 && data.map.difficulty <= 6 && (
185 <span style={{ color: "yellow" }}>Medium</span>
186 )}
187 {data.map.difficulty > 6 && data.map.difficulty <= 8 && (
188 <span style={{ color: "orange" }}>Hard</span>
189 )}
190 {data.map.difficulty > 8 && data.map.difficulty <= 10 && (
191 <span style={{ color: "red" }}>Very Hard</span>
192 )}
148 <div> 193 <div>
149 {data.map.difficulty <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} 194 {data.map.difficulty <= 2 && ? (
150 {data.map.difficulty > 2 && data.map.difficulty <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} 195 <div
151 {data.map.difficulty > 4 && data.map.difficulty <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} 196 className="difficulty-rating"
152 {data.map.difficulty > 6 && data.map.difficulty <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} 197 style={{ backgroundColor: "lime" }}
153 {data.map.difficulty > 8 && data.map.difficulty <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} 198 ></div>
199 ) : (
200 <div className="difficulty-rating"></div>
201 )}
202 {data.map.difficulty > 2 && data.map.difficulty <= 4 && ? (
203 <div
204 className="difficulty-rating"
205 style={{ backgroundColor: "green" }}
206 ></div>
207 ) : (
208 <div className="difficulty-rating"></div>
209 )}
210 {data.map.difficulty > 4 && data.map.difficulty <= 6 && ? (
211 <div
212 className="difficulty-rating"
213 style={{ backgroundColor: "yellow" }}
214 ></div>
215 ) : (
216 <div className="difficulty-rating"></div>
217 )}
218 {data.map.difficulty > 6 && data.map.difficulty <= 8 && ? (
219 <div
220 className="difficulty-rating"
221 style={{ backgroundColor: "orange" }}
222 ></div>
223 ) : (
224 <div className="difficulty-rating"></div>
225 )}
226 {data.map.difficulty > 8 && data.map.difficulty <= 10 && ? (
227 <div
228 className="difficulty-rating"
229 style={{ backgroundColor: "red" }}
230 ></div>
231 ) : (
232 <div className="difficulty-rating"></div>
233 )}
154 </div> 234 </div>
155 </div> 235 </div>
156 {/* <div id='difficulty'> 236 <div id="count">
157 <span>Difficulty</span>
158 {data.summary.routes[selectedRun].rating <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)}
159 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 && (<span style={{ color: "green" }}>Easy</span>)}
160 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)}
161 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 && (<span style={{ color: "orange" }}>Hard</span>)}
162 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 && (<span style={{ color: "red" }}>Very hard</span>)}
163 <div>
164 {data.summary.routes[selectedRun].rating <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)}
165 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)}
166 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)}
167 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)}
168 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)}
169 </div>
170 </div> */}
171 <div id='count'>
172 <span>Completion Count</span> 237 <span>Completion Count</span>
173 <div>{data.summary.routes[selectedRun].completion_count}</div> 238 <div>{data.summary.routes[selectedRun].completion_count}</div>
174 </div> 239 </div>
175 </section> 240 </section>
176 241
177 <section id='section5' className='summary1'> 242 <section id="section5" className="summary1">
178 <div id='description'> 243 <div id="description">
179 {data.summary.routes[selectedRun].showcase !== "" ? 244 {data.summary.routes[selectedRun].showcase !== "" ? (
180 <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> 245 <iframe
181 : ""} 246 title="Showcase video"
182 <h3>Route Description</h3> 247 src={
183 <span id='description-text'> 248 "https://www.youtube.com/embed/" +
184 <ReactMarkdown> 249 _get_youtube_id(data.summary.routes[selectedRun].showcase)
250 }
251 >
252 {" "}
253 </iframe>
254 ) : (
255 ""
256 )}
257 <h3 className="font-semibold">Route Description</h3>
258 <span id="description-text">
259 <ReactMarkdown className="text-foreground">
185 {data.summary.routes[selectedRun].description} 260 {data.summary.routes[selectedRun].description}
186 </ReactMarkdown> 261 </ReactMarkdown>
187 </span> 262 </span>
188 </div> 263 </div>
189 </section> 264 </section>
190
191 </> 265 </>
192 ); 266 );
193}; 267};
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index c02fdb8..0034019 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -1,15 +1,14 @@
1import React from 'react'; 1import React from "react";
2import { UploadRunContent } from '@customTypes/Content'; 2import { UploadRunContent } from "@customTypes/Content";
3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; 3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp";
4 4
5import '@css/UploadRunDialog.css'; 5import { Game } from "@customTypes/Game";
6import { Game } from '@customTypes/Game'; 6import { API } from "@api/Api";
7import { API } from '@api/Api'; 7import { useNavigate } from "react-router-dom";
8import { useNavigate } from 'react-router-dom'; 8import useMessage from "@hooks/UseMessage";
9import useMessage from '@hooks/UseMessage'; 9import useConfirm from "@hooks/UseConfirm";
10import useConfirm from '@hooks/UseConfirm';
11import useMessageLoad from "@hooks/UseMessageLoad"; 10import useMessageLoad from "@hooks/UseMessageLoad";
12import { MapNames } from '@customTypes/MapNames'; 11import { MapNames } from "@customTypes/MapNames";
13 12
14interface UploadRunDialogProps { 13interface UploadRunDialogProps {
15 token?: string; 14 token?: string;
@@ -18,18 +17,24 @@ interface UploadRunDialogProps {
18 games: Game[]; 17 games: Game[];
19} 18}
20 19
21const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { 20const UploadRunDialog: React.FC<UploadRunDialogProps> = ({
22 21 token,
22 open,
23 onClose,
24 games,
25}) => {
23 const { message, MessageDialogComponent } = useMessage(); 26 const { message, MessageDialogComponent } = useMessage();
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 27 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 28 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
29 useMessageLoad();
26 30
27 const navigate = useNavigate(); 31 const navigate = useNavigate();
28 32
29 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 33 const [uploadRunContent, setUploadRunContent] =
30 host_demo: null, 34 React.useState<UploadRunContent>({
31 partner_demo: null, 35 host_demo: null,
32 }); 36 partner_demo: null,
37 });
33 38
34 const [selectedGameID, setSelectedGameID] = React.useState<number>(0); 39 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
35 const [selectedGameName, setSelectedGameName] = React.useState<string>(""); 40 const [selectedGameName, setSelectedGameName] = React.useState<string>("");
@@ -41,7 +46,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
41 const [loading, setLoading] = React.useState<boolean>(false); 46 const [loading, setLoading] = React.useState<boolean>(false);
42 47
43 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); 48 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false);
44 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); 49 const [dragHightlightPartner, setDragHighlightPartner] =
50 React.useState<boolean>(false);
45 51
46 const fileInputRef = React.useRef<HTMLInputElement>(null); 52 const fileInputRef = React.useRef<HTMLInputElement>(null);
47 const fileInputRefPartner = React.useRef<HTMLInputElement>(null); 53 const fileInputRefPartner = React.useRef<HTMLInputElement>(null);
@@ -52,9 +58,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
52 } else { 58 } else {
53 fileInputRefPartner.current?.click(); 59 fileInputRefPartner.current?.click();
54 } 60 }
55 } 61 };
56 62
57 const _handle_drag_over = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 63 const _handle_drag_over = (
64 e: React.DragEvent<HTMLDivElement>,
65 host: boolean
66 ) => {
58 e.preventDefault(); 67 e.preventDefault();
59 e.stopPropagation(); 68 e.stopPropagation();
60 if (host) { 69 if (host) {
@@ -62,9 +71,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
62 } else { 71 } else {
63 setDragHighlightPartner(true); 72 setDragHighlightPartner(true);
64 } 73 }
65 } 74 };
66 75
67 const _handle_drag_leave = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 76 const _handle_drag_leave = (
77 e: React.DragEvent<HTMLDivElement>,
78 host: boolean
79 ) => {
68 e.preventDefault(); 80 e.preventDefault();
69 e.stopPropagation(); 81 e.stopPropagation();
70 if (host) { 82 if (host) {
@@ -72,7 +84,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
72 } else { 84 } else {
73 setDragHighlightPartner(false); 85 setDragHighlightPartner(false);
74 } 86 }
75 } 87 };
76 88
77 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 89 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => {
78 e.preventDefault(); 90 e.preventDefault();
@@ -80,18 +92,18 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
80 setDragHighlight(true); 92 setDragHighlight(true);
81 93
82 _handle_file_change(e.dataTransfer.files, host); 94 _handle_file_change(e.dataTransfer.files, host);
83 } 95 };
84 96
85 const _handle_dropdowns = (dropdown: number) => { 97 const _handle_dropdowns = (dropdown: number) => {
86 setDropdown1Vis(false); 98 setDropdown1Vis(false);
87 setDropdown2Vis(false); 99 setDropdown2Vis(false);
88 if (dropdown == 1) { 100 if (dropdown === 1) {
89 setDropdown1Vis(!dropdown1Vis); 101 setDropdown1Vis(!dropdown1Vis);
90 } else if (dropdown == 2) { 102 } else if (dropdown === 2) {
91 setDropdown2Vis(!dropdown2Vis); 103 setDropdown2Vis(!dropdown2Vis);
92 document.querySelector("#dropdown2")?.scrollTo(0, 0); 104 document.querySelector("#dropdown2")?.scrollTo(0, 0);
93 } 105 }
94 } 106 };
95 107
96 const _handle_game_select = async (game_id: string, game_name: string) => { 108 const _handle_game_select = async (game_id: string, game_name: string) => {
97 setLoading(true); 109 setLoading(true);
@@ -120,53 +132,76 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
120 if (token) { 132 if (token) {
121 if (games[selectedGameID].is_coop) { 133 if (games[selectedGameID].is_coop) {
122 if (uploadRunContent.host_demo === null) { 134 if (uploadRunContent.host_demo === null) {
123 await message("Error", "You must select a host demo to upload.") 135 await message("Error", "You must select a host demo to upload.");
124 return 136 return;
125 } else if (uploadRunContent.partner_demo === null) { 137 } else if (uploadRunContent.partner_demo === null) {
126 await message("Error", "You must select a partner demo to upload.") 138 await message("Error", "You must select a partner demo to upload.");
127 return 139 return;
128 } 140 }
129 } else { 141 } else {
130 if (uploadRunContent.host_demo === null) { 142 if (uploadRunContent.host_demo === null) {
131 await message("Error", "You must select a demo to upload.") 143 await message("Error", "You must select a demo to upload.");
132 return 144 return;
133 } 145 }
134 } 146 }
135 const demo = SourceDemoParser.default() 147 const demo = SourceDemoParser.default()
136 .setOptions({ packets: true, header: true }) 148 .setOptions({ packets: true, header: true })
137 .parse(await uploadRunContent.host_demo.arrayBuffer()); 149 .parse(await uploadRunContent.host_demo.arrayBuffer());
138 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => { 150 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>(msg => {
139 return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate; 151 return (
140 }) 152 msg instanceof NetMessages.SvcUserMessage &&
153 msg.userMessage instanceof ScoreboardTempUpdate
154 );
155 });
141 156
142 if (!scoreboard) { 157 if (!scoreboard) {
143 await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") 158 await message(
144 return 159 "Error",
160 "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode."
161 );
162 return;
145 } 163 }
146 164
147 if (!demo.mapName || !MapNames[demo.mapName]) { 165 if (!demo.mapName || !MapNames[demo.mapName]) {
148 await message("Error", "Error while processing demo: Invalid map name.") 166 await message(
149 return 167 "Error",
168 "Error while processing demo: Invalid map name."
169 );
170 return;
150 } 171 }
151 172
152 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { 173 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) {
153 await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") 174 await message(
154 return 175 "Error",
176 "Error while processing demo: Invalid cooperative demo in singleplayer submission."
177 );
178 return;
155 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { 179 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) {
156 await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") 180 await message(
157 return 181 "Error",
182 "Error while processing demo: Invalid singleplayer demo in cooperative submission."
183 );
184 return;
158 } 185 }
159 186
160 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 187 const { portalScore, timeScore } =
188 scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
161 189
162 const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); 190 const userConfirmed = await confirm(
191 "Upload Record",
192 `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`
193 );
163 194
164 if (!userConfirmed) { 195 if (!userConfirmed) {
165 return; 196 return;
166 } 197 }
167 198
168 messageLoad("Uploading..."); 199 messageLoad("Uploading...");
169 const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); 200 const [success, response] = await API.post_record(
201 token,
202 uploadRunContent,
203 MapNames[demo.mapName]
204 );
170 messageLoadClose(); 205 messageLoadClose();
171 await message("Upload Record", response); 206 await message("Upload Record", response);
172 if (success) { 207 if (success) {
@@ -196,84 +231,191 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
196 {MessageDialogLoadComponent} 231 {MessageDialogLoadComponent}
197 {ConfirmDialogComponent} 232 {ConfirmDialogComponent}
198 233
199 <div id='upload-run-menu'> 234 <div id="upload-run-menu">
200 <div id='upload-run-menu-add'> 235 <div id="upload-run-menu-add">
201 <div id='upload-run-route-category'> 236 <div id="upload-run-route-category">
202 <div style={{ padding: "15px 0px" }} className='upload-run-dropdown-container upload-run-item'> 237 <div
238 style={{ padding: "15px 0px" }}
239 className="upload-run-dropdown-container upload-run-item"
240 >
203 <h3 style={{ margin: "0px 0px" }}>Select Game</h3> 241 <h3 style={{ margin: "0px 0px" }}>Select Game</h3>
204 <div onClick={() => _handle_dropdowns(1)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> 242 <div
205 <div className='dropdown-cur'>{selectedGameName}</div> 243 onClick={() => _handle_dropdowns(1)}
206 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> 244 style={{
245 display: "flex",
246 alignItems: "center",
247 cursor: "pointer",
248 justifyContent: "space-between",
249 margin: "10px 0px",
250 }}
251 >
252 <div className="dropdown-cur">{selectedGameName}</div>
253 <i
254 style={{
255 rotate: "-90deg",
256 transform: "translate(-5px, 10px)",
257 }}
258 className="triangle"
259 ></i>
207 </div> 260 </div>
208 <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> 261 <div
209 {games.map((game) => ( 262 style={{ top: "110px" }}
210 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> 263 className={
264 dropdown1Vis
265 ? "upload-run-dropdown"
266 : "upload-run-dropdown hidden"
267 }
268 >
269 {games.map(game => (
270 <div
271 onClick={() => {
272 _handle_game_select(game.id.toString(), game.name);
273 _handle_dropdowns(1);
274 }}
275 key={game.id}
276 >
277 {game.name}
278 </div>
211 ))} 279 ))}
212 </div> 280 </div>
213 </div> 281 </div>
214 282
215 { 283 {!loading && (
216 !loading && 284 <>
217 ( 285 <div>
218 <> 286 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3>
219 287 <div
220 <div> 288 onClick={() => {
221 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> 289 _handle_file_click(true);
222 <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> 290 }}
223 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> 291 onDragOver={e => {
224 {!uploadRunContent.host_demo ? 292 _handle_drag_over(e, true);
293 }}
294 onDrop={e => {
295 _handle_drop(e, true);
296 }}
297 onDragLeave={e => {
298 _handle_drag_leave(e, true);
299 }}
300 className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}
301 >
302 <input
303 ref={fileInputRef}
304 type="file"
305 name="host_demo"
306 id="host_demo"
307 accept=".dem"
308 onChange={e =>
309 _handle_file_change(e.target.files, true)
310 }
311 />
312 {!uploadRunContent.host_demo ? (
313 <div>
314 <span>Drag and drop</span>
225 <div> 315 <div>
226 <span>Drag and drop</span> 316 <span
227 <div> 317 style={{
228 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 318 fontFamily: "BarlowSemiCondensed-Regular",
229 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 319 }}
230 </div> 320 >
321 Or click here
322 </span>
323 <br />
324 <button
325 style={{
326 borderRadius: "24px",
327 padding: "5px 8px",
328 margin: "5px 0px",
329 }}
330 >
331 Upload
332 </button>
231 </div> 333 </div>
232 : null} 334 </div>
233 335 ) : null}
234 <span className="upload-run-demo-name">{uploadRunContent.host_demo?.name}</span>
235 </div>
236 {
237 games[selectedGameID].is_coop &&
238 (
239 <>
240 <div>
241 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
242 <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}>
243 <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ?
244 <div>
245 <span>Drag and drop</span>
246 <div>
247 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
248 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button>
249 </div>
250 </div>
251 : null}
252
253 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span>
254 </div>
255 </div>
256 </>
257 )
258 }
259 </div>
260 <div className='search-container'>
261 336
337 <span className="upload-run-demo-name">
338 {uploadRunContent.host_demo?.name}
339 </span>
262 </div> 340 </div>
263 341 {games[selectedGameID].is_coop && (
264 </> 342 <>
265 ) 343 <div>
266 } 344 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
345 <div
346 onClick={() => {
347 _handle_file_click(false);
348 }}
349 onDragOver={e => {
350 _handle_drag_over(e, false);
351 }}
352 onDrop={e => {
353 _handle_drop(e, false);
354 }}
355 onDragLeave={e => {
356 _handle_drag_leave(e, false);
357 }}
358 className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}
359 >
360 <input
361 ref={fileInputRefPartner}
362 type="file"
363 name="partner_demo"
364 id="partner_demo"
365 accept=".dem"
366 onChange={e =>
367 _handle_file_change(e.target.files, false)
368 }
369 />{" "}
370 {!uploadRunContent.partner_demo ? (
371 <div>
372 <span>Drag and drop</span>
373 <div>
374 <span
375 style={{
376 fontFamily: "BarlowSemiCondensed-Regular",
377 }}
378 >
379 Or click here
380 </span>
381 <br />
382 <button
383 style={{
384 borderRadius: "24px",
385 padding: "5px 8px",
386 margin: "5px 0px",
387 }}
388 >
389 Upload
390 </button>
391 </div>
392 </div>
393 ) : null}
394 <span className="upload-run-demo-name">
395 {uploadRunContent.partner_demo?.name}
396 </span>
397 </div>
398 </div>
399 </>
400 )}
401 </div>
402 <div className="search-container"></div>
403 </>
404 )}
267 </div> 405 </div>
268 <div className='upload-run-buttons-container'> 406 <div className="upload-run-buttons-container">
269 <button onClick={_upload_run}>Submit</button> 407 <button onClick={_upload_run}>Submit</button>
270 <button onClick={() => { 408 <button
271 onClose(false); 409 onClick={() => {
272 setUploadRunContent({ 410 onClose(false);
273 host_demo: null, 411 setUploadRunContent({
274 partner_demo: null, 412 host_demo: null,
275 }); 413 partner_demo: null,
276 }}>Cancel</button> 414 });
415 }}
416 >
417 Cancel
418 </button>
277 </div> 419 </div>
278 </div> 420 </div>
279 </div> 421 </div>
@@ -281,10 +423,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
281 ); 423 );
282 } 424 }
283 425
284 return ( 426 return <></>;
285 <></>
286 );
287
288}; 427};
289 428
290export default UploadRunDialog; 429export default UploadRunDialog;
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx
index e86d70d..4692d53 100644
--- a/frontend/src/hooks/UseConfirm.tsx
+++ b/frontend/src/hooks/UseConfirm.tsx
@@ -1,40 +1,47 @@
1import React, { useState } from 'react'; 1import React, { useState } from "react";
2import ConfirmDialog from '@components/ConfirmDialog'; 2import ConfirmDialog from "@components/ConfirmDialog";
3 3
4const useConfirm = () => { 4const useConfirm = () => {
5 const [isOpen, setIsOpen] = useState(false); 5 const [isOpen, setIsOpen] = useState(false);
6 const [title, setTitle] = useState<string>(""); 6 const [title, setTitle] = useState<string>("");
7 const [subtitle, setSubtitle] = useState<string>(""); 7 const [subtitle, setSubtitle] = useState<string>("");
8 const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null); 8 const [resolvePromise, setResolvePromise] = useState<
9 ((value: boolean) => void) | null
10 >(null);
9 11
10 const confirm = ( titleN: string, subtitleN: string ) => { 12 const confirm = (titleN: string, subtitleN: string) => {
11 setIsOpen(true); 13 setIsOpen(true);
12 setTitle(titleN); 14 setTitle(titleN);
13 setSubtitle(subtitleN); 15 setSubtitle(subtitleN);
14 return new Promise<boolean>((resolve) => { 16 return new Promise<boolean>(resolve => {
15 setResolvePromise(() => resolve); 17 setResolvePromise(() => resolve);
16 }); 18 });
17 }; 19 };
18 20
19 const handleConfirm = () => { 21 const handleConfirm = () => {
20 setIsOpen(false); 22 setIsOpen(false);
21 if (resolvePromise) { 23 if (resolvePromise) {
22 resolvePromise(true); 24 resolvePromise(true);
23 }
24 } 25 }
26 };
25 27
26 const handleCancel = () => { 28 const handleCancel = () => {
27 setIsOpen(false); 29 setIsOpen(false);
28 if (resolvePromise) { 30 if (resolvePromise) {
29 resolvePromise(false); 31 resolvePromise(false);
30 }
31 } 32 }
33 };
32 34
33 const ConfirmDialogComponent = isOpen && ( 35 const ConfirmDialogComponent = isOpen && (
34 <ConfirmDialog title={title} subtitle={subtitle} onConfirm={handleConfirm} onCancel={handleCancel}></ConfirmDialog> 36 <ConfirmDialog
35 ); 37 title={title}
38 subtitle={subtitle}
39 onConfirm={handleConfirm}
40 onCancel={handleCancel}
41 ></ConfirmDialog>
42 );
36 43
37 return { confirm, ConfirmDialogComponent }; 44 return { confirm, ConfirmDialogComponent };
38} 45};
39 46
40export default useConfirm; 47export default useConfirm;
diff --git a/frontend/src/hooks/UseMessage.tsx b/frontend/src/hooks/UseMessage.tsx
index 97ec746..b639fac 100644
--- a/frontend/src/hooks/UseMessage.tsx
+++ b/frontend/src/hooks/UseMessage.tsx
@@ -1,37 +1,43 @@
1import React, { useState } from 'react'; 1import React, { useState } from "react";
2import MessageDialog from "@components/MessageDialog"; 2import MessageDialog from "@components/MessageDialog";
3 3
4const useMessage = () => { 4const useMessage = () => {
5 const [isOpen, setIsOpen] = useState(false); 5 const [isOpen, setIsOpen] = useState(false);
6 6
7 const [title, setTitle] = useState<string>(""); 7 const [title, setTitle] = useState<string>("");
8 const [subtitle, setSubtitle] = useState<string>(""); 8 const [subtitle, setSubtitle] = useState<string>("");
9 const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); 9 const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(
10 null
11 );
10 12
11 const message = (title: string, subtitle: string) => { 13 const message = (title: string, subtitle: string) => {
12 setIsOpen(true); 14 setIsOpen(true);
13 setTitle(title); 15 setTitle(title);
14 setSubtitle(subtitle); 16 setSubtitle(subtitle);
15 return new Promise((resolve) => { 17 return new Promise(resolve => {
16 setResolvePromise(() => resolve); 18 setResolvePromise(() => resolve);
17 }); 19 });
18 }; 20 };
19 21
20 const handleClose = () => { 22 const handleClose = () => {
21 setIsOpen(false); 23 setIsOpen(false);
22 if (resolvePromise) { 24 if (resolvePromise) {
23 resolvePromise(); 25 resolvePromise();
24 setResolvePromise(null); 26 setResolvePromise(null);
25 } 27 }
26 }; 28 };
27 29
28 const MessageDialogComponent = isOpen && ( 30 const MessageDialogComponent = isOpen && (
29 <div className="dialog-container"> 31 <div className="dialog-container">
30 <MessageDialog title={title} subtitle={subtitle} onClose={handleClose}></MessageDialog> 32 <MessageDialog
31 </div> 33 title={title}
32 ); 34 subtitle={subtitle}
35 onClose={handleClose}
36 ></MessageDialog>
37 </div>
38 );
33 39
34 return { message, MessageDialogComponent }; 40 return { message, MessageDialogComponent };
35} 41};
36 42
37export default useMessage; 43export default useMessage;
diff --git a/frontend/src/hooks/UseMessageLoad.tsx b/frontend/src/hooks/UseMessageLoad.tsx
index 228c2b4..4cbd0f7 100644
--- a/frontend/src/hooks/UseMessageLoad.tsx
+++ b/frontend/src/hooks/UseMessageLoad.tsx
@@ -1,35 +1,40 @@
1import React, { useState } from 'react'; 1import React, { useState } from "react";
2import MessageDialogLoad from "@components/MessageDialogLoad"; 2import MessageDialogLoad from "@components/MessageDialogLoad";
3 3
4const useMessageLoad = () => { 4const useMessageLoad = () => {
5 const [isOpen, setIsOpen] = useState(false); 5 const [isOpen, setIsOpen] = useState(false);
6 6
7 const [title, setTitle] = useState<string>(""); 7 const [title, setTitle] = useState<string>("");
8 const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); 8 const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(
9 null
10 );
9 11
10 const messageLoad = (title: string) => { 12 const messageLoad = (title: string) => {
11 setIsOpen(true); 13 setIsOpen(true);
12 setTitle(title); 14 setTitle(title);
13 return new Promise((resolve) => { 15 return new Promise(resolve => {
14 setResolvePromise(() => resolve); 16 setResolvePromise(() => resolve);
15 }); 17 });
16 }; 18 };
17 19
18 const messageLoadClose = () => { 20 const messageLoadClose = () => {
19 setIsOpen(false); 21 setIsOpen(false);
20 if (resolvePromise) { 22 if (resolvePromise) {
21 resolvePromise(); 23 resolvePromise();
22 setResolvePromise(null); 24 setResolvePromise(null);
23 } 25 }
24 }; 26 };
25 27
26 const MessageDialogLoadComponent = isOpen && ( 28 const MessageDialogLoadComponent = isOpen && (
27 <div className="dialog-container"> 29 <div className="dialog-container">
28 <MessageDialogLoad title={title} onClose={messageLoadClose}></MessageDialogLoad> 30 <MessageDialogLoad
29 </div> 31 title={title}
30 ); 32 onClose={messageLoadClose}
33 ></MessageDialogLoad>
34 </div>
35 );
31 36
32 return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; 37 return { messageLoad, messageLoadClose, MessageDialogLoadComponent };
33} 38};
34 39
35export default useMessageLoad; 40export default useMessageLoad;
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx
index 198431b..6b46893 100644
--- a/frontend/src/images/Images.tsx
+++ b/frontend/src/images/Images.tsx
@@ -1,29 +1,29 @@
1import logo from "./png/logo.png" 1import logo from "./png/logo.png";
2import login from "./png/login.png" 2import { LoginIcon as Login } from "./svgs/steam.tsx";
3import img1 from './png/1.png'; 3import img1 from "./png/1.png";
4import img2 from './png/2.png'; 4import img2 from "./png/2.png";
5import img3 from './png/3.png'; 5import img3 from "./png/3.png";
6import img4 from './png/4.png'; 6import img4 from "./png/4.png";
7import img5 from './png/5.png'; 7import img5 from "./png/5.png";
8import img6 from './png/6.png'; 8import img6 from "./png/6.png";
9import img7 from './png/7.png'; 9import img7 from "./png/7.png";
10import img8 from './png/8.png'; 10import img8 from "./png/8.png";
11import img9 from './png/9.png'; 11import img9 from "./png/9.png";
12import img10 from './png/10.png'; 12import img10 from "./png/10.png";
13import img11 from './png/11.png'; 13import img11 from "./png/11.png";
14import img12 from './png/12.png'; 14import img12 from "./png/12.png";
15import img13 from './png/13.png'; 15import img13 from "./png/13.png";
16import img14 from './png/14.png'; 16import img14 from "./png/14.png";
17import img15 from './png/15.png'; 17import img15 from "./png/15.png";
18import img16 from './png/16.png'; 18import img16 from "./png/16.png";
19import img17 from './png/17.png'; 19import img17 from "./png/17.png";
20import img18 from './png/18.png'; 20import img18 from "./png/18.png";
21import img19 from './png/19.png'; 21import img19 from "./png/19.png";
22import img20 from './png/20.png'; 22import img20 from "./png/20.png";
23import img21 from "./png/21.png"; 23import img21 from "./png/21.png";
24 24
25export const LogoIcon = logo; 25export const LogoIcon = logo;
26export const LoginIcon = login; 26export const LoginIcon = Login;
27 27
28export const SearchIcon = img1; 28export const SearchIcon = img1;
29export const HomeIcon = img2; 29export const HomeIcon = img2;
@@ -45,4 +45,4 @@ export const SteamIcon = img17;
45export const HistoryIcon = img18; 45export const HistoryIcon = img18;
46export const SortIcon = img19; 46export const SortIcon = img19;
47export const UploadIcon = img20; 47export const UploadIcon = img20;
48export const DeleteIcon = img21; \ No newline at end of file 48export const DeleteIcon = img21;
diff --git a/frontend/src/images/svgs/steam.tsx b/frontend/src/images/svgs/steam.tsx
new file mode 100644
index 0000000..0dc9a04
--- /dev/null
+++ b/frontend/src/images/svgs/steam.tsx
@@ -0,0 +1,7 @@
1export function LoginIcon(){
2 return (
3 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="text-white" height={32} width={32}>
4 <path d="M504 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5l0 1.2-59.2 85.7c-15.5-.9-30.7 3.4-43.5 12.1L8 236.1C18.2 108.4 125.1 8 255.6 8 392.8 8 504 119 504 256zM163.7 384.3l-30.5-12.6c5.6 11.6 15.3 20.8 27.2 25.8 26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3 .1-40.3S214 305.6 201 300.2c-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zM337.5 129.8a62.3 62.3 0 1 1 0 124.6 62.3 62.3 0 1 1 0-124.6zm.1 109a46.8 46.8 0 1 0 0-93.6 46.8 46.8 0 1 0 0 93.6z" fill="currentColor"/>
5 </svg>
6 )
7} \ No newline at end of file
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index eec2ff4..13d180c 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,11 +1,11 @@
1import React from 'react'; 1import React from "react";
2import ReactDOM from 'react-dom/client'; 2import ReactDOM from "react-dom/client";
3import { BrowserRouter } from "react-router-dom"; 3import { BrowserRouter } from "react-router-dom";
4 4
5import App from './App'; 5import App from "./App";
6 6
7const root = ReactDOM.createRoot( 7const root = ReactDOM.createRoot(
8 document.getElementById('root') as HTMLElement 8 document.getElementById("root") as HTMLElement
9); 9);
10 10
11root.render( 11root.render(
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index a8b7826..7802d75 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -1,40 +1,36 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4
5import '@css/About.css';
6 4
7const About: React.FC = () => { 5const About: React.FC = () => {
6 const [aboutText, setAboutText] = React.useState<string>("");
8 7
9 const [aboutText, setAboutText] = React.useState<string>(""); 8 React.useEffect(() => {
10 9 const fetchReadme = async () => {
11 React.useEffect(() => { 10 try {
12 const fetchReadme = async () => { 11 const response = await fetch(
13 try { 12 "https://raw.githubusercontent.com/pektezol/lphub/main/README.md"
14 const response = await fetch( 13 );
15 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' 14 if (!response.ok) {
16 ); 15 throw new Error("Failed to fetch README");
17 if (!response.ok) { 16 }
18 throw new Error('Failed to fetch README'); 17 const readmeText = await response.text();
19 } 18 setAboutText(readmeText);
20 const readmeText = await response.text(); 19 } catch (error) {
21 setAboutText(readmeText); 20 console.error("Error fetching README:", error);
22 } catch (error) { 21 }
23 console.error('Error fetching README:', error); 22 };
24 } 23 fetchReadme();
25 }; 24 }, []);
26 fetchReadme();
27 }, []);
28
29 25
30 return ( 26 return (
31 <div id="about"> 27 <div className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
32 <Helmet> 28 <Helmet>
33 <title>LPHUB | About</title> 29 <title>LPHUB | About</title>
34 </Helmet> 30 </Helmet>
35 <ReactMarkdown>{aboutText}</ReactMarkdown> 31 <ReactMarkdown className={"overflow-auto"}>{aboutText}</ReactMarkdown>
36 </div> 32 </div>
37 ); 33 );
38}; 34};
39 35
40export default About; 36export default About;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index 15cc891..8587635 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -1,46 +1,29 @@
1import React from 'react'; 1import React from "react";
2import { Helmet } from 'react-helmet'; 2import { Helmet } from "react-helmet";
3 3
4import GameEntry from '@components/GameEntry'; 4import GameEntry from "@components/GameEntry";
5import { Game } from '@customTypes/Game'; 5import { Game } from "@customTypes/Game";
6import "@css/Maps.css"
7 6
8interface GamesProps { 7interface GamesProps {
9 games: Game[]; 8 games: Game[];
10} 9}
11 10
12const Games: React.FC<GamesProps> = ({ games }) => { 11const Games: React.FC<GamesProps> = ({ games }) => {
13 12 return (
14 const _page_load = () => { 13 <div className="ml-20 min-h-screen text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin">
15 const loaders = document.querySelectorAll(".loader"); 14 <Helmet>
16 loaders.forEach((loader) => { 15 <title>LPHUB | Games</title>
17 (loader as HTMLElement).style.display = "none"; 16 </Helmet>
18 }); 17 <section className="py-12 px-12 w-full">
19 } 18 <h1 className="text-3xl font-bold mb-8">Games</h1>
20 19 <div className="flex flex-col w-full">
21 React.useEffect(() => { 20 {games.map((game, index) => (
22 document.querySelectorAll(".games-page-item-body").forEach((game, index) => { 21 <GameEntry game={game} key={index} />
23 game.innerHTML = ""; 22 ))}
24 });
25 _page_load();
26 }, []);
27
28 return (
29 <div className='games-page'>
30 <Helmet>
31 <title>LPHUB | Games</title>
32 </Helmet>
33 <section>
34 <div className='games-page-content'>
35 <div className='games-page-item-content'>
36 {games.map((game, index) => (
37 <GameEntry game={game} key={index} />
38 ))}
39 </div>
40 </div>
41 </section>
42 </div> 23 </div>
43 ); 24 </section>
25 </div>
26 );
44}; 27};
45 28
46export default Games; 29export default Games;
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 4f46af5..b4ac3b0 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -1,22 +1,31 @@
1import React from 'react'; 1import React from "react";
2import { Helmet } from 'react-helmet'; 2import { Helmet } from "react-helmet";
3 3
4const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
5 5 return (
6 return ( 6 <main className="ml-12 relative left-0 w-fullmin-h-screen p-4 sm:p-8 text-foreground font-[--font-barlow-semicondensed-regular]">
7 <main> 7 <Helmet>
8 <Helmet> 8 <title>LPHUB | Homepage</title>
9 <title>LPHUB | Homepage</title> 9 </Helmet>
10 </Helmet> 10 <section className="p-8">
11 <section> 11 <p />
12 <p /> 12 <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1>
13 <h1>Welcome to Least Portals Hub!</h1> 13 <p className="text-lg mb-4 leading-relaxed">
14 <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> 14 At the moment, LPHUB is in beta state. This means that the site has
15 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> 15 only the core functionalities enabled for providing both collaborative
16 <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> 16 information and competitive leaderboards.
17 </section> 17 </p>
18 </main> 18 <p className="text-lg mb-4 leading-relaxed">
19 ); 19 The website should feel intuitive to navigate around. For any type of
20 feedback, reach us at LPHUB Discord server.
21 </p>
22 <p className="text-lg mb-4 leading-relaxed">
23 By using LPHUB, you agree that you have read the 'Leaderboard Rules'
24 and the 'About LPHUB' pages.
25 </p>
26 </section>
27 </main>
28 );
20}; 29};
21 30
22export default Homepage; 31export default Homepage;
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index 76f9a52..2f0491e 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -2,7 +2,6 @@ import React, { useEffect } from "react";
2import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; 2import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import "@css/Maplist.css";
6import { API } from "@api/Api"; 5import { API } from "@api/Api";
7import { Game } from "@customTypes/Game"; 6import { Game } from "@customTypes/Game";
8import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 7import { GameChapter, GamesChapters } from "@customTypes/Chapters";
@@ -11,7 +10,6 @@ const Maplist: React.FC = () => {
11 const [game, setGame] = React.useState<Game | null>(null); 10 const [game, setGame] = React.useState<Game | null>(null);
12 const [catNum, setCatNum] = React.useState(0); 11 const [catNum, setCatNum] = React.useState(0);
13 const [id, setId] = React.useState(0); 12 const [id, setId] = React.useState(0);
14 const [category, setCategory] = React.useState(0);
15 const [load, setLoad] = React.useState(false); 13 const [load, setLoad] = React.useState(false);
16 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0); 14 const [currentlySelected, setCurrentlySelected] = React.useState<number>(0);
17 const [hasClicked, setHasClicked] = React.useState(false); 15 const [hasClicked, setHasClicked] = React.useState(false);
@@ -21,7 +19,7 @@ const Maplist: React.FC = () => {
21 19
22 const [dropdownActive, setDropdownActive] = React.useState("none"); 20 const [dropdownActive, setDropdownActive] = React.useState("none");
23 21
24 const params = useParams<{ id: string, chapter: string }>(); 22 const params = useParams<{ id: string; chapter: string }>();
25 const location = useLocation(); 23 const location = useLocation();
26 const navigate = useNavigate(); 24 const navigate = useNavigate();
27 25
@@ -34,15 +32,15 @@ const Maplist: React.FC = () => {
34 const _fetch_chapters = async (chapter_id: string) => { 32 const _fetch_chapters = async (chapter_id: string) => {
35 const chapters = await API.get_chapters(chapter_id); 33 const chapters = await API.get_chapters(chapter_id);
36 setCurChapter(chapters); 34 setCurChapter(chapters);
37 } 35 };
38 36
39 const _handle_dropdown_click = () => { 37 const _handle_dropdown_click = () => {
40 if (dropdownActive == "none") { 38 if (dropdownActive === "none") {
41 setDropdownActive("block"); 39 setDropdownActive("block");
42 } else { 40 } else {
43 setDropdownActive("none"); 41 setDropdownActive("none");
44 } 42 }
45 } 43 };
46 44
47 // im sorry but im too lazy to fix this right now 45 // im sorry but im too lazy to fix this right now
48 useEffect(() => { 46 useEffect(() => {
@@ -54,7 +52,7 @@ const Maplist: React.FC = () => {
54 const queryParams = new URLSearchParams(location.search); 52 const queryParams = new URLSearchParams(location.search);
55 if (queryParams.get("chapter")) { 53 if (queryParams.get("chapter")) {
56 let cat = parseFloat(queryParams.get("chapter") || ""); 54 let cat = parseFloat(queryParams.get("chapter") || "");
57 if (gameId == 2) { 55 if (gameId === 2) {
58 cat += 10; 56 cat += 10;
59 } 57 }
60 _fetch_chapters(cat.toString()); 58 _fetch_chapters(cat.toString());
@@ -62,7 +60,7 @@ const Maplist: React.FC = () => {
62 60
63 const _fetch_game = async () => { 61 const _fetch_game = async () => {
64 const games = await API.get_games(); 62 const games = await API.get_games();
65 const foundGame = games.find((game) => game.id === gameId); 63 const foundGame = games.find(game => game.id === gameId);
66 // console.log(foundGame) 64 // console.log(foundGame)
67 if (foundGame) { 65 if (foundGame) {
68 setGame(foundGame); 66 setGame(foundGame);
@@ -74,111 +72,175 @@ const Maplist: React.FC = () => {
74 const games_chapters = await API.get_games_chapters(gameId.toString()); 72 const games_chapters = await API.get_games_chapters(gameId.toString());
75 setGameChapters(games_chapters); 73 setGameChapters(games_chapters);
76 setNumChapters(games_chapters.chapters.length); 74 setNumChapters(games_chapters.chapters.length);
77 } 75 };
78 76
79 setLoad(true); 77 setLoad(true);
80 _fetch_game(); 78 _fetch_game();
81 _fetch_game_chapters(); 79 _fetch_game_chapters();
82 }, []); 80 }, [location.search]);
83 81
84 useEffect(() => { 82 useEffect(() => {
85 const queryParams = new URLSearchParams(location.search); 83 const queryParams = new URLSearchParams(location.search);
86 if (gameChapters != undefined && !queryParams.get("chapter")) { 84 if (gameChapters !== undefined && !queryParams.get("chapter")) {
87 _fetch_chapters(gameChapters!.chapters[0].id.toString()); 85 _fetch_chapters(gameChapters!.chapters[0].id.toString());
88 } 86 }
89 }, [gameChapters]) 87 }, [gameChapters, location.search]);
90
91
92 88
93 return ( 89 return (
94 <main> 90 <main className="*:text-foreground w-[calc(100vw-80px)] relative left-0 ml-20 min-h-screen p-4 sm:p-8">
95 <Helmet> 91 <Helmet>
96 <title>LPHUB | Maplist</title> 92 <title>LPHUB | Maplist</title>
97 </Helmet> 93 </Helmet>
98 <section style={{ marginTop: "20px" }}> 94
99 <Link to="/games"> 95 <section className="mt-5">
100 <button className="nav-button" style={{ borderRadius: "20px" }}> 96 <Link to="/games">
101 <i className="triangle"></i> 97 <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2">
102 <span>Games List</span> 98 <i className="triangle mr-2"></i>
103 </button> 99 <span className="px-2">Games List</span>
104 </Link> 100 </button>
101 </Link>
105 </section> 102 </section>
103
106 {load ? ( 104 {load ? (
107 <div></div> 105 <div></div>
108 ) : ( 106 ) : (
107 <section>
108 <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground">
109 {game?.name}
110 </h1>
111
112 <div
113 className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative"
114 style={{ backgroundImage: `url(${game?.image})` }}
115 >
116 <div className="backdrop-blur-sm flex flex-col w-full">
117 <div className="h-full flex flex-col justify-center items-center py-6">
118 <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground">
119 {
120 game?.category_portals.find(
121 obj => obj.category.id === catNum + 1
122 )?.portal_count
123 }
124 </h2>
125 <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground">
126 portals
127 </h3>
128 </div>
129
130 <div className="flex h-12 bg-surface gap-0.5">
131 {game?.category_portals.map((cat, index) => (
132 <button
133 key={index}
134 className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${
135 currentlySelected === cat.category.id ||
136 (cat.category.id - 1 === catNum && !hasClicked)
137 ? "bg-surface"
138 : "bg-surface1 hover:bg-surface"
139 }`}
140 onClick={() => {
141 setCatNum(cat.category.id - 1);
142 _update_currently_selected(cat.category.id);
143 }}
144 >
145 <span className="truncate">{cat.category.name}</span>
146 </button>
147 ))}
148 </div>
149 </div>
150 </div>
151
152 <div>
109 <section> 153 <section>
110 <h1>{game?.name}</h1> 154 <div>
155 <span className="text-lg sm:text-lg translate-y-1.5 block mt-2.5 text-foreground">
156 {curChapter?.chapter.name.split(" - ")[0]}
157 </span>
158 </div>
159 <div
160 onClick={_handle_dropdown_click}
161 className="cursor-pointer select-none flex w-fit items-center"
162 >
163 <span className="text-foreground text-base sm:text-2xl">
164 {curChapter?.chapter.name.split(" - ")[1]}
165 </span>
166 <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i>
167 </div>
168 \
111 <div 169 <div
112 style={{ backgroundImage: `url(${game?.image})` }} 170 className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${
113 className="game-header" 171 dropdownActive === "none" ? "hidden" : "block"
172 }`}
114 > 173 >
115 <div className="blur"> 174 {gameChapters?.chapters.map((chapter, i) => {
116 <div className="game-header-portal-count"> 175 return (
117 <h2 className="portal-count"> 176 <div
118 { 177 key={i}
119 game?.category_portals.find( 178 className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground"
120 (obj) => obj.category.id === catNum + 1 179 onClick={() => {
121 )?.portal_count 180 _fetch_chapters(chapter.id.toString());
122 } 181 _handle_dropdown_click();
123 </h2> 182 }}
124 <h3>portals</h3> 183 >
125 </div> 184 {chapter.name}
126 <div className="game-header-categories">
127 {game?.category_portals.map((cat, index) => (
128 <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => { setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id) }}>
129 <span>{cat.category.name}</span>
130 </button>
131 ))}
132 </div>
133 </div> 185 </div>
186 );
187 })}
134 </div> 188 </div>
189 </section>
135 190
136 <div> 191 <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5">
137 <section className="chapter-select-container"> 192 {curChapter?.maps.map((map, i) => {
138 <div> 193 return (
139 <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> 194 <div key={i} className="bg-surface rounded-3xl overflow-hidden">
195 <Link to={`/maps/${map.id}`}>
196 <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate">
197 {map.name}
198 </span>
199 <div
200 className="flex h-40 sm:h-48 bg-cover relative"
201 style={{ backgroundImage: `url(${map.image})` }}
202 >
203 <div className="backdrop-blur-sm w-full flex items-center justify-center">
204 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5">
205 {map.is_disabled
206 ? map.category_portals[0].portal_count
207 : map.category_portals.find(
208 obj => obj.category.id === catNum + 1
209 )?.portal_count}
210 </span>
211 <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white">
212 portals
213 </span>
140 </div> 214 </div>
141 <div onClick={_handle_dropdown_click} className="dropdown">
142 <span>{curChapter?.chapter.name.split(" - ")[1]}</span>
143 <i className="triangle"></i>
144 </div> 215 </div>
145 <div className="dropdown-elements" style={{ display: dropdownActive }}>
146 {gameChapters?.chapters.map((chapter, i) => {
147 return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div>
148 })
149 216
150 } 217 <div className="flex mx-2.5 my-4">
218 <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px">
219 {[1, 2, 3, 4, 5].map((point) => (
220 <div
221 key={point}
222 className={`flex h-0.5 w-full rounded-3xl ${
223 point <= (map.difficulty + 1)
224 ? map.difficulty === 0
225 ? "bg-green-500"
226 : map.difficulty === 1 || map.difficulty === 2
227 ? "bg-lime-500"
228 : map.difficulty === 3
229 ? "bg-red-400"
230 : "bg-red-600"
231 : "bg-surface1"
232 }`}
233 />
234 ))}
151 </div> 235 </div>
152 </section> 236 </div>
153 <section className="maplist"> 237 </Link>
154 {curChapter?.maps.map((map, i) => { 238 </div>
155 return <div className="maplist-entry"> 239 );
156 <Link to={`/maps/${map.id}`}> 240 })}
157 <span>{map.name}</span>
158 <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}>
159 <div className="blur map">
160 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find(
161 (obj) => obj.category.id === catNum + 1
162 )?.portal_count}</span>
163 <span>portals</span>
164 </div>
165 </div>
166 <div className="difficulty-bar">
167 {/* <span>Difficulty:</span> */}
168 <div className={map.difficulty <= 2 ? "one" : map.difficulty <= 4 ? "two" : map.difficulty <= 6 ? "three" : map.difficulty <= 8 ? "four" : map.difficulty <= 10 ? "five" : "one"}>
169 <div className="difficulty-point"></div>
170 <div className="difficulty-point"></div>
171 <div className="difficulty-point"></div>
172 <div className="difficulty-point"></div>
173 <div className="difficulty-point"></div>
174 </div>
175 </div>
176 </Link>
177 </div>
178 })}
179 </section>
180 </div>
181 </section> 241 </section>
242 </div>
243 </section>
182 )} 244 )}
183 </main> 245 </main>
184 ); 246 );
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index fb13563..50fe03b 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -1,28 +1,32 @@
1import React from 'react'; 1import React from "react";
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images'; 5import { PortalIcon, FlagIcon, ChatIcon } from "../images/Images";
6import Summary from '@components/Summary'; 6import Summary from "@components/Summary";
7import Leaderboards from '@components/Leaderboards'; 7import Leaderboards from "@components/Leaderboards";
8import Discussions from '@components/Discussions'; 8import Discussions from "@components/Discussions";
9import ModMenu from '@components/ModMenu'; 9import ModMenu from "@components/ModMenu";
10import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map'; 10import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map";
11import { API } from '@api/Api'; 11import { API } from "@api/Api";
12import "@css/Maps.css";
13 12
14interface MapProps { 13interface MapProps {
15 token?: string; 14 token?: string;
16 isModerator: boolean; 15 isModerator: boolean;
17}; 16}
18 17
19const Maps: React.FC<MapProps> = ({ token, isModerator }) => { 18const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
20
21 const [selectedRun, setSelectedRun] = React.useState<number>(0); 19 const [selectedRun, setSelectedRun] = React.useState<number>(0);
22 20
23 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); 21 const [mapSummaryData, setMapSummaryData] = React.useState<
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); 22 MapSummary | undefined
25 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined); 23 >(undefined);
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<
25 MapLeaderboard | undefined
26 >(undefined);
27 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<
28 MapDiscussions | undefined
29 >(undefined);
26 30
27 const [navState, setNavState] = React.useState<number>(0); 31 const [navState, setNavState] = React.useState<number>(0);
28 32
@@ -30,45 +34,66 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
30 34
31 const mapID = location.pathname.split("/")[2]; 35 const mapID = location.pathname.split("/")[2];
32 36
33 const _fetch_map_summary = async () => { 37 const _fetch_map_summary = React.useCallback(async () => {
34 const mapSummary = await API.get_map_summary(mapID); 38 const mapSummary = await API.get_map_summary(mapID);
35 setMapSummaryData(mapSummary); 39 setMapSummaryData(mapSummary);
36 }; 40 }, [mapID]);
37 41
38 const _fetch_map_leaderboards = async () => { 42 const _fetch_map_leaderboards = React.useCallback(async () => {
39 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); 43 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1");
40 setMapLeaderboardData(mapLeaderboards); 44 setMapLeaderboardData(mapLeaderboards);
41 }; 45 }, [mapID]);
42 46
43 const _fetch_map_discussions = async () => { 47 const _fetch_map_discussions = React.useCallback(async () => {
44 const mapDiscussions = await API.get_map_discussions(mapID); 48 const mapDiscussions = await API.get_map_discussions(mapID);
45 setMapDiscussionsData(mapDiscussions); 49 setMapDiscussionsData(mapDiscussions);
46 }; 50 }, [mapID]);
47 51
48 React.useEffect(() => { 52 React.useEffect(() => {
49 _fetch_map_summary(); 53 _fetch_map_summary();
50 _fetch_map_leaderboards(); 54 _fetch_map_leaderboards();
51 _fetch_map_discussions(); 55 _fetch_map_discussions();
52 }, [mapID]); 56 }, [
57 mapID,
58 _fetch_map_discussions,
59 _fetch_map_leaderboards,
60 _fetch_map_summary,
61 ]);
53 62
54 if (!mapSummaryData) { 63 if (!mapSummaryData) {
55 // loading placeholder 64 // loading placeholder
56 return ( 65 return (
57 <> 66 <>
58 <main> 67 <main className="*:text-foreground relative left-0 w-[calc(100%-20rem)] min-h-screen p-4 sm:p-8">
59 <section id='section1' className='summary1'> 68 <section id="section1" className="summary1">
60 <div> 69 <div>
61 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 70 <Link to="/games">
71 <button
72 className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"
73 >
74 <i className="triangle"></i>
75 <span className="px-2">Games List</span>
76 </button>
77 </Link>
62 </div> 78 </div>
63 </section> 79 </section>
64 80
65 <section id='section2' className='summary1'> 81 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
66 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> 82 <button className="nav-button">
67 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 83 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
68 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> 84 <span>Summary</span>
85 </button>
86 <button className="nav-button">
87 <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
88 <span>Leaderboards</span>
89 </button>
90 <button className="nav-button">
91 <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
92 <span>Discussions</span>
93 </button>
69 </section> 94 </section>
70 95
71 <section id='section6' className='summary2' /> 96 <section id="section6" className="summary2 mt-4" />
72 </main> 97 </main>
73 </> 98 </>
74 ); 99 );
@@ -80,29 +105,78 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
80 <title>LPHUB | {mapSummaryData.map.map_name}</title> 105 <title>LPHUB | {mapSummaryData.map.map_name}</title>
81 <meta name="description" content={mapSummaryData.map.map_name} /> 106 <meta name="description" content={mapSummaryData.map.map_name} />
82 </Helmet> 107 </Helmet>
83 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 108 {isModerator && (
84 109 <ModMenu
85 <div id='background-image'> 110 token={token}
111 data={mapSummaryData}
112 selectedRun={selectedRun}
113 mapID={mapID}
114 />
115 )}
116
117 <div id="background-image">
86 <img src={mapSummaryData.map.image} alt="" /> 118 <img src={mapSummaryData.map.image} alt="" />
87 </div> 119 </div>
88 <main> 120 <main className="relative left-0 w-full sm:ml-80 sm:w-[calc(100%-20rem)] min-h-screen max-h-screen overflow-y-auto p-4 sm:p-8 scrollbar-thin scrollbar-track-surface scrollbar-thumb-muted hover:scrollbar-thumb-surface1">
89 <section id='section1' className='summary1'> 121 <section id="section1" className="summary1">
90 <div> 122 <div>
91 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 123 <Link to="/games">
92 <Link to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> 124 <button
93 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 125 className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"
126 >
127 <i className="triangle"></i>
128 <span className="px-2">Games List</span>
129 </button>
130 </Link>
131 <Link
132 to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}
133 >
134 <button
135 className="nav-button ml-2"
136 >
137 <i className="triangle"></i>
138 <span className="px-2">{mapSummaryData.map.chapter_name}</span>
139 </button>
140 </Link>
141 <br />
142 <span className="block mt-2 text-lg sm:text-xl text-foreground">
143 <b>{mapSummaryData.map.map_name}</b>
144 </span>
94 </div> 145 </div>
95 </section> 146 </section>
96 147
97 <section id='section2' className='summary1'> 148 <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap">
98 <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> 149 <button className="nav-button" onClick={() => setNavState(0)}>
99 <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 150 <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
100 <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> 151 <span>Summary</span>
152 </button>
153 <button className="nav-button" onClick={() => setNavState(1)}>
154 <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
155 <span>Leaderboards</span>
156 </button>
157 <button className="nav-button" onClick={() => setNavState(2)}>
158 <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" />
159 <span>Discussions</span>
160 </button>
101 </section> 161 </section>
102 162
103 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 163 {navState === 0 && (
164 <Summary
165 selectedRun={selectedRun}
166 setSelectedRun={setSelectedRun}
167 data={mapSummaryData}
168 />
169 )}
104 {navState === 1 && <Leaderboards mapID={mapID} />} 170 {navState === 1 && <Leaderboards mapID={mapID} />}
105 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 171 {navState === 2 && (
172 <Discussions
173 data={mapDiscussionsData}
174 token={token}
175 isModerator={isModerator}
176 mapID={mapID}
177 onRefresh={() => _fetch_map_discussions()}
178 />
179 )}
106 </main> 180 </main>
107 </> 181 </>
108 ); 182 );
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 48233bf..f44f587 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,16 +1,27 @@
1import React from 'react'; 1import React from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; 5import {
6import { UserProfile } from '@customTypes/Profile'; 6 SteamIcon,
7import { Game, GameChapters } from '@customTypes/Game'; 7 TwitchIcon,
8import { Map } from '@customTypes/Map'; 8 YouTubeIcon,
9import { ticks_to_time } from '@utils/Time'; 9 PortalIcon,
10import "@css/Profile.css"; 10 FlagIcon,
11import { API } from '@api/Api'; 11 StatisticsIcon,
12import useConfirm from '@hooks/UseConfirm'; 12 SortIcon,
13import useMessage from '@hooks/UseMessage'; 13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16 DeleteIcon,
17} from "@images/Images";
18import { UserProfile } from "@customTypes/Profile";
19import { Game, GameChapters } from "@customTypes/Game";
20import { Map } from "@customTypes/Map";
21import { ticks_to_time } from "@utils/Time";
22import { API } from "@api/Api";
23import useConfirm from "@hooks/UseConfirm";
24import useMessage from "@hooks/UseMessage";
14import useMessageLoad from "@hooks/UseMessageLoad"; 25import useMessageLoad from "@hooks/UseMessageLoad";
15 26
16interface ProfileProps { 27interface ProfileProps {
@@ -20,17 +31,25 @@ interface ProfileProps {
20 onDeleteRecord: () => void; 31 onDeleteRecord: () => void;
21} 32}
22 33
23const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { 34const Profile: React.FC<ProfileProps> = ({
35 profile,
36 token,
37 gameData,
38 onDeleteRecord,
39}) => {
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 40 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { message, MessageDialogComponent } = useMessage(); 41 const { message, MessageDialogComponent } = useMessage();
26 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 42 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
43 useMessageLoad();
27 const [navState, setNavState] = React.useState(0); 44 const [navState, setNavState] = React.useState(0);
28 const [pageNumber, setPageNumber] = React.useState(1); 45 const [pageNumber, setPageNumber] = React.useState(1);
29 const [pageMax, setPageMax] = React.useState(0); 46 const [pageMax, setPageMax] = React.useState(0);
30 47
31 const [game, setGame] = React.useState("0") 48 const [game, setGame] = React.useState("0");
32 const [chapter, setChapter] = React.useState("0") 49 const [chapter, setChapter] = React.useState("0");
33 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 50 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
51 null
52 );
34 const [maps, setMaps] = React.useState<Map[]>([]); 53 const [maps, setMaps] = React.useState<Map[]>([]);
35 54
36 const navigate = useNavigate(); 55 const navigate = useNavigate();
@@ -41,7 +60,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
41 } 60 }
42 }; 61 };
43 62
44 const _get_game_chapters = async () => { 63 const _get_game_chapters = React.useCallback(async () => {
45 if (game && game !== "0") { 64 if (game && game !== "0") {
46 const gameChapters = await API.get_games_chapters(game); 65 const gameChapters = await API.get_games_chapters(game);
47 setChapterData(gameChapters); 66 setChapterData(gameChapters);
@@ -49,9 +68,9 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
49 setPageMax(Math.ceil(profile!.records.length / 20)); 68 setPageMax(Math.ceil(profile!.records.length / 20));
50 setPageNumber(1); 69 setPageNumber(1);
51 } 70 }
52 }; 71 }, [game, profile]);
53 72
54 const _get_game_maps = async () => { 73 const _get_game_maps = React.useCallback(async () => {
55 if (chapter === "0") { 74 if (chapter === "0") {
56 const gameMaps = await API.get_game_maps(game); 75 const gameMaps = await API.get_game_maps(game);
57 setMaps(gameMaps); 76 setMaps(gameMaps);
@@ -63,10 +82,13 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
63 setPageMax(Math.ceil(gameChapters.maps.length / 20)); 82 setPageMax(Math.ceil(gameChapters.maps.length / 20));
64 setPageNumber(1); 83 setPageNumber(1);
65 } 84 }
66 }; 85 }, [chapter, game]);
67 86
68 const _delete_submission = async (map_id: number, record_id: number) => { 87 const _delete_submission = async (map_id: number, record_id: number) => {
69 const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?"); 88 const userConfirmed = await confirm(
89 "Delete Record",
90 "Are you sure you want to delete this record?"
91 );
70 92
71 if (!userConfirmed) { 93 if (!userConfirmed) {
72 return; 94 return;
@@ -87,26 +109,24 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
87 React.useEffect(() => { 109 React.useEffect(() => {
88 if (!profile) { 110 if (!profile) {
89 navigate("/"); 111 navigate("/");
90 }; 112 }
91 }, [profile]); 113 }, [profile, navigate]);
92 114
93 React.useEffect(() => { 115 React.useEffect(() => {
94 if (profile) { 116 if (profile) {
95 _get_game_chapters(); 117 _get_game_chapters();
96 } 118 }
97 }, [profile, game]); 119 }, [profile, game, _get_game_chapters]);
98 120
99 React.useEffect(() => { 121 React.useEffect(() => {
100 if (profile && game !== "0") { 122 if (profile && game !== "0") {
101 _get_game_maps(); 123 _get_game_maps();
102 } 124 }
103 }, [profile, game, chapter, chapterData]) 125 }, [profile, game, chapter, chapterData, _get_game_maps]);
104 126
105 if (!profile) { 127 if (!profile) {
106 return ( 128 return <></>;
107 <></> 129 }
108 );
109 };
110 130
111 return ( 131 return (
112 <div> 132 <div>
@@ -119,230 +139,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
119 {ConfirmDialogComponent} 139 {ConfirmDialogComponent}
120 140
121 <main> 141 <main>
122 <section id='section1' className='profile'> 142 <section id="section1" className="profile">
123 143 {profile.profile ? (
124 {profile.profile 144 <div id="profile-image" onClick={_update_profile}>
125 ? ( 145 <img src={profile.avatar_link} alt="profile-image"></img>
126 <div id='profile-image' onClick={_update_profile}> 146 <span>Refresh</span>
127 <img src={profile.avatar_link} alt="profile-image"></img> 147 </div>
128 <span>Refresh</span> 148 ) : (
129 </div> 149 <div>
130 ) : ( 150 <img src={profile.avatar_link} alt="profile-image"></img>
131 <div> 151 </div>
132 <img src={profile.avatar_link} alt="profile-image"></img> 152 )}
133 </div>
134 )}
135 153
136 <div id='profile-top'> 154 <div id="profile-top">
137 <div> 155 <div>
138 <div>{profile.user_name}</div> 156 <div>{profile.user_name}</div>
139 <div> 157 <div>
140 {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} 158 {profile.country_code === "XX" ? (
159 ""
160 ) : (
161 <img
162 src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`}
163 alt={profile.country_code}
164 />
165 )}
141 </div> 166 </div>
142 <div> 167 <div>
143 {profile.titles.map(e => ( 168 {profile.titles.map(e => (
144 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 169 <span
170 className="titles"
171 style={{ backgroundColor: `#${e.color}` }}
172 >
145 {e.name} 173 {e.name}
146 </span> 174 </span>
147 ))} 175 ))}
148 </div> 176 </div>
149 </div> 177 </div>
150 <div> 178 <div>
151 {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 179 {profile.links.steam === "-" ? (
152 {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 180 ""
153 {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 181 ) : (
154 {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 182 <a href={profile.links.steam}>
183 <img src={SteamIcon} alt="Steam" />
184 </a>
185 )}
186 {profile.links.twitch === "-" ? (
187 ""
188 ) : (
189 <a href={profile.links.twitch}>
190 <img src={TwitchIcon} alt="Twitch" />
191 </a>
192 )}
193 {profile.links.youtube === "-" ? (
194 ""
195 ) : (
196 <a href={profile.links.youtube}>
197 <img src={YouTubeIcon} alt="Youtube" />
198 </a>
199 )}
200 {profile.links.p2sr === "-" ? (
201 ""
202 ) : (
203 <a href={profile.links.p2sr}>
204 <img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} />
205 </a>
206 )}
155 </div> 207 </div>
156
157 </div> 208 </div>
158 <div id='profile-bottom'> 209 <div id="profile-bottom">
159 <div> 210 <div>
160 <span>Overall</span> 211 <span>Overall</span>
161 <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} 212 <span>
162 <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> 213 {profile.rankings.overall.rank === 0
214 ? "N/A "
215 : "#" + profile.rankings.overall.rank + " "}
216 <span>
217 ({profile.rankings.overall.completion_count}/
218 {profile.rankings.overall.completion_total})
219 </span>
163 </span> 220 </span>
164 </div> 221 </div>
165 <div> 222 <div>
166 <span>Singleplayer</span> 223 <span>Singleplayer</span>
167 <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} 224 <span>
168 <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> 225 {profile.rankings.singleplayer.rank === 0
226 ? "N/A "
227 : "#" + profile.rankings.singleplayer.rank + " "}
228 <span>
229 ({profile.rankings.singleplayer.completion_count}/
230 {profile.rankings.singleplayer.completion_total})
231 </span>
169 </span> 232 </span>
170 </div> 233 </div>
171 <div> 234 <div>
172 <span>Cooperative</span> 235 <span>Cooperative</span>
173 <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} 236 <span>
174 <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> 237 {profile.rankings.cooperative.rank === 0
238 ? "N/A "
239 : "#" + profile.rankings.cooperative.rank + " "}
240 <span>
241 ({profile.rankings.cooperative.completion_count}/
242 {profile.rankings.cooperative.completion_total})
243 </span>
175 </span> 244 </span>
176 </div> 245 </div>
177 </div> 246 </div>
178 </section> 247 </section>
179 248
180 249 <section id="section2" className="profile">
181 <section id='section2' className='profile'> 250 <button onClick={() => setNavState(0)}>
182 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 251 <img src={FlagIcon} alt="" />
183 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 252 &nbsp;Player Records
253 </button>
254 <button onClick={() => setNavState(1)}>
255 <img src={StatisticsIcon} alt="" />
256 &nbsp;Statistics
257 </button>
184 </section> 258 </section>
185 259
186 260 <section id="section3" className="profile1">
187 261 <div id="profileboard-nav">
188 262 {gameData === null ? (
189 263 <select>error</select>
190 <section id='section3' className='profile1'> 264 ) : (
191 <div id='profileboard-nav'> 265 <select
192 {gameData === null ? <select>error</select> : 266 id="select-game"
193
194 <select id='select-game'
195 onChange={() => { 267 onChange={() => {
196 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 268 setGame(
269 (document.querySelector("#select-game") as HTMLInputElement)
270 .value
271 );
197 setChapter("0"); 272 setChapter("0");
198 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 273 const chapterSelect = document.querySelector(
274 "#select-chapter"
275 ) as HTMLSelectElement;
199 if (chapterSelect) { 276 if (chapterSelect) {
200 chapterSelect.value = "0"; 277 chapterSelect.value = "0";
201 } 278 }
202 }}> 279 }}
203 <option value={0} key={0}>All Scores</option> 280 >
281 <option value={0} key={0}>
282 All Scores
283 </option>
204 {gameData.map((e, i) => ( 284 {gameData.map((e, i) => (
205 <option value={e.id} key={i + 1}>{e.name}</option> 285 <option value={e.id} key={i + 1}>
206 ))}</select> 286 {e.name}
207 } 287 </option>
288 ))}
289 </select>
290 )}
208 291
209 {game === "0" ? 292 {game === "0" ? (
210 <select disabled> 293 <select disabled>
211 <option>All Chapters</option> 294 <option>All Chapters</option>
212 </select> 295 </select>
213 : chapterData === null ? <select></select> : 296 ) : chapterData === null ? (
214 297 <select></select>
215 <select id='select-chapter' 298 ) : (
216 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 299 <select
217 <option value="0" key="0">All Chapters</option> 300 id="select-chapter"
218 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 301 onChange={() =>
219 <option value={e.id} key={i + 1}>{e.name}</option> 302 setChapter(
220 ))}</select> 303 (
221 } 304 document.querySelector(
305 "#select-chapter"
306 ) as HTMLInputElement
307 ).value
308 )
309 }
310 >
311 <option value="0" key="0">
312 All Chapters
313 </option>
314 {chapterData.chapters
315 .filter(e => e.is_disabled === false)
316 .map((e, i) => (
317 <option value={e.id} key={i + 1}>
318 {e.name}
319 </option>
320 ))}
321 </select>
322 )}
222 </div> 323 </div>
223 <div id='profileboard-top'> 324 <div id="profileboard-top">
224 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 325 <span>
225 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 326 <span>Map Name</span>
226 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 327 <img src={SortIcon} alt="" />
227 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 328 </span>
329 <span style={{ justifyContent: "center" }}>
330 <span>Portals</span>
331 <img src={SortIcon} alt="" />
332 </span>
333 <span style={{ justifyContent: "center" }}>
334 <span>WRΔ </span>
335 <img src={SortIcon} alt="" />
336 </span>
337 <span style={{ justifyContent: "center" }}>
338 <span>Time</span>
339 <img src={SortIcon} alt="" />
340 </span>
228 <span> </span> 341 <span> </span>
229 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 342 <span>
230 <span><span>Date</span><img src={SortIcon} alt="" /></span> 343 <span>Rank</span>
231 <div id='page-number'> 344 <img src={SortIcon} alt="" />
345 </span>
346 <span>
347 <span>Date</span>
348 <img src={SortIcon} alt="" />
349 </span>
350 <div id="page-number">
232 <div> 351 <div>
233 <button onClick={() => { 352 <button
234 if (pageNumber !== 1) { 353 onClick={() => {
235 setPageNumber(prevPageNumber => prevPageNumber - 1); 354 if (pageNumber !== 1) {
236 const records = document.querySelectorAll(".profileboard-record"); 355 setPageNumber(prevPageNumber => prevPageNumber - 1);
237 records.forEach((r) => { 356 const records = document.querySelectorAll(
238 (r as HTMLInputElement).style.height = "44px"; 357 ".profileboard-record"
239 }); 358 );
240 } 359 records.forEach(r => {
241 }} 360 (r as HTMLInputElement).style.height = "44px";
242 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 361 });
243 <span>{pageNumber}/{pageMax}</span> 362 }
244 <button onClick={() => { 363 }}
245 if (pageNumber !== pageMax) { 364 >
246 setPageNumber(prevPageNumber => prevPageNumber + 1); 365 <i
247 const records = document.querySelectorAll(".profileboard-record"); 366 className="triangle"
248 records.forEach((r) => { 367 style={{ position: "relative", left: "-5px" }}
249 (r as HTMLInputElement).style.height = "44px"; 368 ></i>{" "}
250 }); 369 </button>
251 } 370 <span>
252 }} 371 {pageNumber}/{pageMax}
253 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 372 </span>
373 <button
374 onClick={() => {
375 if (pageNumber !== pageMax) {
376 setPageNumber(prevPageNumber => prevPageNumber + 1);
377 const records = document.querySelectorAll(
378 ".profileboard-record"
379 );
380 records.forEach(r => {
381 (r as HTMLInputElement).style.height = "44px";
382 });
383 }
384 }}
385 >
386 <i
387 className="triangle"
388 style={{
389 position: "relative",
390 left: "5px",
391 transform: "rotate(180deg)",
392 }}
393 ></i>{" "}
394 </button>
254 </div> 395 </div>
255 </div> 396 </div>
256 </div> 397 </div>
257 <hr /> 398 <hr />
258 <div id='profileboard-records'> 399 <div id="profileboard-records">
259 400 {game === "0" ? (
260 {game === "0" 401 profile.records
261 ? ( 402 .sort((a, b) => a.map_id - b.map_id)
262 403 .map((r, index) =>
263 profile.records.sort((a, b) => a.map_id - b.map_id) 404 Math.ceil((index + 1) / 20) === pageNumber ? (
264 .map((r, index) => ( 405 <button className="profileboard-record" key={index}>
265 406 {r.scores.map((e, i) => (
266 Math.ceil((index + 1) / 20) === pageNumber ? ( 407 <>
267 <button className="profileboard-record" key={index}> 408 {i !== 0 ? (
268 {r.scores.map((e, i) => (<> 409 <hr style={{ gridColumn: "1 / span 8" }} />
269 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 410 ) : (
270 411 ""
271 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 412 )}
272 413
273 <span style={{ display: "grid" }}>{e.score_count}</span> 414 <Link to={`/maps/${r.map_id}`}>
415 <span>{r.map_name}</span>
416 </Link>
417
418 <span style={{ display: "grid" }}>
419 {e.score_count}
420 </span>
274 421
275 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 422 <span style={{ display: "grid" }}>
276 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 423 {e.score_count - r.map_wr_count > 0
424 ? `+${e.score_count - r.map_wr_count}`
425 : `-`}
426 </span>
427 <span style={{ display: "grid" }}>
428 {ticks_to_time(e.score_time)}
429 </span>
277 <span> </span> 430 <span> </span>
278 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 431 {i === 0 ? (
432 <span>#{r.placement}</span>
433 ) : (
434 <span> </span>
435 )}
279 <span>{e.date.split("T")[0]}</span> 436 <span>{e.date.split("T")[0]}</span>
280 <span style={{ flexDirection: "row-reverse" }}> 437 <span style={{ flexDirection: "row-reverse" }}>
281 438 <button
282 <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 439 style={{ marginRight: "10px" }}
283 <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button> 440 onClick={() => {
284 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 441 message(
285 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 442 "Demo Information",
286 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 443 `Demo ID: ${e.demo_id}`
287 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 444 );
288 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 445 }}
289 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 446 >
290 } 447 <img src={ThreedotIcon} alt="demo_id" />
291 }><img src={HistoryIcon} alt="history" /></button> : ""} 448 </button>
292 449 <button
450 onClick={() => {
451 _delete_submission(r.map_id, e.record_id);
452 }}
453 >
454 <img src={DeleteIcon} alt="delete icon"></img>
455 </button>
456 <button
457 onClick={() =>
458 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
459 }
460 >
461 <img src={DownloadIcon} alt="download" />
462 </button>
463 {i === 0 && r.scores.length > 1 ? (
464 <button
465 onClick={() => {
466 (
467 document.querySelectorAll(
468 ".profileboard-record"
469 )[index % 20] as HTMLInputElement
470 ).style.height === "44px" ||
471 (
472 document.querySelectorAll(
473 ".profileboard-record"
474 )[index % 20] as HTMLInputElement
475 ).style.height === ""
476 ? ((
477 document.querySelectorAll(
478 ".profileboard-record"
479 )[index % 20] as HTMLInputElement
480 ).style.height =
481 `${r.scores.length * 46}px`)
482 : ((
483 document.querySelectorAll(
484 ".profileboard-record"
485 )[index % 20] as HTMLInputElement
486 ).style.height = "44px");
487 }}
488 >
489 <img src={HistoryIcon} alt="history" />
490 </button>
491 ) : (
492 ""
493 )}
293 </span> 494 </span>
294 </>))} 495 </>
295 496 ))}
497 </button>
498 ) : (
499 ""
500 )
501 )
502 ) : maps ? (
503 maps
504 .filter(e => e.is_disabled === false)
505 .sort((a, b) => a.id - b.id)
506 .map((r, index) => {
507 if (Math.ceil((index + 1) / 20) === pageNumber) {
508 let record = profile.records.find(e => e.map_id === r.id);
509 return record === undefined ? (
510 <button
511 className="profileboard-record"
512 key={index}
513 style={{ backgroundColor: "#1b1b20" }}
514 >
515 <Link to={`/maps/${r.id}`}>
516 <span>{r.name}</span>
517 </Link>
518 <span style={{ display: "grid" }}>N/A</span>
519 <span style={{ display: "grid" }}>N/A</span>
520 <span>N/A</span>
521 <span> </span>
522 <span>N/A</span>
523 <span>N/A</span>
524 <span style={{ flexDirection: "row-reverse" }}></span>
296 </button> 525 </button>
297 ) : "" 526 ) : (
298 ))) : maps ? 527 <button className="profileboard-record" key={index}>
299 528 {record.scores.map((e, i) => (
300 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 529 <>
301 .map((r, index) => { 530 {i !== 0 ? (
302 if (Math.ceil((index + 1) / 20) === pageNumber) { 531 <hr style={{ gridColumn: "1 / span 8" }} />
303 let record = profile.records.find((e) => e.map_id === r.id); 532 ) : (
304 return record === undefined ? ( 533 ""
305 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 534 )}
306 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 535 <Link to={`/maps/${r.id}`}>
307 <span style={{ display: "grid" }}>N/A</span> 536 <span>{r.name}</span>
308 <span style={{ display: "grid" }}>N/A</span> 537 </Link>
309 <span>N/A</span> 538 <span style={{ display: "grid" }}>
310 <span> </span> 539 {record!.scores[i].score_count}
311 <span>N/A</span> 540 </span>
312 <span>N/A</span> 541 <span style={{ display: "grid" }}>
313 <span style={{ flexDirection: "row-reverse" }}></span> 542 {record!.scores[i].score_count -
314 </button> 543 record!.map_wr_count >
315 ) : ( 544 0
316 <button className="profileboard-record" key={index}> 545 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
317 {record.scores.map((e, i) => (<> 546 : `-`}
318 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 547 </span>
319 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 548 <span style={{ display: "grid" }}>
320 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 549 {ticks_to_time(record!.scores[i].score_time)}
321 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> 550 </span>
322 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
323 <span> </span> 551 <span> </span>
324 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 552 {i === 0 ? (
553 <span>#{record!.placement}</span>
554 ) : (
555 <span> </span>
556 )}
325 <span>{record!.scores[i].date.split("T")[0]}</span> 557 <span>{record!.scores[i].date.split("T")[0]}</span>
326 <span style={{ flexDirection: "row-reverse" }}> 558 <span style={{ flexDirection: "row-reverse" }}>
327 559 <button
328 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 560 onClick={() => {
329 <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> 561 message(
330 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 562 "Demo Information",
331 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 563 `Demo ID: ${e.demo_id}`
332 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 564 );
333 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 565 }}
334 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 566 >
335 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 567 <img src={ThreedotIcon} alt="demo_id" />
336 } 568 </button>
337 }><img src={HistoryIcon} alt="history" /></button> : ""} 569 <button
338 570 onClick={() => {
571 _delete_submission(r.id, e.record_id);
572 }}
573 >
574 <img src={DeleteIcon} alt="delete icon"></img>
575 </button>
576 <button
577 onClick={() =>
578 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
579 }
580 >
581 <img src={DownloadIcon} alt="download" />
582 </button>
583 {i === 0 && record!.scores.length > 1 ? (
584 <button
585 onClick={() => {
586 (
587 document.querySelectorAll(
588 ".profileboard-record"
589 )[index % 20] as HTMLInputElement
590 ).style.height === "44px" ||
591 (
592 document.querySelectorAll(
593 ".profileboard-record"
594 )[index % 20] as HTMLInputElement
595 ).style.height === ""
596 ? ((
597 document.querySelectorAll(
598 ".profileboard-record"
599 )[index % 20] as HTMLInputElement
600 ).style.height =
601 `${record!.scores.length * 46}px`)
602 : ((
603 document.querySelectorAll(
604 ".profileboard-record"
605 )[index % 20] as HTMLInputElement
606 ).style.height = "44px");
607 }}
608 >
609 <img src={HistoryIcon} alt="history" />
610 </button>
611 ) : (
612 ""
613 )}
339 </span> 614 </span>
340 </>))} 615 </>
341 </button> 616 ))}
342 617 </button>
343 ) 618 );
344 } else { return null } 619 } else {
345 }) : (<>{console.warn(maps)}</>)} 620 return null;
621 }
622 })
623 ) : (
624 <>{console.warn(maps)}</>
625 )}
346 </div> 626 </div>
347 </section> 627 </section>
348 </main> 628 </main>
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index 71aa427..dec0e17 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -2,146 +2,202 @@ import React, { useEffect } from "react";
2import { Helmet } from "react-helmet"; 2import { Helmet } from "react-helmet";
3 3
4import RankingEntry from "@components/RankingEntry"; 4import RankingEntry from "@components/RankingEntry";
5import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; 5import {
6 Ranking,
7 SteamRanking,
8 RankingType,
9 SteamRankingType,
10} from "@customTypes/Ranking";
6import { API } from "@api/Api"; 11import { API } from "@api/Api";
7 12
8import "@css/Rankings.css"; 13import "@css/Rankings.css";
9 14
10const Rankings: React.FC = () => { 15enum LeaderboardTypes {
11 const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); 16 official,
12 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); 17 unofficial,
13 enum LeaderboardTypes { 18}
14 official,
15 unofficial
16 }
17 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official);
18 19
19 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); 20enum RankingCategories {
21 rankings_overall,
22 rankings_multiplayer,
23 rankings_singleplayer,
24}
20 25
21 enum RankingCategories { 26const Rankings: React.FC = () => {
22 rankings_overall, 27 const [leaderboardData, setLeaderboardData] = React.useState<
23 rankings_multiplayer, 28 Ranking | SteamRanking
24 rankings_singleplayer 29 >();
30 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<
31 RankingType[] | SteamRankingType[]
32 >();
33 const [currentRankingType, setCurrentRankingType] =
34 React.useState<LeaderboardTypes>(LeaderboardTypes.official);
35
36 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false);
37
38 const [currentLeaderboardType, setCurrentLeaderboardType] =
39 React.useState<RankingCategories>(RankingCategories.rankings_singleplayer);
40 const [load, setLoad] = React.useState<boolean>(false);
41
42 const _fetch_rankings = React.useCallback(async () => {
43 setLeaderboardLoad(false);
44 const rankings = await API.get_official_rankings();
45 setLeaderboardData(rankings);
46 if (currentLeaderboardType === RankingCategories.rankings_singleplayer) {
47 setCurrentLeaderboard(rankings.rankings_singleplayer);
48 } else if (
49 currentLeaderboardType === RankingCategories.rankings_multiplayer
50 ) {
51 setCurrentLeaderboard(rankings.rankings_multiplayer);
52 } else {
53 setCurrentLeaderboard(rankings.rankings_overall);
25 } 54 }
26 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); 55 setLoad(true);
27 const [load, setLoad] = React.useState<boolean>(false); 56 setLeaderboardLoad(true);
28 57 }, [currentLeaderboardType]);
29 const _fetch_rankings = async () => { 58
30 setLeaderboardLoad(false); 59 const __dev_fetch_unofficial_rankings = async () => {
31 const rankings = await API.get_official_rankings(); 60 try {
32 setLeaderboardData(rankings); 61 setLeaderboardLoad(false);
33 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 62 const rankings = await API.get_unofficial_rankings();
34 setCurrentLeaderboard(rankings.rankings_singleplayer) 63 setLeaderboardData(rankings);
35 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 64 if (currentLeaderboardType === RankingCategories.rankings_singleplayer) {
36 setCurrentLeaderboard(rankings.rankings_multiplayer) 65 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
37 } else { 66 setCurrentLeaderboard(rankings.rankings_singleplayer);
38 setCurrentLeaderboard(rankings.rankings_overall) 67 } else if (
39 } 68 currentLeaderboardType === RankingCategories.rankings_multiplayer
40 setLoad(true); 69 ) {
41 setLeaderboardLoad(true); 70 setCurrentLeaderboard(rankings.rankings_multiplayer);
71 } else {
72 setCurrentLeaderboard(rankings.rankings_overall);
73 }
74 setLeaderboardLoad(true);
75 } catch (e) {
76 console.log(e);
42 } 77 }
43 78 };
44 const __dev_fetch_unofficial_rankings = async () => { 79
45 try { 80 const _set_current_leaderboard = React.useCallback(
46 setLeaderboardLoad(false); 81 (ranking_cat: RankingCategories) => {
47 const rankings = await API.get_unofficial_rankings(); 82 if (ranking_cat === RankingCategories.rankings_singleplayer) {
48 setLeaderboardData(rankings); 83 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
49 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 84 } else if (ranking_cat === RankingCategories.rankings_multiplayer) {
50 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) 85 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
51 setCurrentLeaderboard(rankings.rankings_singleplayer) 86 } else {
52 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 87 setCurrentLeaderboard(leaderboardData!.rankings_overall);
53 setCurrentLeaderboard(rankings.rankings_multiplayer) 88 }
54 } else { 89
55 setCurrentLeaderboard(rankings.rankings_overall) 90 setCurrentLeaderboardType(ranking_cat);
91 },
92 [leaderboardData]
93 );
94
95 // unused func
96 // const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => {
97 // if (leaderboard_type === LeaderboardTypes.official) {
98 // _fetch_rankings();
99 // } else {
100 // }
101 // };
102
103 useEffect(() => {
104 _fetch_rankings();
105 }, [_fetch_rankings]);
106
107 return (
108 <main className="*:text-foreground">
109 <Helmet>
110 <title>LPHUB | Rankings</title>
111 </Helmet>
112 <section className="nav-container nav-1">
113 <div>
114 <button
115 onClick={() => {
116 _fetch_rankings();
117 setCurrentRankingType(LeaderboardTypes.official);
118 }}
119 className={`nav-1-btn ${currentRankingType === LeaderboardTypes.official ? "selected" : ""}`}
120 >
121 <span>Official (LPHUB)</span>
122 </button>
123 <button
124 onClick={() => {
125 __dev_fetch_unofficial_rankings();
126 setCurrentRankingType(LeaderboardTypes.unofficial);
127 }}
128 className={`nav-1-btn ${currentRankingType === LeaderboardTypes.unofficial ? "selected" : ""}`}
129 >
130 <span>Unofficial (Steam)</span>
131 </button>
132 </div>
133 </section>
134 <section className="nav-container nav-2">
135 <div>
136 <button
137 onClick={() =>
138 _set_current_leaderboard(RankingCategories.rankings_singleplayer)
56 } 139 }
57 setLeaderboardLoad(true); 140 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_singleplayer ? "selected" : ""}`}
58 } catch (e) { 141 >
59 console.log(e) 142 <span>Singleplayer</span>
60 } 143 </button>
61 } 144 <button
62 145 onClick={() =>
63 const _set_current_leaderboard = (ranking_cat: RankingCategories) => { 146 _set_current_leaderboard(RankingCategories.rankings_multiplayer)
64 if (ranking_cat == RankingCategories.rankings_singleplayer) { 147 }
65 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); 148 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_multiplayer ? "selected" : ""}`}
66 } else if (ranking_cat == RankingCategories.rankings_multiplayer) { 149 >
67 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); 150 <span>Cooperative</span>
68 } else { 151 </button>
69 setCurrentLeaderboard(leaderboardData!.rankings_overall); 152 <button
70 } 153 onClick={() =>
71 154 _set_current_leaderboard(RankingCategories.rankings_overall)
72 setCurrentLeaderboardType(ranking_cat); 155 }
73 } 156 className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_overall ? "selected" : ""}`}
74 157 >
75 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { 158 <span>Overall</span>
76 if (leaderboard_type == LeaderboardTypes.official) { 159 </button>
77 _fetch_rankings(); 160 </div>
78 } else { 161 </section>
79 162
80 } 163 {load ? (
81 } 164 <section className="rankings-leaderboard">
82 165 <div className="ranks-container">
83 useEffect(() => { 166 <div className="leaderboard-entry header">
84 _fetch_rankings(); 167 <span>Rank</span>
85 if (load) { 168 <span>Player</span>
86 _set_current_leaderboard(RankingCategories.rankings_singleplayer); 169 <span>Portals</span>
87 } 170 </div>
88 }, [load]) 171
89 172 <div className="splitter"></div>
90 return ( 173
91 <main> 174 {leaderboardLoad &&
92 <Helmet> 175 currentLeaderboard?.map((curRankingData, i) => {
93 <title>LPHUB | Rankings</title> 176 return (
94 </Helmet> 177 <RankingEntry
95 <section className="nav-container nav-1"> 178 currentLeaderboardType={currentLeaderboardType}
96 <div> 179 curRankingData={curRankingData}
97 <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> 180 key={i}
98 <span>Official (LPHUB)</span> 181 ></RankingEntry>
99 </button> 182 );
100 <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> 183 })}
101 <span>Unofficial (Steam)</span> 184
102 </button> 185 {leaderboardLoad ? null : (
103 </div> 186 <div
104 </section> 187 style={{
105 <section className="nav-container nav-2"> 188 display: "flex",
106 <div> 189 justifyContent: "center",
107 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> 190 margin: "30px 0px",
108 <span>Singleplayer</span> 191 }}
109 </button> 192 >
110 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> 193 <span className="loader"></span>
111 <span>Cooperative</span> 194 </div>
112 </button> 195 )}
113 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> 196 </div>
114 <span>Overall</span> 197 </section>
115 </button> 198 ) : null}
116 </div> 199 </main>
117 </section> 200 );
118 201};
119 {load ?
120 <section className="rankings-leaderboard">
121 <div className="ranks-container">
122 <div className="leaderboard-entry header">
123 <span>Rank</span>
124 <span>Player</span>
125 <span>Portals</span>
126 </div>
127
128 <div className="splitter"></div>
129
130 {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => {
131 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry>
132 })
133 }
134
135 {leaderboardLoad ? null :
136 <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}>
137 <span className="loader"></span>
138 </div>
139 }
140 </div>
141 </section>
142 : null}
143 </main>
144 )
145}
146 202
147export default Rankings; 203export default Rankings;
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 9f57b7e..9c7885c 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -1,41 +1,37 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4
5import '@css/Rules.css';
6 4
7const Rules: React.FC = () => { 5const Rules: React.FC = () => {
6 const [rulesText, setRulesText] = React.useState<string>("");
8 7
9 const [rulesText, setRulesText] = React.useState<string>(""); 8 React.useEffect(() => {
10 9 const fetchRules = async () => {
11 React.useEffect(() => { 10 try {
12 const fetchRules = async () => { 11 const response = await fetch(
13 try { 12 "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md"
14 const response = await fetch( 13 );
15 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' 14 if (!response.ok) {
16 ); 15 throw new Error("Failed to fetch README");
17 if (!response.ok) { 16 }
18 throw new Error('Failed to fetch README'); 17 const rulesText = await response.text();
19 } 18 setRulesText(rulesText);
20 const rulesText = await response.text(); 19 } catch (error) {
21 setRulesText(rulesText); 20 console.error("Error fetching Rules:", error);
22 } catch (error) { 21 }
23 console.error('Error fetching Rules:', error); 22 // setRulesText(rulesText)
24 } 23 };
25 // setRulesText(rulesText) 24 fetchRules();
26 }; 25 }, []);
27 fetchRules();
28 }, []);
29
30 26
31 return ( 27 return (
32 <main> 28 <main className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none">
33 <Helmet> 29 <Helmet>
34 <title>LPHUB | Rules</title> 30 <title>LPHUB | Rules</title>
35 </Helmet> 31 </Helmet>
36 <ReactMarkdown>{rulesText}</ReactMarkdown> 32 <ReactMarkdown>{rulesText}</ReactMarkdown>
37 </main> 33 </main>
38 ); 34 );
39}; 35};
40 36
41export default Rules; 37export default Rules;
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index d43c0c6..8c699b1 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -1,15 +1,25 @@
1import React from 'react'; 1import React from "react";
2import { Link, useLocation, useNavigate } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from "react-router-dom";
3import { Helmet } from 'react-helmet'; 3import { Helmet } from "react-helmet";
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; 5import {
6import { UserProfile } from '@customTypes/Profile'; 6 SteamIcon,
7import { Game, GameChapters } from '@customTypes/Game'; 7 TwitchIcon,
8import { Map } from '@customTypes/Map'; 8 YouTubeIcon,
9import { API } from '@api/Api'; 9 PortalIcon,
10import { ticks_to_time } from '@utils/Time'; 10 FlagIcon,
11import "@css/Profile.css"; 11 StatisticsIcon,
12import useMessage from '@hooks/UseMessage'; 12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16} from "@images/Images";
17import { UserProfile } from "@customTypes/Profile";
18import { Game, GameChapters } from "@customTypes/Game";
19import { Map } from "@customTypes/Map";
20import { API } from "@api/Api";
21import { ticks_to_time } from "@utils/Time";
22import useMessage from "@hooks/UseMessage";
13 23
14interface UserProps { 24interface UserProps {
15 profile?: UserProfile; 25 profile?: UserProfile;
@@ -18,7 +28,6 @@ interface UserProps {
18} 28}
19 29
20const User: React.FC<UserProps> = ({ token, profile, gameData }) => { 30const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
21
22 const { message, MessageDialogComponent } = useMessage(); 31 const { message, MessageDialogComponent } = useMessage();
23 32
24 const [user, setUser] = React.useState<UserProfile | undefined>(undefined); 33 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
@@ -29,13 +38,15 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
29 38
30 const [game, setGame] = React.useState("0"); 39 const [game, setGame] = React.useState("0");
31 const [chapter, setChapter] = React.useState("0"); 40 const [chapter, setChapter] = React.useState("0");
32 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 41 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
42 null
43 );
33 const [maps, setMaps] = React.useState<Map[]>([]); 44 const [maps, setMaps] = React.useState<Map[]>([]);
34 45
35 const location = useLocation(); 46 const location = useLocation();
36 const navigate = useNavigate(); 47 const navigate = useNavigate();
37 48
38 const _fetch_user = async () => { 49 const _fetch_user = React.useCallback(async () => {
39 const userID = location.pathname.split("/")[2]; 50 const userID = location.pathname.split("/")[2];
40 if (token && profile && profile.profile && profile.steam_id === userID) { 51 if (token && profile && profile.profile && profile.steam_id === userID) {
41 navigate("/profile"); 52 navigate("/profile");
@@ -43,9 +54,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
43 } 54 }
44 const userData = await API.get_user(userID); 55 const userData = await API.get_user(userID);
45 setUser(userData); 56 setUser(userData);
46 }; 57 }, [location.pathname, token, profile, navigate]);
47 58
48 const _get_game_chapters = async () => { 59 const _get_game_chapters = React.useCallback(async () => {
49 if (game !== "0") { 60 if (game !== "0") {
50 const gameChapters = await API.get_games_chapters(game); 61 const gameChapters = await API.get_games_chapters(game);
51 setChapterData(gameChapters); 62 setChapterData(gameChapters);
@@ -53,9 +64,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
53 setPageMax(Math.ceil(user!.records.length / 20)); 64 setPageMax(Math.ceil(user!.records.length / 20));
54 setPageNumber(1); 65 setPageNumber(1);
55 } 66 }
56 }; 67 }, [game, user]);
57 68
58 const _get_game_maps = async () => { 69 const _get_game_maps = React.useCallback(async () => {
59 if (chapter === "0") { 70 if (chapter === "0") {
60 const gameMaps = await API.get_game_maps(game); 71 const gameMaps = await API.get_game_maps(game);
61 setMaps(gameMaps); 72 setMaps(gameMaps);
@@ -67,251 +78,331 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
67 setPageMax(Math.ceil(gameChapters.maps.length / 20)); 78 setPageMax(Math.ceil(gameChapters.maps.length / 20));
68 setPageNumber(1); 79 setPageNumber(1);
69 } 80 }
70 }; 81 }, [chapter, game]);
71 82
72 React.useEffect(() => { 83 React.useEffect(() => {
73 _fetch_user(); 84 _fetch_user();
74 }, [location]); 85 }, [location, _fetch_user]);
75 86
76 React.useEffect(() => { 87 React.useEffect(() => {
77 if (user) { 88 if (user) {
78 _get_game_chapters(); 89 _get_game_chapters();
79 } 90 }
80 }, [user, game, location]); 91 }, [user, game, location, _get_game_chapters]);
81 92
82 React.useEffect(() => { 93 React.useEffect(() => {
83 if (user && game !== "0") { 94 if (user && game !== "0") {
84 _get_game_maps(); 95 _get_game_maps();
85 } 96 }
86 }, [user, game, chapter, location]) 97 }, [user, game, chapter, location, _get_game_maps]);
87 98
88 if (!user) { 99 if (!user) {
89 return ( 100 return (
90 <></> 101 <div className="flex justify-center items-center h-[50vh] text-lg text-foreground">
102 Loading...
103 </div>
91 ); 104 );
92 }; 105 }
93 106
94 return ( 107 return (
95 <main> 108 <main className="ml-20 overflow-auto overflow-x-hidden relative w-[calc(100%px)] h-screen font-[--font-barlow-semicondensed-regular] text-foreground text-xl">
96 <Helmet> 109 <Helmet>
97 <title>LPHUB | {user.user_name}</title> 110 <title>LPHUB | {user.user_name}</title>
98 <meta name="description" content={user.user_name} /> 111 <meta name="description" content={user.user_name} />
99 </Helmet> 112 </Helmet>
113
100 {MessageDialogComponent} 114 {MessageDialogComponent}
101 <section id='section1' className='profile'> 115
102 <div> 116 <section className="m-5 bg-gradient-to-t from-[#202232] from-50% to-[#2b2e46] to-50% rounded-3xl p-[30px] mb-[30px] text-foreground">
103 <img src={user.avatar_link} alt="profile-image"></img> 117 <div className="grid grid-cols-[200px_1fr_auto] items-center gap-[25px] mb-[25px]">
104 </div> 118 <img
105 <div id='profile-top'> 119 src={user.avatar_link}
120 alt="Profile"
121 className="w-[120px] h-[120px] rounded-full border-[3px] border-[rgba(205,207,223,0.2)]"
122 />
106 <div> 123 <div>
107 <div>{user.user_name}</div> 124 <h1 className="m-0 mb-[10px] text-[50px] font-bold text-white font-[--font-barlow-semicondensed-regular]">
108 <div> 125 {user.user_name}
109 {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} 126 </h1>
110 </div> 127 {user.country_code !== "XX" && (
111 <div> 128 <div className="flex items-center gap-3 mb-[15px]">
112 {user.titles.map(e => ( 129 <img
113 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 130 src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`}
114 {e.name} 131 alt={user.country_code}
132 className="w-6 h-4 rounded-[10px]"
133 />
134 <span>{user.country_code}</span>
135 </div>
136 )}
137 <div className="flex flex-wrap gap-2">
138 {user.titles.map((title, index) => (
139 <span
140 key={index}
141 className="py-[6px] px-5 pt-[6px] rounded-[10px] text-lg font-normal text-white"
142 style={{ backgroundColor: `#${title.color}` }}
143 >
144 {title.name}
115 </span> 145 </span>
116 ))} 146 ))}
117 </div> 147 </div>
118 </div> 148 </div>
119 <div> 149 <div className="flex gap-[15px] items-center pr-[10px]">
120 {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 150 {user.links.steam !== "-" && (
121 {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 151 <a href={user.links.steam} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
122 {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 152 <img src={SteamIcon} alt="Steam" className="h-[50px] px-[5px] scale-90 brightness-200" />
123 {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 153 </a>
154 )}
155 {user.links.twitch !== "-" && (
156 <a href={user.links.twitch} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
157 <img src={TwitchIcon} alt="Twitch" className="h-[50px] px-[5px] scale-90 brightness-200" />
158 </a>
159 )}
160 {user.links.youtube !== "-" && (
161 <a href={user.links.youtube} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
162 <img src={YouTubeIcon} alt="YouTube" className="h-[50px] px-[5px] scale-90 brightness-200" />
163 </a>
164 )}
165 {user.links.p2sr !== "-" && (
166 <a href={user.links.p2sr} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5">
167 <img src={PortalIcon} alt="P2SR" className="h-[50px] px-[5px] scale-90 brightness-200" />
168 </a>
169 )}
124 </div> 170 </div>
125
126 </div> 171 </div>
127 <div id='profile-bottom'> 172
128 <div> 173 <div className="grid grid-cols-3 gap-3 mt-24">
129 <span>Overall</span> 174 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
130 <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} 175 <div className="text-inherit text-lg">Overall</div>
131 <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> 176 <div className="text-white text-[40px]">
132 </span> 177 {user.rankings.overall.rank === 0 ? "N/A" : `#${user.rankings.overall.rank}`}
178 </div>
179 <div className="text-white text-xl">
180 {user.rankings.overall.completion_count}/{user.rankings.overall.completion_total}
181 </div>
133 </div> 182 </div>
134 <div> 183 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
135 <span>Singleplayer</span> 184 <div className="text-inherit text-lg">Singleplayer</div>
136 <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} 185 <div className="text-white text-[40px]">
137 <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> 186 {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`}
138 </span> 187 </div>
188 <div className="text-white text-xl">
189 {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total}
190 </div>
139 </div> 191 </div>
140 <div> 192 <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]">
141 <span>Cooperative</span> 193 <div className="text-inherit text-lg">Cooperative</div>
142 <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} 194 <div className="text-white text-[40px]">
143 <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> 195 {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`}
144 </span> 196 </div>
197 <div className="text-white text-xl">
198 {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total}
199 </div>
145 </div> 200 </div>
146 </div> 201 </div>
147 </section> 202 </section>
148 203
149 204 <section className="m-5 h-[60px] grid grid-cols-2">
150 <section id='section2' className='profile'> 205 <button
151 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 206 className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-l-3xl hover:bg-[#202232] ${
152 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 207 navState === 0 ? 'bg-[#202232]' : ''
208 }`}
209 onClick={() => setNavState(0)}
210 >
211 <img src={FlagIcon} alt="" className="w-5 h-5 scale-[1.2]" />
212 Player Records
213 </button>
214 <button
215 className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-r-3xl hover:bg-[#202232] ${
216 navState === 1 ? 'bg-[#202232]' : ''
217 }`}
218 onClick={() => setNavState(1)}
219 >
220 <img src={StatisticsIcon} alt="" className="w-5 h-5 scale-[1.2]" />
221 Statistics
222 </button>
153 </section> 223 </section>
154 224
155 225 {navState === 0 && (
156 226 <section className="m-5 block bg-[#202232] rounded-3xl overflow-hidden">
157 227 <div className="grid grid-cols-2 mx-5 my-5 mt-[10px] mb-5">
158 228 <select
159 <section id='section3' className='profile1'> 229 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px]"
160 <div id='profileboard-nav'> 230 value={game}
161 {gameData === null ? <select>error</select> : 231 onChange={(e) => {
162 232 setGame(e.target.value);
163 <select id='select-game'
164 onChange={() => {
165 setGame((document.querySelector('#select-game') as HTMLInputElement).value);
166 setChapter("0"); 233 setChapter("0");
167 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 234 }}
168 if (chapterSelect) { 235 >
169 chapterSelect.value = "0"; 236 <option value="0">All Games</option>
170 } 237 {gameData?.map((g) => (
171 }}> 238 <option key={g.id} value={g.id}>
172 <option value={0} key={0}>All Scores</option> 239 {g.name}
173 {gameData.map((e, i) => ( 240 </option>
174 <option value={e.id} key={i + 1}>{e.name}</option> 241 ))}
175 ))}</select> 242 </select>
176 }
177 243
178 {game === "0" ? 244 <select
179 <select disabled> 245 className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px] disabled:opacity-50"
180 <option>All Chapters</option> 246 value={chapter}
247 onChange={(e) => setChapter(e.target.value)}
248 disabled={game === "0"}
249 >
250 <option value="0">All Chapters</option>
251 {chapterData?.chapters
252 .filter(c => !c.is_disabled)
253 .map((c) => (
254 <option key={c.id} value={c.id}>
255 {c.name}
256 </option>
257 ))}
181 </select> 258 </select>
182 : chapterData === null ? <select></select> : 259 </div>
183 260
184 <select id='select-chapter' 261 <div className="h-[34px] grid text-xl pl-[60px] mx-5 my-0 grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%]">
185 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 262 <div className="flex place-items-end cursor-pointer">
186 <option value="0" key="0">All Chapters</option> 263 <span>Map Name</span>
187 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 264 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
188 <option value={e.id} key={i + 1}>{e.name}</option> 265 </div>
189 ))}</select> 266 <div className="flex place-items-end cursor-pointer">
190 } 267 <span>Portals</span>
191 </div> 268 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
192 <div id='profileboard-top'> 269 </div>
193 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 270 <div className="flex place-items-end cursor-pointer">
194 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 271 <span>WRΔ</span>
195 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 272 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
196 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 273 </div>
197 <span> </span> 274 <div className="flex place-items-end cursor-pointer">
198 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 275 <span>Time</span>
199 <span><span>Date</span><img src={SortIcon} alt="" /></span> 276 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
200 <div id='page-number'> 277 </div>
201 <div> 278 <div></div>
202 <button onClick={() => { 279 <div className="flex place-items-end cursor-pointer">
203 if (pageNumber !== 1) { 280 <span>Rank</span>
204 setPageNumber(prevPageNumber => prevPageNumber - 1); 281 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
205 const records = document.querySelectorAll(".profileboard-record"); 282 </div>
206 records.forEach((r) => { 283 <div className="flex place-items-end cursor-pointer">
207 (r as HTMLInputElement).style.height = "44px"; 284 <span>Date</span>
208 }); 285 <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" />
209 } 286 </div>
210 }} 287 <div className="flex items-center gap-[10px] justify-center">
211 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 288 <button
212 <span>{pageNumber}/{pageMax}</span> 289 className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed"
213 <button onClick={() => { 290 onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
214 if (pageNumber !== pageMax) { 291 disabled={pageNumber === 1}
215 setPageNumber(prevPageNumber => prevPageNumber + 1); 292 >
216 const records = document.querySelectorAll(".profileboard-record"); 293
217 records.forEach((r) => { 294 </button>
218 (r as HTMLInputElement).style.height = "44px"; 295 <span className="text-sm text-foreground">{pageNumber}/{pageMax}</span>
219 }); 296 <button
220 } 297 className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed"
221 }} 298 onClick={() => setPageNumber(Math.min(pageMax, pageNumber + 1))}
222 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 299 disabled={pageNumber === pageMax}
300 >
301
302 </button>
223 </div> 303 </div>
224 </div> 304 </div>
225 </div>
226 <hr />
227 <div id='profileboard-records'>
228
229 {game === "0"
230 ? (
231
232 user.records.sort((a, b) => a.map_id - b.map_id)
233 .map((r, index) => (
234 305
306 <div>
307 {game === "0" ? (
308 user.records
309 .sort((a, b) => a.map_id - b.map_id)
310 .map((record, index) =>
235 Math.ceil((index + 1) / 20) === pageNumber ? ( 311 Math.ceil((index + 1) / 20) === pageNumber ? (
236 <button className="profileboard-record" key={index}> 312 <div key={index} className="w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232]">
237 {r.scores.map((e, i) => (<> 313 <Link to={`/maps/${record.map_id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
238 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 314 {record.map_name}
239 315 </Link>
240 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 316 <span className="flex place-items-center h-11">{record.scores[0]?.score_count || 'N/A'}</span>
241 317 <span className={`flex place-items-center h-11 ${record.scores[0]?.score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}>
242 <span style={{ display: "grid" }}>{e.score_count}</span> 318 {record.scores[0]?.score_count - record.map_wr_count > 0
243 319 ? `+${record.scores[0].score_count - record.map_wr_count}`
244 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 320 : '–'}
245 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 321 </span>
246 <span> </span> 322 <span className="flex place-items-center h-11">{record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
247 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 323 <span className="flex place-items-center h-11"></span>
248 <span>{e.date.split("T")[0]}</span> 324 <span className="flex place-items-center h-11 font-semibold">#{record.placement}</span>
249 <span style={{ flexDirection: "row-reverse" }}> 325 <span className="flex place-items-center h-11">{record.scores[0]?.date.split("T")[0] || 'N/A'}</span>
250 326 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
251 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 327 <button
252 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 328 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
253 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 329 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0]?.demo_id}`)}
254 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 330 title="Demo Info"
255 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 331 >
256 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 332 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
257 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 333 </button>
258 } 334 <button
259 }><img src={HistoryIcon} alt="history" /></button> : ""} 335 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
260 336 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0]?.demo_id}`}
261 </span> 337 title="Download Demo"
262 </>))} 338 >
263 339 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
264 </button> 340 </button>
265 ) : "" 341 {record.scores.length > 1 && (
266 ))) : maps ? 342 <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History">
267 343 <img src={HistoryIcon} alt="History" className="w-4 h-4" />
268 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 344 </button>
269 .map((r, index) => { 345 )}
270 if (Math.ceil((index + 1) / 20) === pageNumber) { 346 </div>
271 let record = user.records.find((e) => e.map_id === r.id); 347 </div>
272 return record === undefined ? ( 348 ) : null
273 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 349 )
274 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 350 ) : (
275 <span style={{ display: "grid" }}>N/A</span> 351 maps
276 <span style={{ display: "grid" }}>N/A</span> 352 ?.filter(map => !map.is_disabled)
277 <span>N/A</span> 353 .sort((a, b) => a.id - b.id)
278 <span> </span> 354 .map((map, index) => {
279 <span>N/A</span> 355 if (Math.ceil((index + 1) / 20) !== pageNumber) return null;
280 <span>N/A</span> 356
281 <span style={{ flexDirection: "row-reverse" }}></span> 357 const record = user.records.find(r => r.map_id === map.id);
282 </button> 358
283 ) : ( 359 return (
284 <button className="profileboard-record" key={index}> 360 <div key={index} className={`w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232] ${!record ? 'opacity-65' : ''}`}>
285 {record.scores.map((e, i) => (<> 361 <Link to={`/maps/${map.id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline">
286 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 362 {map.name}
287 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 363 </Link>
288 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 364 <span className="flex place-items-center h-11">{record?.scores[0]?.score_count || 'N/A'}</span>
289 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> 365 <span className={`flex place-items-center h-11 ${record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}>
290 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> 366 {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0
291 <span> </span> 367 ? `+${record.scores[0].score_count - record.map_wr_count}`
292 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 368 : '–'}
293 <span>{record!.scores[i].date.split("T")[0]}</span> 369 </span>
294 <span style={{ flexDirection: "row-reverse" }}> 370 <span className="flex place-items-center h-11">{record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span>
295 371 <span className="flex place-items-center h-11"></span>
296 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 372 <span className="flex place-items-center h-11 font-semibold">{record ? `#${record.placement}` : 'N/A'}</span>
297 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 373 <span className="flex place-items-center h-11">{record?.scores[0]?.date.split("T")[0] || 'N/A'}</span>
298 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 374 <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11">
299 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 375 {record?.scores[0] && (
300 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 376 <>
301 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 377 <button
302 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 378 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
303 } 379 onClick={() => message("Demo Information", `Demo ID: ${record.scores[0].demo_id}`)}
304 }><img src={HistoryIcon} alt="history" /></button> : ""} 380 title="Demo Info"
305 381 >
306 </span> 382 <img src={ThreedotIcon} alt="Info" className="w-4 h-4" />
307 </>))} 383 </button>
308 </button> 384 <button
309 385 className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]"
310 ) 386 onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0].demo_id}`}
311 } else { return null } 387 title="Download Demo"
312 }) : (<>{console.warn(maps)}</>)} 388 >
313 </div> 389 <img src={DownloadIcon} alt="Download" className="w-4 h-4" />
314 </section> 390 </button>
391 {record.scores.length > 1 && (
392 <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History">
393 <img src={HistoryIcon} alt="History" className="w-4 h-4" />
394 </button>
395 )}
396 </>
397 )}
398 </div>
399 </div>
400 );
401 })
402 )}
403 </div>
404 </section>
405 )}
315 </main> 406 </main>
316 ); 407 );
317}; 408};
diff --git a/frontend/src/types/Chapters.ts b/frontend/src/types/Chapters.ts
index 1d48306..5b494ca 100644
--- a/frontend/src/types/Chapters.ts
+++ b/frontend/src/types/Chapters.ts
@@ -2,18 +2,18 @@ import type { Game } from "@customTypes/Game";
2import type { Map } from "@customTypes/Map"; 2import type { Map } from "@customTypes/Map";
3 3
4interface Chapter { 4interface Chapter {
5 id: number; 5 id: number;
6 name: string; 6 name: string;
7 image: string; 7 image: string;
8 is_disabled: boolean; 8 is_disabled: boolean;
9} 9}
10 10
11export interface GameChapter { 11export interface GameChapter {
12 chapter: Chapter; 12 chapter: Chapter;
13 maps: Map[]; 13 maps: Map[];
14} 14}
15 15
16export interface GamesChapters { 16export interface GamesChapters {
17 game: Game; 17 game: Game;
18 chapters: Chapter[]; 18 chapters: Chapter[];
19} \ No newline at end of file 19}
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts
index 775fab4..77b3970 100644
--- a/frontend/src/types/Content.ts
+++ b/frontend/src/types/Content.ts
@@ -6,18 +6,18 @@ export interface ModMenuContent {
6 showcase: string; 6 showcase: string;
7 description: string; 7 description: string;
8 category_id: number; 8 category_id: number;
9}; 9}
10 10
11export interface MapDiscussionContent { 11export interface MapDiscussionContent {
12 title: string; 12 title: string;
13 content: string; 13 content: string;
14}; 14}
15 15
16export interface MapDiscussionCommentContent { 16export interface MapDiscussionCommentContent {
17 comment: string; 17 comment: string;
18}; 18}
19 19
20export interface UploadRunContent { 20export interface UploadRunContent {
21 host_demo: File | null; 21 host_demo: File | null;
22 partner_demo: File | null; 22 partner_demo: File | null;
23}; 23}
diff --git a/frontend/src/types/Game.ts b/frontend/src/types/Game.ts
index 1a80341..0e7dc80 100644
--- a/frontend/src/types/Game.ts
+++ b/frontend/src/types/Game.ts
@@ -1,5 +1,4 @@
1import type { Map } from '@customTypes/Map'; 1import type { Map } from "@customTypes/Map";
2
3 2
4export interface Game { 3export interface Game {
5 id: number; 4 id: number;
@@ -7,31 +6,31 @@ export interface Game {
7 image: string; 6 image: string;
8 is_coop: boolean; 7 is_coop: boolean;
9 category_portals: GameCategoryPortals[]; 8 category_portals: GameCategoryPortals[];
10}; 9}
11 10
12export interface GameChapters { 11export interface GameChapters {
13 game: Game; 12 game: Game;
14 chapters: Chapter[]; 13 chapters: Chapter[];
15}; 14}
16 15
17export interface GameMaps { 16export interface GameMaps {
18 game: Game; 17 game: Game;
19 maps: Map[]; 18 maps: Map[];
20}; 19}
21 20
22export interface Category { 21export interface Category {
23 id: number; 22 id: number;
24 name: string; 23 name: string;
25}; 24}
26 25
27interface Chapter { 26interface Chapter {
28 id: number; 27 id: number;
29 name: string; 28 name: string;
30 image: string; 29 image: string;
31 is_disabled: boolean; 30 is_disabled: boolean;
32}; 31}
33 32
34export interface GameCategoryPortals { 33export interface GameCategoryPortals {
35 category: Category; 34 category: Category;
36 portal_count: number; 35 portal_count: number;
37}; 36}
diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts
index 4f8eabf..a5c9404 100644
--- a/frontend/src/types/Map.ts
+++ b/frontend/src/types/Map.ts
@@ -1,6 +1,6 @@
1import type { Category, GameCategoryPortals } from '@customTypes/Game'; 1import type { Category, GameCategoryPortals } from "@customTypes/Game";
2import type { Pagination } from '@customTypes/Pagination'; 2import type { Pagination } from "@customTypes/Pagination";
3import type { UserShort } from '@customTypes/Profile'; 3import type { UserShort } from "@customTypes/Profile";
4 4
5export interface Map { 5export interface Map {
6 id: number; 6 id: number;
@@ -9,15 +9,15 @@ export interface Map {
9 is_disabled: boolean; 9 is_disabled: boolean;
10 difficulty: number; 10 difficulty: number;
11 category_portals: GameCategoryPortals[]; 11 category_portals: GameCategoryPortals[];
12}; 12}
13 13
14export interface MapDiscussion { 14export interface MapDiscussion {
15 discussion: MapDiscussionsDetail; 15 discussion: MapDiscussionsDetail;
16}; 16}
17 17
18export interface MapDiscussions { 18export interface MapDiscussions {
19 discussions: MapDiscussionsDetail[]; 19 discussions: MapDiscussionsDetail[];
20}; 20}
21 21
22export interface MapDiscussionsDetail { 22export interface MapDiscussionsDetail {
23 id: number; 23 id: number;
@@ -27,19 +27,21 @@ export interface MapDiscussionsDetail {
27 comments: MapDiscussionDetailComment[]; 27 comments: MapDiscussionDetailComment[];
28 created_at: string; 28 created_at: string;
29 updated_at: string; 29 updated_at: string;
30}; 30}
31 31
32interface MapDiscussionDetailComment { 32interface MapDiscussionDetailComment {
33 comment: string; 33 comment: string;
34 date: string; 34 date: string;
35 user: UserShort; 35 user: UserShort;
36}; 36}
37 37
38export interface MapLeaderboard { 38export interface MapLeaderboard {
39 map: MapSummaryMap; 39 map: MapSummaryMap;
40 records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[]; 40 records:
41 | MapLeaderboardRecordSingleplayer[]
42 | MapLeaderboardRecordMultiplayer[];
41 pagination: Pagination; 43 pagination: Pagination;
42}; 44}
43 45
44export interface MapLeaderboardRecordSingleplayer { 46export interface MapLeaderboardRecordSingleplayer {
45 kind: "singleplayer"; 47 kind: "singleplayer";
@@ -50,7 +52,7 @@ export interface MapLeaderboardRecordSingleplayer {
50 user: UserShort; 52 user: UserShort;
51 demo_id: string; 53 demo_id: string;
52 record_date: string; 54 record_date: string;
53}; 55}
54 56
55export interface MapLeaderboardRecordMultiplayer { 57export interface MapLeaderboardRecordMultiplayer {
56 kind: "multiplayer"; 58 kind: "multiplayer";
@@ -63,13 +65,12 @@ export interface MapLeaderboardRecordMultiplayer {
63 host_demo_id: string; 65 host_demo_id: string;
64 partner_demo_id: string; 66 partner_demo_id: string;
65 record_date: string; 67 record_date: string;
66}; 68}
67
68 69
69export interface MapSummary { 70export interface MapSummary {
70 map: MapSummaryMap; 71 map: MapSummaryMap;
71 summary: MapSummaryDetails; 72 summary: MapSummaryDetails;
72}; 73}
73 74
74interface MapSummaryMap { 75interface MapSummaryMap {
75 id: number; 76 id: number;
@@ -84,7 +85,7 @@ interface MapSummaryMap {
84 85
85interface MapSummaryDetails { 86interface MapSummaryDetails {
86 routes: MapSummaryDetailsRoute[]; 87 routes: MapSummaryDetailsRoute[];
87}; 88}
88 89
89interface MapSummaryDetailsRoute { 90interface MapSummaryDetailsRoute {
90 route_id: number; 91 route_id: number;
@@ -94,16 +95,15 @@ interface MapSummaryDetailsRoute {
94 completion_count: number; 95 completion_count: number;
95 description: string; 96 description: string;
96 showcase: string; 97 showcase: string;
97}; 98}
98 99
99interface MapSummaryDetailsRouteHistory { 100interface MapSummaryDetailsRouteHistory {
100 runner_name: string; 101 runner_name: string;
101 score_count: number; 102 score_count: number;
102 date: string; 103 date: string;
103}; 104}
104 105
105export interface MapDeleteEndpoint { 106export interface MapDeleteEndpoint {
106 map_id: number; 107 map_id: number;
107 record_id: number; 108 record_id: number;
108} 109}
109
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts
index b6313e7..9ea9851 100644
--- a/frontend/src/types/MapNames.ts
+++ b/frontend/src/types/MapNames.ts
@@ -1,127 +1,127 @@
1export const MapNames: { [key: string]: number } = { 1export const MapNames: { [key: string]: number } = {
2 "sp_a1_intro1": 1, 2 sp_a1_intro1: 1,
3 "sp_a1_intro2": 2, 3 sp_a1_intro2: 2,
4 "sp_a1_intro3": 3, 4 sp_a1_intro3: 3,
5 "sp_a1_intro4": 4, 5 sp_a1_intro4: 4,
6 "sp_a1_intro5": 5, 6 sp_a1_intro5: 5,
7 "sp_a1_intro6": 6, 7 sp_a1_intro6: 6,
8 "sp_a1_intro7": 7, 8 sp_a1_intro7: 7,
9 "sp_a1_wakeup": 8, 9 sp_a1_wakeup: 8,
10 "sp_a2_intro": 9, 10 sp_a2_intro: 9,
11 11
12 "sp_a2_laser_intro": 10, 12 sp_a2_laser_intro: 10,
13 "sp_a2_laser_stairs": 11, 13 sp_a2_laser_stairs: 11,
14 "sp_a2_dual_lasers": 12, 14 sp_a2_dual_lasers: 12,
15 "sp_a2_laser_over_goo": 13, 15 sp_a2_laser_over_goo: 13,
16 "sp_a2_catapult_intro": 14, 16 sp_a2_catapult_intro: 14,
17 "sp_a2_trust_fling": 15, 17 sp_a2_trust_fling: 15,
18 "sp_a2_pit_flings": 16, 18 sp_a2_pit_flings: 16,
19 "sp_a2_fizzler_intro": 17, 19 sp_a2_fizzler_intro: 17,
20 20
21 "sp_a2_sphere_peek": 18, 21 sp_a2_sphere_peek: 18,
22 "sp_a2_ricochet": 19, 22 sp_a2_ricochet: 19,
23 "sp_a2_bridge_intro": 20, 23 sp_a2_bridge_intro: 20,
24 "sp_a2_bridge_the_gap": 21, 24 sp_a2_bridge_the_gap: 21,
25 "sp_a2_turret_intro": 22, 25 sp_a2_turret_intro: 22,
26 "sp_a2_laser_relays": 23, 26 sp_a2_laser_relays: 23,
27 "sp_a2_turret_blocker": 24, 27 sp_a2_turret_blocker: 24,
28 "sp_a2_laser_vs_turret": 25, 28 sp_a2_laser_vs_turret: 25,
29 "sp_a2_pull_the_rug": 26, 29 sp_a2_pull_the_rug: 26,
30 30
31 "sp_a2_column_blocker": 27, 31 sp_a2_column_blocker: 27,
32 "sp_a2_laser_chaining": 28, 32 sp_a2_laser_chaining: 28,
33 "sp_a2_triple_laser": 29, 33 sp_a2_triple_laser: 29,
34 "sp_a2_bts1": 30, 34 sp_a2_bts1: 30,
35 "sp_a2_bts2": 31, 35 sp_a2_bts2: 31,
36 36
37 "sp_a2_bts3": 32, 37 sp_a2_bts3: 32,
38 "sp_a2_bts4": 33, 38 sp_a2_bts4: 33,
39 "sp_a2_bts5": 34, 39 sp_a2_bts5: 34,
40 "sp_a2_core": 35, 40 sp_a2_core: 35,
41 41
42 "sp_a3_01": 36, 42 sp_a3_01: 36,
43 "sp_a3_03": 37, 43 sp_a3_03: 37,
44 "sp_a3_jump_intro": 38, 44 sp_a3_jump_intro: 38,
45 "sp_a3_bomb_flings": 39, 45 sp_a3_bomb_flings: 39,
46 "sp_a3_crazy_box": 40, 46 sp_a3_crazy_box: 40,
47 "sp_a3_transition01": 41, 47 sp_a3_transition01: 41,
48 48
49 "sp_a3_speed_ramp": 42, 49 sp_a3_speed_ramp: 42,
50 "sp_a3_speed_flings": 43, 50 sp_a3_speed_flings: 43,
51 "sp_a3_portal_intro": 44, 51 sp_a3_portal_intro: 44,
52 "sp_a3_end": 45, 52 sp_a3_end: 45,
53 53
54 "sp_a4_intro": 46, 54 sp_a4_intro: 46,
55 "sp_a4_tb_intro": 47, 55 sp_a4_tb_intro: 47,
56 "sp_a4_tb_trust_drop": 48, 56 sp_a4_tb_trust_drop: 48,
57 "sp_a4_tb_wall_button": 49, 57 sp_a4_tb_wall_button: 49,
58 "sp_a4_tb_polarity": 50, 58 sp_a4_tb_polarity: 50,
59 "sp_a4_tb_catch": 51, 59 sp_a4_tb_catch: 51,
60 "sp_a4_stop_the_box": 52, 60 sp_a4_stop_the_box: 52,
61 "sp_a4_laser_catapult": 53, 61 sp_a4_laser_catapult: 53,
62 "sp_a4_laser_platform": 54, 62 sp_a4_laser_platform: 54,
63 "sp_a4_speed_tb_catch": 55, 63 sp_a4_speed_tb_catch: 55,
64 "sp_a4_jump_polarity": 56, 64 sp_a4_jump_polarity: 56,
65 65
66 "sp_a4_finale1": 57, 66 sp_a4_finale1: 57,
67 "sp_a4_finale2": 58, 67 sp_a4_finale2: 58,
68 "sp_a4_finale3": 59, 68 sp_a4_finale3: 59,
69 "sp_a4_finale4": 60, 69 sp_a4_finale4: 60,
70 70
71 "mp_coop_start": 61, 71 mp_coop_start: 61,
72 "mp_coop_lobby_3": 62, 72 mp_coop_lobby_3: 62,
73 73
74 "mp_coop_doors": 63, 74 mp_coop_doors: 63,
75 "mp_coop_race_2": 64, 75 mp_coop_race_2: 64,
76 "mp_coop_laser_2": 65, 76 mp_coop_laser_2: 65,
77 "mp_coop_rat_maze": 66, 77 mp_coop_rat_maze: 66,
78 "mp_coop_laser_crusher": 67, 78 mp_coop_laser_crusher: 67,
79 "mp_coop_teambts": 68, 79 mp_coop_teambts: 68,
80 80
81 "mp_coop_fling_3": 69, 81 mp_coop_fling_3: 69,
82 "mp_coop_infinifling_train": 70, 82 mp_coop_infinifling_train: 70,
83 "mp_coop_come_along": 71, 83 mp_coop_come_along: 71,
84 "mp_coop_fling_1": 72, 84 mp_coop_fling_1: 72,
85 "mp_coop_catapult_1": 73, 85 mp_coop_catapult_1: 73,
86 "mp_coop_multifling_1": 74, 86 mp_coop_multifling_1: 74,
87 "mp_coop_fling_crushers": 75, 87 mp_coop_fling_crushers: 75,
88 "mp_coop_fan": 76, 88 mp_coop_fan: 76,
89 89
90 "mp_coop_wall_intro": 77, 90 mp_coop_wall_intro: 77,
91 "mp_coop_wall_2": 78, 91 mp_coop_wall_2: 78,
92 "mp_coop_catapult_wall_intro": 79, 92 mp_coop_catapult_wall_intro: 79,
93 "mp_coop_wall_block": 80, 93 mp_coop_wall_block: 80,
94 "mp_coop_catapult_2": 81, 94 mp_coop_catapult_2: 81,
95 "mp_coop_turret_walls": 82, 95 mp_coop_turret_walls: 82,
96 "mp_coop_turret_ball": 83, 96 mp_coop_turret_ball: 83,
97 "mp_coop_wall_5": 84, 97 mp_coop_wall_5: 84,
98 98
99 "mp_coop_tbeam_redirect": 85, 99 mp_coop_tbeam_redirect: 85,
100 "mp_coop_tbeam_drill": 86, 100 mp_coop_tbeam_drill: 86,
101 "mp_coop_tbeam_catch_grind_1": 87, 101 mp_coop_tbeam_catch_grind_1: 87,
102 "mp_coop_tbeam_laser_1": 88, 102 mp_coop_tbeam_laser_1: 88,
103 "mp_coop_tbeam_polarity": 89, 103 mp_coop_tbeam_polarity: 89,
104 "mp_coop_tbeam_polarity2": 90, 104 mp_coop_tbeam_polarity2: 90,
105 "mp_coop_tbeam_polarity3": 91, 105 mp_coop_tbeam_polarity3: 91,
106 "mp_coop_tbeam_maze": 92, 106 mp_coop_tbeam_maze: 92,
107 "mp_coop_tbeam_end": 93, 107 mp_coop_tbeam_end: 93,
108 108
109 "mp_coop_paint_come_along": 94, 109 mp_coop_paint_come_along: 94,
110 "mp_coop_paint_redirect": 95, 110 mp_coop_paint_redirect: 95,
111 "mp_coop_paint_bridge": 96, 111 mp_coop_paint_bridge: 96,
112 "mp_coop_paint_walljumps": 97, 112 mp_coop_paint_walljumps: 97,
113 "mp_coop_paint_speed_fling": 98, 113 mp_coop_paint_speed_fling: 98,
114 "mp_coop_paint_red_racer": 99, 114 mp_coop_paint_red_racer: 99,
115 "mp_coop_paint_speed_catch": 100, 115 mp_coop_paint_speed_catch: 100,
116 "mp_coop_paint_longjump_intro": 101, 116 mp_coop_paint_longjump_intro: 101,
117 117
118 "mp_coop_separation_1": 102, 118 mp_coop_separation_1: 102,
119 "mp_coop_tripleaxis": 103, 119 mp_coop_tripleaxis: 103,
120 "mp_coop_catapult_catch": 104, 120 mp_coop_catapult_catch: 104,
121 "mp_coop_2paints_1bridge": 105, 121 mp_coop_2paints_1bridge: 105,
122 "mp_coop_paint_conversion": 106, 122 mp_coop_paint_conversion: 106,
123 "mp_coop_bridge_catch": 107, 123 mp_coop_bridge_catch: 107,
124 "mp_coop_laser_tbeam": 108, 124 mp_coop_laser_tbeam: 108,
125 "mp_coop_paint_rat_maze": 109, 125 mp_coop_paint_rat_maze: 109,
126 "mp_coop_paint_crazy_box": 110, 126 mp_coop_paint_crazy_box: 110,
127}; 127};
diff --git a/frontend/src/types/Pagination.ts b/frontend/src/types/Pagination.ts
index ccff04b..18494eb 100644
--- a/frontend/src/types/Pagination.ts
+++ b/frontend/src/types/Pagination.ts
@@ -3,4 +3,4 @@ export interface Pagination {
3 total_pages: number; 3 total_pages: number;
4 current_page: number; 4 current_page: number;
5 page_size: number; 5 page_size: number;
6}; 6}
diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts
index 42e5c3e..3c83d29 100644
--- a/frontend/src/types/Profile.ts
+++ b/frontend/src/types/Profile.ts
@@ -4,7 +4,7 @@ export interface UserShort {
4 steam_id: string; 4 steam_id: string;
5 user_name: string; 5 user_name: string;
6 avatar_link: string; 6 avatar_link: string;
7}; 7}
8 8
9export interface UserProfile { 9export interface UserProfile {
10 profile: boolean; 10 profile: boolean;
@@ -17,25 +17,25 @@ export interface UserProfile {
17 rankings: UserProfileRankings; 17 rankings: UserProfileRankings;
18 records: UserProfileRecords[]; 18 records: UserProfileRecords[];
19 pagination: Pagination; 19 pagination: Pagination;
20}; 20}
21 21
22interface UserProfileTitles { 22interface UserProfileTitles {
23 name: string; 23 name: string;
24 color: string; 24 color: string;
25}; 25}
26 26
27interface UserProfileLinks { 27interface UserProfileLinks {
28 p2sr: string; 28 p2sr: string;
29 steam: string; 29 steam: string;
30 youtube: string; 30 youtube: string;
31 twitch: string; 31 twitch: string;
32}; 32}
33 33
34interface UserProfileRankings { 34interface UserProfileRankings {
35 overall: UserProfileRankingsDetail; 35 overall: UserProfileRankingsDetail;
36 singleplayer: UserProfileRankingsDetail; 36 singleplayer: UserProfileRankingsDetail;
37 cooperative: UserProfileRankingsDetail; 37 cooperative: UserProfileRankingsDetail;
38}; 38}
39 39
40interface UserProfileRecords { 40interface UserProfileRecords {
41 game_id: number; 41 game_id: number;
@@ -44,8 +44,8 @@ interface UserProfileRecords {
44 map_name: string; 44 map_name: string;
45 map_wr_count: number; 45 map_wr_count: number;
46 placement: number; 46 placement: number;
47 scores: UserProfileRecordsScores[] 47 scores: UserProfileRecordsScores[];
48}; 48}
49 49
50interface UserProfileRecordsScores { 50interface UserProfileRecordsScores {
51 record_id: number; 51 record_id: number;
@@ -53,11 +53,10 @@ interface UserProfileRecordsScores {
53 score_count: number; 53 score_count: number;
54 score_time: number; 54 score_time: number;
55 date: string; 55 date: string;
56}; 56}
57 57
58interface UserProfileRankingsDetail { 58interface UserProfileRankingsDetail {
59 rank: number; 59 rank: number;
60 completion_count: number; 60 completion_count: number;
61 completion_total: number; 61 completion_total: number;
62}; 62}
63
diff --git a/frontend/src/types/Ranking.ts b/frontend/src/types/Ranking.ts
index a143355..800f4be 100644
--- a/frontend/src/types/Ranking.ts
+++ b/frontend/src/types/Ranking.ts
@@ -1,31 +1,31 @@
1import type { UserShort } from "@customTypes/Profile"; 1import type { UserShort } from "@customTypes/Profile";
2 2
3export interface RankingType { 3export interface RankingType {
4 placement: number; 4 placement: number;
5 user: UserShort; 5 user: UserShort;
6 total_score: number; 6 total_score: number;
7} 7}
8 8
9export interface SteamRankingType { 9export interface SteamRankingType {
10 user_name: string; 10 user_name: string;
11 avatar_link: string; 11 avatar_link: string;
12 steam_id: string; 12 steam_id: string;
13 sp_score: number; 13 sp_score: number;
14 mp_score: number; 14 mp_score: number;
15 overall_score: number; 15 overall_score: number;
16 sp_rank: number; 16 sp_rank: number;
17 mp_rank: number; 17 mp_rank: number;
18 overall_rank: number; 18 overall_rank: number;
19} 19}
20 20
21export interface Ranking { 21export interface Ranking {
22 rankings_overall: RankingType[]; 22 rankings_overall: RankingType[];
23 rankings_singleplayer: RankingType[]; 23 rankings_singleplayer: RankingType[];
24 rankings_multiplayer: RankingType[]; 24 rankings_multiplayer: RankingType[];
25} 25}
26 26
27export interface SteamRanking { 27export interface SteamRanking {
28 rankings_overall: SteamRankingType[]; 28 rankings_overall: SteamRankingType[];
29 rankings_singleplayer: SteamRankingType[]; 29 rankings_singleplayer: SteamRankingType[];
30 rankings_multiplayer: SteamRankingType[]; 30 rankings_multiplayer: SteamRankingType[];
31} \ No newline at end of file 31}
diff --git a/frontend/src/types/Search.ts b/frontend/src/types/Search.ts
index d218806..b258ee3 100644
--- a/frontend/src/types/Search.ts
+++ b/frontend/src/types/Search.ts
@@ -3,11 +3,11 @@ import type { UserShort } from "@customTypes/Profile";
3export interface Search { 3export interface Search {
4 players: UserShort[]; 4 players: UserShort[];
5 maps: SearchMap[]; 5 maps: SearchMap[];
6}; 6}
7 7
8interface SearchMap { 8interface SearchMap {
9 id: number; 9 id: number;
10 game: string; 10 game: string;
11 chapter: string; 11 chapter: string;
12 map: string; 12 map: string;
13}; 13}
diff --git a/frontend/src/utils/Jwt.ts b/frontend/src/utils/Jwt.ts
index ce351fb..dc6ec92 100644
--- a/frontend/src/utils/Jwt.ts
+++ b/frontend/src/utils/Jwt.ts
@@ -1,44 +1,48 @@
1// llm ahh funcs 1// llm ahh funcs
2export function get_user_id_from_token(token: string | undefined): string | undefined { 2export function get_user_id_from_token(
3 token: string | undefined
4): string | undefined {
3 if (!token) { 5 if (!token) {
4 return undefined; 6 return undefined;
5 } 7 }
6 const parts = token.split('.'); 8 const parts = token.split(".");
7 if (parts.length !== 3) { 9 if (parts.length !== 3) {
8 return undefined; 10 return undefined;
9 } 11 }
10 const base64Url = parts[1]; 12 const base64Url = parts[1];
11 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 13 const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
12 14
13 const jsonPayload = decodeURIComponent( 15 const jsonPayload = decodeURIComponent(
14 atob(base64) 16 atob(base64)
15 .split('') 17 .split("")
16 .map(function (c) { 18 .map(function (c) {
17 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 19 return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
18 }) 20 })
19 .join('') 21 .join("")
20 ); 22 );
21 return JSON.parse(jsonPayload).sub; 23 return JSON.parse(jsonPayload).sub;
22}; 24}
23 25
24export function get_user_mod_from_token(token: string | undefined): boolean | undefined { 26export function get_user_mod_from_token(
27 token: string | undefined
28): boolean | undefined {
25 if (!token) { 29 if (!token) {
26 return undefined; 30 return undefined;
27 } 31 }
28 const parts = token.split('.'); 32 const parts = token.split(".");
29 if (parts.length !== 3) { 33 if (parts.length !== 3) {
30 return undefined; 34 return undefined;
31 } 35 }
32 const base64Url = parts[1]; 36 const base64Url = parts[1];
33 const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); 37 const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
34 38
35 const jsonPayload = decodeURIComponent( 39 const jsonPayload = decodeURIComponent(
36 atob(base64) 40 atob(base64)
37 .split('') 41 .split("")
38 .map(function (c) { 42 .map(function (c) {
39 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 43 return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
40 }) 44 })
41 .join('') 45 .join("")
42 ); 46 );
43 return JSON.parse(jsonPayload).mod; 47 return JSON.parse(jsonPayload).mod;
44}; 48}
diff --git a/frontend/src/utils/Time.ts b/frontend/src/utils/Time.ts
index b83a7ed..8f2c03c 100644
--- a/frontend/src/utils/Time.ts
+++ b/frontend/src/utils/Time.ts
@@ -1,42 +1,62 @@
1export function time_ago(date: any) { 1export function time_ago(date: any) {
2 const now = new Date().getTime(); 2 const now = new Date().getTime();
3 3
4 const localDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); 4 const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
5 const seconds = Math.floor((now - localDate.getTime()) / 1000); 5 const seconds = Math.floor((now - localDate.getTime()) / 1000);
6 6
7 let interval = Math.floor(seconds / 31536000); 7 let interval = Math.floor(seconds / 31536000);
8 if (interval === 1) {return interval + ' year ago';} 8 if (interval === 1) {
9 if (interval > 1) {return interval + ' years ago';} 9 return interval + " year ago";
10 }
11 if (interval > 1) {
12 return interval + " years ago";
13 }
10 14
11 interval = Math.floor(seconds / 2592000); 15 interval = Math.floor(seconds / 2592000);
12 if (interval === 1) {return interval + ' month ago';} 16 if (interval === 1) {
13 if (interval > 1) {return interval + ' months ago';} 17 return interval + " month ago";
18 }
19 if (interval > 1) {
20 return interval + " months ago";
21 }
14 22
15 interval = Math.floor(seconds / 86400); 23 interval = Math.floor(seconds / 86400);
16 if (interval === 1) {return interval + ' day ago';} 24 if (interval === 1) {
17 if (interval > 1) {return interval + ' days ago';} 25 return interval + " day ago";
26 }
27 if (interval > 1) {
28 return interval + " days ago";
29 }
18 30
19 interval = Math.floor(seconds / 3600); 31 interval = Math.floor(seconds / 3600);
20 if (interval === 1) {return interval + ' hour ago';} 32 if (interval === 1) {
21 if (interval > 1) {return interval + ' hours ago';} 33 return interval + " hour ago";
34 }
35 if (interval > 1) {
36 return interval + " hours ago";
37 }
22 38
23 interval = Math.floor(seconds / 60); 39 interval = Math.floor(seconds / 60);
24 if (interval === 1) {return interval + ' minute ago';} 40 if (interval === 1) {
25 if (interval > 1) {return interval + ' minutes ago';} 41 return interval + " minute ago";
42 }
43 if (interval > 1) {
44 return interval + " minutes ago";
45 }
26 46
27 if(seconds < 10) return 'just now'; 47 if (seconds < 10) return "just now";
28 48
29 return Math.floor(seconds) + ' seconds ago'; 49 return Math.floor(seconds) + " seconds ago";
30}; 50}
31 51
32export function ticks_to_time(ticks: number) { 52export function ticks_to_time(ticks: number) {
33 let seconds = Math.floor(ticks / 60) 53 let seconds = Math.floor(ticks / 60);
34 let minutes = Math.floor(seconds / 60) 54 let minutes = Math.floor(seconds / 60);
35 let hours = Math.floor(minutes / 60) 55 let hours = Math.floor(minutes / 60);
36 56
37 let milliseconds = Math.floor((ticks % 60) * 1000 / 60) 57 let milliseconds = Math.floor(((ticks % 60) * 1000) / 60);
38 seconds = seconds % 60; 58 seconds = seconds % 60;
39 minutes = minutes % 60; 59 minutes = minutes % 60;
40 60
41 return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')}`; 61 return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, "0") + ":" : minutes + ":"}${minutes > 0 ? seconds.toString().padStart(2, "0") : seconds}.${milliseconds.toString().padStart(3, "0")}`;
42}; \ No newline at end of file 62}