aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorFifthWit <fifthwitbusiness@gmail.com>2025-01-30 10:44:30 -0600
committerFifthWit <fifthwitbusiness@gmail.com>2025-01-30 10:44:30 -0600
commite40f07211f5f15dcb138e2520a76d13afd3c0cfd (patch)
tree46bad6a17e66d55a4a65088c0b6eb8c48641615a /frontend/src
parentadded prettier for more consistency (diff)
downloadlphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.gz
lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.bz2
lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.zip
formatted with prettier
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.tsx42
-rw-r--r--frontend/src/api/Api.ts18
-rw-r--r--frontend/src/api/Auth.ts8
-rw-r--r--frontend/src/api/Games.ts28
-rw-r--r--frontend/src/api/Maps.ts160
-rw-r--r--frontend/src/api/Mod.ts104
-rw-r--r--frontend/src/api/Rankings.ts6
-rw-r--r--frontend/src/api/User.ts6
-rw-r--r--frontend/src/components/ConfirmDialog.tsx49
-rw-r--r--frontend/src/components/Discussions.tsx70
-rw-r--r--frontend/src/components/GameCategory.tsx37
-rw-r--r--frontend/src/components/GameEntry.tsx33
-rw-r--r--frontend/src/components/Leaderboards.tsx72
-rw-r--r--frontend/src/components/Login.tsx103
-rw-r--r--frontend/src/components/MapEntry.tsx10
-rw-r--r--frontend/src/components/MessageDialog.tsx46
-rw-r--r--frontend/src/components/MessageDialogLoad.tsx44
-rw-r--r--frontend/src/components/ModMenu.tsx126
-rw-r--r--frontend/src/components/RankingEntry.tsx94
-rw-r--r--frontend/src/components/Sidebar.tsx262
-rw-r--r--frontend/src/components/Summary.tsx265
-rw-r--r--frontend/src/components/UploadRunDialog.tsx366
-rw-r--r--frontend/src/hooks/UseConfirm.tsx61
-rw-r--r--frontend/src/hooks/UseMessage.tsx60
-rw-r--r--frontend/src/hooks/UseMessageLoad.tsx55
-rw-r--r--frontend/src/images/Images.tsx8
-rw-r--r--frontend/src/index.tsx2
-rw-r--r--frontend/src/pages/About.tsx54
-rw-r--r--frontend/src/pages/Games.tsx61
-rw-r--r--frontend/src/pages/Homepage.tsx39
-rw-r--r--frontend/src/pages/Maplist.tsx165
-rw-r--r--frontend/src/pages/Maps.tsx129
-rw-r--r--frontend/src/pages/Profile.tsx669
-rw-r--r--frontend/src/pages/Rankings.tsx319
-rw-r--r--frontend/src/pages/Rules.tsx56
-rw-r--r--frontend/src/pages/User.tsx599
-rw-r--r--frontend/src/react-app-env.d.ts4
-rw-r--r--frontend/src/types/Chapters.ts22
-rw-r--r--frontend/src/types/Content.ts8
-rw-r--r--frontend/src/types/Game.ts13
-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.ts21
-rw-r--r--frontend/src/types/Ranking.ts40
-rw-r--r--frontend/src/types/Search.ts6
-rw-r--r--frontend/src/utils/Jwt.ts12
-rw-r--r--frontend/src/utils/Time.ts62
48 files changed, 2936 insertions, 1736 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 81589f6..754f5a2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,24 +1,24 @@
1import React from "react"; 1import React 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);
@@ -78,7 +78,7 @@ const App: React.FC = () => {
78 <UploadRunDialog 78 <UploadRunDialog
79 token={token} 79 token={token}
80 open={uploadRunDialog} 80 open={uploadRunDialog}
81 onClose={(updateProfile) => { 81 onClose={updateProfile => {
82 setUploadRunDialog(false); 82 setUploadRunDialog(false);
83 if (updateProfile) { 83 if (updateProfile) {
84 _set_profile(get_user_id_from_token(token)); 84 _set_profile(get_user_id_from_token(token));
@@ -118,7 +118,7 @@ const App: React.FC = () => {
118 <Route path="/rules" element={<Rules />} /> 118 <Route path="/rules" element={<Rules />} />
119 <Route path="/about" element={<About />} /> 119 <Route path="/about" element={<About />} />
120 <Route path="/rankings" element={<Rankings />}></Route> 120 <Route path="/rankings" element={<Rankings />}></Route>
121 <Route path="*" element={"404"} /> 121 <Route path="*" element={'404'} />
122 </Routes> 122 </Routes>
123 </> 123 </>
124 ); 124 );
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts
index 0e1658c..b98dda3 100644
--- a/frontend/src/api/Api.ts
+++ b/frontend/src/api/Api.ts
@@ -1,14 +1,14 @@
1import { 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 { 4import {
5 get_games, 5 get_games,
6 get_chapters, 6 get_chapters,
7 get_games_chapters, 7 get_games_chapters,
8 get_game_maps, 8 get_game_maps,
9 get_search, 9 get_search,
10} from "@api/Games"; 10} from '@api/Games';
11import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; 11import { get_official_rankings, get_unofficial_rankings } from '@api/Rankings';
12import { 12import {
13 get_map_summary, 13 get_map_summary,
14 get_map_leaderboard, 14 get_map_leaderboard,
@@ -19,14 +19,14 @@ import {
19 delete_map_discussion, 19 delete_map_discussion,
20 post_record, 20 post_record,
21 delete_map_record, 21 delete_map_record,
22} from "@api/Maps"; 22} from '@api/Maps';
23import { 23import {
24 delete_map_summary, 24 delete_map_summary,
25 post_map_summary, 25 post_map_summary,
26 put_map_image, 26 put_map_image,
27 put_map_summary, 27 put_map_summary,
28} from "@api/Mod"; 28} from '@api/Mod';
29import { UploadRunContent } from "@customTypes/Content"; 29import { UploadRunContent } from '@customTypes/Content';
30 30
31// add new api call function entries here 31// add new api call function entries here
32// example usage: API.get_games(); 32// example usage: API.get_games();
@@ -91,7 +91,7 @@ export const API = {
91 delete_map_summary(token, map_id, route_id), 91 delete_map_summary(token, map_id, route_id),
92}; 92};
93 93
94const BASE_API_URL: string = "/api/v1/"; 94const BASE_API_URL: string = '/api/v1/';
95 95
96export function url(path: string): string { 96export function url(path: string): string {
97 return BASE_API_URL + path; 97 return BASE_API_URL + path;
diff --git a/frontend/src/api/Auth.ts b/frontend/src/api/Auth.ts
index 875c7e5..e495d47 100644
--- a/frontend/src/api/Auth.ts
+++ b/frontend/src/api/Auth.ts
@@ -1,8 +1,8 @@
1import axios from "axios"; 1import 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 }
@@ -10,5 +10,5 @@ export const get_token = async (): Promise<string | undefined> => {
10}; 10};
11 11
12export const delete_token = async () => { 12export const delete_token = async () => {
13 await axios.delete(url("token")); 13 await axios.delete(url('token'));
14}; 14};
diff --git a/frontend/src/api/Games.ts b/frontend/src/api/Games.ts
index 72bb4b3..0e47091 100644
--- a/frontend/src/api/Games.ts
+++ b/frontend/src/api/Games.ts
@@ -1,31 +1,35 @@
1import axios from "axios"; 1import axios from 'axios';
2import { url } from "@api/Api"; 2import { url } from '@api/Api';
3import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 3import { GameChapter, GamesChapters } from '@customTypes/Chapters';
4import { Game } from "@customTypes/Game"; 4import { Game } from '@customTypes/Game';
5import { Map } from "@customTypes/Map"; 5import { 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..3a22f88 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 }
@@ -25,7 +35,9 @@ export const get_map_leaderboard = async (map_id: string, page: string): Promise
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..69e76c5 100644
--- a/frontend/src/api/Mod.ts
+++ b/frontend/src/api/Mod.ts
@@ -1,58 +1,86 @@
1import axios from "axios"; 1import 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/Rankings.ts b/frontend/src/api/Rankings.ts
index b8d9bec..9afd999 100644
--- a/frontend/src/api/Rankings.ts
+++ b/frontend/src/api/Rankings.ts
@@ -1,6 +1,6 @@
1import axios from "axios"; 1import axios from 'axios';
2import { url } from "@api/Api"; 2import { url } from '@api/Api';
3import { Ranking, SteamRanking } from "@customTypes/Ranking"; 3import { Ranking, SteamRanking } from '@customTypes/Ranking';
4 4
5export const get_official_rankings = async (): Promise<Ranking> => { 5export const get_official_rankings = async (): Promise<Ranking> => {
6 const response = await axios.get(url(`rankings/lphub`)); 6 const response = await axios.get(url(`rankings/lphub`));
diff --git a/frontend/src/api/User.ts b/frontend/src/api/User.ts
index 004aa22..4ce21e1 100644
--- a/frontend/src/api/User.ts
+++ b/frontend/src/api/User.ts
@@ -1,6 +1,6 @@
1import axios from "axios"; 1import axios from 'axios';
2import { url } from "@api/Api"; 2import { url } from '@api/Api';
3import { UserProfile } from "@customTypes/Profile"; 3import { UserProfile } from '@customTypes/Profile';
4 4
5export const get_user = async (user_id: string): Promise<UserProfile> => { 5export const get_user = async (user_id: string): Promise<UserProfile> => {
6 const response = await axios.get(url(`users/${user_id}`)); 6 const response = await axios.get(url(`users/${user_id}`));
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index 44a653b..925118d 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,31 +1,36 @@
1import React from 'react'; 1import React from 'react';
2 2
3import "@css/Dialog.css" 3import '@css/Dialog.css';
4 4
5interface ConfirmDialogProps { 5interface ConfirmDialogProps {
6 title: string; 6 title: string;
7 subtitle: string; 7 subtitle: string;
8 onConfirm: () => void; 8 onConfirm: () => void;
9 onCancel: () => void; 9 onCancel: () => void;
10}; 10}
11 11
12const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { 12const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
13 return ( 13 title,
14 <div className='dimmer'> 14 subtitle,
15 <div className='dialog'> 15 onConfirm,
16 <div className='dialog-element dialog-header'> 16 onCancel,
17 <span>{title}</span> 17}) => {
18 </div> 18 return (
19 <div className='dialog-element dialog-description'> 19 <div className="dimmer">
20 <span>{subtitle}</span> 20 <div className="dialog">
21 </div> 21 <div className="dialog-element dialog-header">
22 <div className='dialog-element dialog-btns-container'> 22 <span>{title}</span>
23 <button onClick={onCancel}>Cancel</button> 23 </div>
24 <button onClick={onConfirm}>Confirm</button> 24 <div className="dialog-element dialog-description">
25 </div> 25 <span>{subtitle}</span>
26 </div> 26 </div>
27 <div className="dialog-element dialog-btns-container">
28 <button onClick={onCancel}>Cancel</button>
29 <button onClick={onConfirm}>Confirm</button>
27 </div> 30 </div>
28 ) 31 </div>
32 </div>
33 );
29}; 34};
30 35
31export default ConfirmDialog; 36export default ConfirmDialog;
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx
index 62a9fc7..994cd8e 100644
--- a/frontend/src/components/Discussions.tsx
+++ b/frontend/src/components/Discussions.tsx
@@ -1,16 +1,16 @@
1import React from "react"; 1import React from 'react';
2 2
3import { 3import {
4 MapDiscussion, 4 MapDiscussion,
5 MapDiscussions, 5 MapDiscussions,
6 MapDiscussionsDetail, 6 MapDiscussionsDetail,
7} from "@customTypes/Map"; 7} from '@customTypes/Map';
8import { MapDiscussionContent } from "@customTypes/Content"; 8import { MapDiscussionContent } from '@customTypes/Content';
9import { time_ago } from "@utils/Time"; 9import { time_ago } from '@utils/Time';
10import { API } from "@api/Api"; 10import { API } from '@api/Api';
11import "@css/Maps.css"; 11import '@css/Maps.css';
12import { Link } from "react-router-dom"; 12import { Link } from 'react-router-dom';
13import useConfirm from "@hooks/UseConfirm"; 13import useConfirm from '@hooks/UseConfirm';
14 14
15interface DiscussionsProps { 15interface DiscussionsProps {
16 token?: string; 16 token?: string;
@@ -32,17 +32,17 @@ const Discussions: React.FC<DiscussionsProps> = ({
32 const [discussionThread, setDiscussionThread] = React.useState< 32 const [discussionThread, setDiscussionThread] = React.useState<
33 MapDiscussion | undefined 33 MapDiscussion | undefined
34 >(undefined); 34 >(undefined);
35 const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); 35 const [discussionSearch, setDiscussionSearch] = React.useState<string>('');
36 36
37 const [createDiscussion, setCreateDiscussion] = 37 const [createDiscussion, setCreateDiscussion] =
38 React.useState<boolean>(false); 38 React.useState<boolean>(false);
39 const [createDiscussionContent, setCreateDiscussionContent] = 39 const [createDiscussionContent, setCreateDiscussionContent] =
40 React.useState<MapDiscussionContent>({ 40 React.useState<MapDiscussionContent>({
41 title: "", 41 title: '',
42 content: "", 42 content: '',
43 }); 43 });
44 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = 44 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] =
45 React.useState<string>(""); 45 React.useState<string>('');
46 46
47 const _open_map_discussion = async (discussion_id: number) => { 47 const _open_map_discussion = async (discussion_id: number) => {
48 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); 48 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id);
@@ -72,7 +72,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
72 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { 72 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => {
73 if ( 73 if (
74 await confirm( 74 await confirm(
75 "Delete Map Discussion", 75 'Delete Map Discussion',
76 `Are you sure you want to remove post: ${discussion.title}?` 76 `Are you sure you want to remove post: ${discussion.title}?`
77 ) 77 )
78 ) { 78 ) {
@@ -90,8 +90,8 @@ const Discussions: React.FC<DiscussionsProps> = ({
90 <input 90 <input
91 type="text" 91 type="text"
92 value={discussionSearch} 92 value={discussionSearch}
93 placeholder={"Search for posts..."} 93 placeholder={'Search for posts...'}
94 onChange={(e) => setDiscussionSearch(e.target.value)} 94 onChange={e => setDiscussionSearch(e.target.value)}
95 /> 95 />
96 <div> 96 <div>
97 <button onClick={() => setCreateDiscussion(true)}>New Post</button> 97 <button onClick={() => setCreateDiscussion(true)}>New Post</button>
@@ -104,11 +104,11 @@ const Discussions: React.FC<DiscussionsProps> = ({
104 <div id="discussion-create"> 104 <div id="discussion-create">
105 <span>Create Post</span> 105 <span>Create Post</span>
106 <button onClick={() => setCreateDiscussion(false)}>X</button> 106 <button onClick={() => setCreateDiscussion(false)}>X</button>
107 <div style={{ gridColumn: "1 / span 2" }}> 107 <div style={{ gridColumn: '1 / span 2' }}>
108 <input 108 <input
109 id="discussion-create-title" 109 id="discussion-create-title"
110 placeholder="Title..." 110 placeholder="Title..."
111 onChange={(e) => 111 onChange={e =>
112 setCreateDiscussionContent({ 112 setCreateDiscussionContent({
113 ...createDiscussionContent, 113 ...createDiscussionContent,
114 title: e.target.value, 114 title: e.target.value,
@@ -118,7 +118,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
118 <input 118 <input
119 id="discussion-create-content" 119 id="discussion-create-content"
120 placeholder="Enter the content..." 120 placeholder="Enter the content..."
121 onChange={(e) => 121 onChange={e =>
122 setCreateDiscussionContent({ 122 setCreateDiscussionContent({
123 ...createDiscussionContent, 123 ...createDiscussionContent,
124 content: e.target.value, 124 content: e.target.value,
@@ -126,7 +126,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
126 } 126 }
127 /> 127 />
128 </div> 128 </div>
129 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> 129 <div style={{ placeItems: 'end', gridColumn: '1 / span 2' }}>
130 <button 130 <button
131 id="discussion-create-button" 131 id="discussion-create-button"
132 onClick={() => _create_map_discussion()} 132 onClick={() => _create_map_discussion()}
@@ -157,8 +157,8 @@ const Discussions: React.FC<DiscussionsProps> = ({
157 {time_ago( 157 {time_ago(
158 new Date( 158 new Date(
159 discussionThread.discussion.created_at 159 discussionThread.discussion.created_at
160 .replace("T", " ") 160 .replace('T', ' ')
161 .replace("Z", "") 161 .replace('Z', '')
162 ) 162 )
163 )} 163 )}
164 </span> 164 </span>
@@ -170,7 +170,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
170 (a, b) => 170 (a, b) =>
171 new Date(a.date).getTime() - new Date(b.date).getTime() 171 new Date(a.date).getTime() - new Date(b.date).getTime()
172 ) 172 )
173 .map((e) => ( 173 .map(e => (
174 <> 174 <>
175 <Link to={`/users/${e.user.steam_id}`}> 175 <Link to={`/users/${e.user.steam_id}`}>
176 <img src={e.user.avatar_link} alt="" /> 176 <img src={e.user.avatar_link} alt="" />
@@ -180,7 +180,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
180 <span> 180 <span>
181 {time_ago( 181 {time_ago(
182 new Date( 182 new Date(
183 e.date.replace("T", " ").replace("Z", "") 183 e.date.replace('T', ' ').replace('Z', '')
184 ) 184 )
185 )} 185 )}
186 </span> 186 </span>
@@ -188,29 +188,29 @@ const Discussions: React.FC<DiscussionsProps> = ({
188 </div> 188 </div>
189 </> 189 </>
190 )) 190 ))
191 : ""} 191 : ''}
192 </div> 192 </div>
193 <div id="discussion-send"> 193 <div id="discussion-send">
194 <input 194 <input
195 type="text" 195 type="text"
196 value={createDiscussionCommentContent} 196 value={createDiscussionCommentContent}
197 placeholder={"Message"} 197 placeholder={'Message'}
198 onKeyDown={(e) => 198 onKeyDown={e =>
199 e.key === "Enter" && 199 e.key === 'Enter' &&
200 _create_map_discussion_comment(discussionThread.discussion.id) 200 _create_map_discussion_comment(discussionThread.discussion.id)
201 } 201 }
202 onChange={(e) => 202 onChange={e =>
203 setCreateDiscussionCommentContent(e.target.value) 203 setCreateDiscussionCommentContent(e.target.value)
204 } 204 }
205 /> 205 />
206 <div> 206 <div>
207 <button 207 <button
208 onClick={() => { 208 onClick={() => {
209 if (createDiscussionCommentContent !== "") { 209 if (createDiscussionCommentContent !== '') {
210 _create_map_discussion_comment( 210 _create_map_discussion_comment(
211 discussionThread.discussion.id 211 discussionThread.discussion.id
212 ); 212 );
213 setCreateDiscussionCommentContent(""); 213 setCreateDiscussionCommentContent('');
214 } 214 }
215 }} 215 }}
216 > 216 >
@@ -222,7 +222,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
222 ) : data ? ( 222 ) : data ? (
223 <> 223 <>
224 {data.discussions 224 {data.discussions
225 .filter((f) => f.title.includes(discussionSearch)) 225 .filter(f => f.title.includes(discussionSearch))
226 .sort( 226 .sort(
227 (a, b) => 227 (a, b) =>
228 new Date(b.updated_at).getTime() - 228 new Date(b.updated_at).getTime() -
@@ -234,7 +234,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
234 <span>{e.title}</span> 234 <span>{e.title}</span>
235 {isModerator ? ( 235 {isModerator ? (
236 <button 236 <button
237 onClick={(m) => { 237 onClick={m => {
238 m.stopPropagation(); 238 m.stopPropagation();
239 _delete_map_discussion(e); 239 _delete_map_discussion(e);
240 }} 240 }}
@@ -248,10 +248,10 @@ const Discussions: React.FC<DiscussionsProps> = ({
248 <b>{e.creator.user_name}:</b> {e.content} 248 <b>{e.creator.user_name}:</b> {e.content}
249 </span> 249 </span>
250 <span> 250 <span>
251 Last Updated:{" "} 251 Last Updated:{' '}
252 {time_ago( 252 {time_ago(
253 new Date( 253 new Date(
254 e.updated_at.replace("T", " ").replace("Z", "") 254 e.updated_at.replace('T', ' ').replace('Z', '')
255 ) 255 )
256 )} 256 )}
257 </span> 257 </span>
@@ -260,7 +260,7 @@ const Discussions: React.FC<DiscussionsProps> = ({
260 ))} 260 ))}
261 </> 261 </>
262 ) : ( 262 ) : (
263 <span style={{ textAlign: "center", display: "block" }}> 263 <span style={{ textAlign: 'center', display: 'block' }}>
264 No Discussions... 264 No Discussions...
265 </span> 265 </span>
266 ) 266 )
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index d8879ef..b13be48 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -1,24 +1,31 @@
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" 5import '@css/Games.css';
6 6
7interface GameCategoryProps { 7interface GameCategoryProps {
8 game: Game; 8 game: Game;
9 cat: GameCategoryPortals; 9 cat: GameCategoryPortals;
10} 10}
11 11
12const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { 12const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
13 return ( 13 return (
14 <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> 14 <Link
15 <div> 15 className="games-page-item-body-item"
16 <span className='games-page-item-body-item-title'>{cat.category.name}</span> 16 to={'/games/' + game.id + '?cat=' + cat.category.id}
17 <br /> 17 >
18 <span className='games-page-item-body-item-num'>{cat.portal_count}</span> 18 <div>
19 </div> 19 <span className="games-page-item-body-item-title">
20 </Link> 20 {cat.category.name}
21 ) 21 </span>
22} 22 <br />
23 <span className="games-page-item-body-item-num">
24 {cat.portal_count}
25 </span>
26 </div>
27 </Link>
28 );
29};
23 30
24export default GameCategory; 31export default GameCategory;
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index 3bd2842..b58bbdd 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -1,8 +1,8 @@
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" 5import '@css/Games.css';
6 6
7import GameCategory from '@components/GameCategory'; 7import GameCategory from '@components/GameCategory';
8 8
@@ -18,17 +18,26 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 }, [game.category_portals]); 18 }, [game.category_portals]);
19 19
20 return ( 20 return (
21 <Link to={"/games/" + game.id}><div className='games-page-item'> 21 <Link to={'/games/' + game.id}>
22 <div className='games-page-item-header'> 22 <div className="games-page-item">
23 <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> 23 <div className="games-page-item-header">
24 <span><b>{game.name}</b></span> 24 <div
25 style={{ backgroundImage: `url(${game.image})` }}
26 className="games-page-item-header-img"
27 ></div>
28 <span>
29 <b>{game.name}</b>
30 </span>
31 </div>
32 <div id={game.id as any as string} className="games-page-item-body">
33 {catInfo.map((cat, index) => {
34 return (
35 <GameCategory cat={cat} game={game} key={index}></GameCategory>
36 );
37 })}
38 </div>
25 </div> 39 </div>
26 <div id={game.id as any as string} className='games-page-item-body'> 40 </Link>
27 {catInfo.map((cat, index) => {
28 return <GameCategory cat={cat} game={game} key={index}></GameCategory>
29 })}
30 </div>
31 </div></Link>
32 ); 41 );
33}; 42};
34 43
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index 05f01d0..fb72f2b 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -1,12 +1,12 @@
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 { 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;
@@ -35,7 +35,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
35 if (!data) { 35 if (!data) {
36 return ( 36 return (
37 <section id="section6" className="summary2"> 37 <section id="section6" className="summary2">
38 <h1 style={{ textAlign: "center" }}> 38 <h1 style={{ textAlign: 'center' }}>
39 Map is not available for competitive boards. 39 Map is not available for competitive boards.
40 </h1> 40 </h1>
41 </section> 41 </section>
@@ -45,7 +45,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
45 if (data.records.length === 0) { 45 if (data.records.length === 0) {
46 return ( 46 return (
47 <section id="section6" className="summary2"> 47 <section id="section6" className="summary2">
48 <h1 style={{ textAlign: "center" }}>No records found.</h1> 48 <h1 style={{ textAlign: 'center' }}>No records found.</h1>
49 </section> 49 </section>
50 ); 50 );
51 } 51 }
@@ -58,8 +58,8 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
58 id="leaderboard-top" 58 id="leaderboard-top"
59 style={ 59 style={
60 data.map.is_coop 60 data.map.is_coop
61 ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } 61 ? { gridTemplateColumns: '7.5% 40% 7.5% 15% 15% 15%' }
62 : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" } 62 : { gridTemplateColumns: '7.5% 30% 10% 20% 17.5% 15%' }
63 } 63 }
64 > 64 >
65 <span>Place</span> 65 <span>Place</span>
@@ -82,13 +82,13 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
82 onClick={() => 82 onClick={() =>
83 pageNumber === 1 83 pageNumber === 1
84 ? null 84 ? null
85 : setPageNumber((prevPageNumber) => prevPageNumber - 1) 85 : setPageNumber(prevPageNumber => prevPageNumber - 1)
86 } 86 }
87 > 87 >
88 <i 88 <i
89 className="triangle" 89 className="triangle"
90 style={{ position: "relative", left: "-5px" }} 90 style={{ position: 'relative', left: '-5px' }}
91 ></i>{" "} 91 ></i>{' '}
92 </button> 92 </button>
93 <span> 93 <span>
94 {data.pagination.current_page}/{data.pagination.total_pages} 94 {data.pagination.current_page}/{data.pagination.total_pages}
@@ -97,17 +97,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
97 onClick={() => 97 onClick={() =>
98 pageNumber === data.pagination.total_pages 98 pageNumber === data.pagination.total_pages
99 ? null 99 ? null
100 : setPageNumber((prevPageNumber) => prevPageNumber + 1) 100 : setPageNumber(prevPageNumber => prevPageNumber + 1)
101 } 101 }
102 > 102 >
103 <i 103 <i
104 className="triangle" 104 className="triangle"
105 style={{ 105 style={{
106 position: "relative", 106 position: 'relative',
107 left: "5px", 107 left: '5px',
108 transform: "rotate(180deg)", 108 transform: 'rotate(180deg)',
109 }} 109 }}
110 ></i>{" "} 110 ></i>{' '}
111 </button> 111 </button>
112 </div> 112 </div>
113 </div> 113 </div>
@@ -120,33 +120,33 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
120 key={index} 120 key={index}
121 style={ 121 style={
122 data.map.is_coop 122 data.map.is_coop
123 ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } 123 ? { gridTemplateColumns: '3% 4.5% 40% 4% 3.5% 15% 15% 14.5%' }
124 : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" } 124 : { gridTemplateColumns: '3% 4.5% 30% 4% 6% 20% 17% 15%' }
125 } 125 }
126 > 126 >
127 <span>{r.placement}</span> 127 <span>{r.placement}</span>
128 <span> </span> 128 <span> </span>
129 {r.kind === "multiplayer" ? ( 129 {r.kind === 'multiplayer' ? (
130 <div> 130 <div>
131 <Link to={`/users/${r.host.steam_id}`}> 131 <Link to={`/users/${r.host.steam_id}`}>
132 <span> 132 <span>
133 <img src={r.host.avatar_link} alt="" /> &nbsp;{" "} 133 <img src={r.host.avatar_link} alt="" /> &nbsp;{' '}
134 {r.host.user_name} 134 {r.host.user_name}
135 </span> 135 </span>
136 </Link> 136 </Link>
137 <Link to={`/users/${r.partner.steam_id}`}> 137 <Link to={`/users/${r.partner.steam_id}`}>
138 <span> 138 <span>
139 <img src={r.partner.avatar_link} alt="" /> &nbsp;{" "} 139 <img src={r.partner.avatar_link} alt="" /> &nbsp;{' '}
140 {r.partner.user_name} 140 {r.partner.user_name}
141 </span> 141 </span>
142 </Link> 142 </Link>
143 </div> 143 </div>
144 ) : ( 144 ) : (
145 r.kind === "singleplayer" && ( 145 r.kind === 'singleplayer' && (
146 <div> 146 <div>
147 <Link to={`/users/${r.user.steam_id}`}> 147 <Link to={`/users/${r.user.steam_id}`}>
148 <span> 148 <span>
149 <img src={r.user.avatar_link} alt="" /> &nbsp;{" "} 149 <img src={r.user.avatar_link} alt="" /> &nbsp;{' '}
150 {r.user.user_name} 150 {r.user.user_name}
151 </span> 151 </span>
152 </Link> 152 </Link>
@@ -158,25 +158,25 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
158 <span> </span> 158 <span> </span>
159 <span 159 <span
160 className="hover-popup" 160 className="hover-popup"
161 popup-text={r.score_time + " ticks"} 161 popup-text={r.score_time + ' ticks'}
162 > 162 >
163 {ticks_to_time(r.score_time)} 163 {ticks_to_time(r.score_time)}
164 </span> 164 </span>
165 <span 165 <span
166 className="hover-popup" 166 className="hover-popup"
167 popup-text={r.record_date.replace("T", " ").split(".")[0]} 167 popup-text={r.record_date.replace('T', ' ').split('.')[0]}
168 > 168 >
169 {time_ago( 169 {time_ago(
170 new Date(r.record_date.replace("T", " ").replace("Z", "")) 170 new Date(r.record_date.replace('T', ' ').replace('Z', ''))
171 )} 171 )}
172 </span> 172 </span>
173 173
174 {r.kind === "multiplayer" ? ( 174 {r.kind === 'multiplayer' ? (
175 <span> 175 <span>
176 <button 176 <button
177 onClick={() => { 177 onClick={() => {
178 message( 178 message(
179 "Demo Information", 179 'Demo Information',
180 `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}` 180 `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`
181 ); 181 );
182 }} 182 }}
@@ -193,7 +193,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
193 alt="download" 193 alt="download"
194 style={{ 194 style={{
195 filter: 195 filter:
196 "hue-rotate(160deg) contrast(60%) saturate(1000%)", 196 'hue-rotate(160deg) contrast(60%) saturate(1000%)',
197 }} 197 }}
198 /> 198 />
199 </button> 199 </button>
@@ -207,17 +207,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
207 alt="download" 207 alt="download"
208 style={{ 208 style={{
209 filter: 209 filter:
210 "hue-rotate(300deg) contrast(60%) saturate(1000%)", 210 'hue-rotate(300deg) contrast(60%) saturate(1000%)',
211 }} 211 }}
212 /> 212 />
213 </button> 213 </button>
214 </span> 214 </span>
215 ) : ( 215 ) : (
216 r.kind === "singleplayer" && ( 216 r.kind === 'singleplayer' && (
217 <span> 217 <span>
218 <button 218 <button
219 onClick={() => { 219 onClick={() => {
220 message("Demo Information", `Demo ID: ${r.demo_id}`); 220 message('Demo Information', `Demo ID: ${r.demo_id}`);
221 }} 221 }}
222 > 222 >
223 <img src={ThreedotIcon} alt="demo_id" /> 223 <img src={ThreedotIcon} alt="demo_id" />
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index f1628b2..093e406 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -4,77 +4,78 @@ import { Link, useNavigate } from 'react-router-dom';
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"; 7import '@css/Login.css';
8 8
9interface LoginProps { 9interface LoginProps {
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 11 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
13}; 13}
14 14
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { 15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
16
17 const navigate = useNavigate(); 16 const navigate = useNavigate();
18 17
19 const _login = () => { 18 const _login = () => {
20 window.location.href = "/api/v1/login"; 19 window.location.href = '/api/v1/login';
21 }; 20 };
22 21
23 const _logout = () => { 22 const _logout = () => {
24 setProfile(undefined); 23 setProfile(undefined);
25 setToken(undefined); 24 setToken(undefined);
26 API.delete_token(); 25 API.delete_token();
27 navigate("/"); 26 navigate('/');
28 }; 27 };
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="login">
37 ( 36 <button className="sidebar-button">
38 <> 37 <img
39 <Link to="/profile" tabIndex={-1} className='login'> 38 className="avatar-img"
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>{profile.user_name}</span>
44 <button className='logout-button' onClick={_logout}> 43 </button>
45 <img src={ExitIcon} alt="" /><span /> 44 <button className="logout-button" 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="login">
54 <button className='sidebar-button'> 53 <button className="sidebar-button">
55 <img className="avatar-img" src={profile.avatar_link} alt="" /> 54 <img
56 <span>Loading Profile...</span> 55 className="avatar-img"
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>Loading Profile...</span>
61 </Link> 60 </button>
62 </> 61 <button disabled className="logout-button" 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} className="login">
72 <span> 71 <button className="sidebar-button" onClick={_login}>
73 <img src={LoginIcon} alt="Sign in through Steam" /> 72 <img className="avatar-img" src={UserIcon} alt="" />
74 </span> 73 <span>
75 </button> 74 <img src={LoginIcon} alt="Sign in through Steam" />
76 </Link> 75 </span>
77 )} 76 </button>
77 </Link>
78 )}
78 </> 79 </>
79 ); 80 );
80}; 81};
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx
index 0f494ad..f1dee5b 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..b739ebc 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..acea27d 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 f765cd8..140d6a3 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -1,12 +1,12 @@
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 '@css/ModMenu.css';
9import useConfirm from "@hooks/UseConfirm"; 9import useConfirm from '@hooks/UseConfirm';
10 10
11interface ModMenuProps { 11interface ModMenuProps {
12 token?: string; 12 token?: string;
@@ -28,26 +28,26 @@ const ModMenu: React.FC<ModMenuProps> = ({
28 28
29 const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ 29 const [routeContent, setRouteContent] = React.useState<ModMenuContent>({
30 id: 0, 30 id: 0,
31 name: "", 31 name: '',
32 score: 0, 32 score: 0,
33 date: "", 33 date: '',
34 showcase: "", 34 showcase: '',
35 description: "No description available.", 35 description: 'No description available.',
36 category_id: 1, 36 category_id: 1,
37 }); 37 });
38 38
39 const [image, setImage] = React.useState<string>(""); 39 const [image, setImage] = React.useState<string>('');
40 const [md, setMd] = React.useState<string>(""); 40 const [md, setMd] = React.useState<string>('');
41 41
42 const navigate = useNavigate(); 42 const navigate = useNavigate();
43 43
44 function compressImage(file: File): Promise<string> { 44 function compressImage(file: File): Promise<string> {
45 const reader = new FileReader(); 45 const reader = new FileReader();
46 reader.readAsDataURL(file); 46 reader.readAsDataURL(file);
47 return new Promise((resolve) => { 47 return new Promise(resolve => {
48 reader.onload = () => { 48 reader.onload = () => {
49 const img = new Image(); 49 const img = new Image();
50 if (typeof reader.result === "string") { 50 if (typeof reader.result === 'string') {
51 img.src = reader.result; 51 img.src = reader.result;
52 img.onload = () => { 52 img.onload = () => {
53 let { width, height } = img; 53 let { width, height } = img;
@@ -59,10 +59,10 @@ const ModMenu: React.FC<ModMenuProps> = ({
59 width *= 320 / height; 59 width *= 320 / height;
60 height = 320; 60 height = 320;
61 } 61 }
62 const canvas = document.createElement("canvas"); 62 const canvas = document.createElement('canvas');
63 canvas.width = width; 63 canvas.width = width;
64 canvas.height = height; 64 canvas.height = height;
65 canvas.getContext("2d")!.drawImage(img, 0, 0, width, height); 65 canvas.getContext('2d')!.drawImage(img, 0, 0, width, height);
66 resolve(canvas.toDataURL(file.type, 0.6)); 66 resolve(canvas.toDataURL(file.type, 0.6));
67 }; 67 };
68 } 68 }
@@ -73,8 +73,8 @@ const ModMenu: React.FC<ModMenuProps> = ({
73 const _edit_map_summary_image = async () => { 73 const _edit_map_summary_image = async () => {
74 if ( 74 if (
75 await confirm( 75 await confirm(
76 "Edit Map Summary Image", 76 'Edit Map Summary Image',
77 "Are you sure you want to submit this to the database?" 77 'Are you sure you want to submit this to the database?'
78 ) 78 )
79 ) { 79 ) {
80 if (token) { 80 if (token) {
@@ -82,7 +82,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
82 if (success) { 82 if (success) {
83 navigate(0); 83 navigate(0);
84 } else { 84 } else {
85 alert("Error. Check logs."); 85 alert('Error. Check logs.');
86 } 86 }
87 } 87 }
88 } 88 }
@@ -91,17 +91,17 @@ const ModMenu: React.FC<ModMenuProps> = ({
91 const _edit_map_summary_route = async () => { 91 const _edit_map_summary_route = async () => {
92 if ( 92 if (
93 await confirm( 93 await confirm(
94 "Edit Map Summary Route", 94 'Edit Map Summary Route',
95 "Are you sure you want to submit this to the database?" 95 'Are you sure you want to submit this to the database?'
96 ) 96 )
97 ) { 97 ) {
98 if (token) { 98 if (token) {
99 routeContent.date += "T00:00:00Z"; 99 routeContent.date += 'T00:00:00Z';
100 const success = await API.put_map_summary(token, mapID, routeContent); 100 const success = await API.put_map_summary(token, mapID, routeContent);
101 if (success) { 101 if (success) {
102 navigate(0); 102 navigate(0);
103 } else { 103 } else {
104 alert("Error. Check logs."); 104 alert('Error. Check logs.');
105 } 105 }
106 } 106 }
107 } 107 }
@@ -110,17 +110,17 @@ const ModMenu: React.FC<ModMenuProps> = ({
110 const _create_map_summary_route = async () => { 110 const _create_map_summary_route = async () => {
111 if ( 111 if (
112 await confirm( 112 await confirm(
113 "Create Map Summary Route", 113 'Create Map Summary Route',
114 "Are you sure you want to submit this to the database?" 114 'Are you sure you want to submit this to the database?'
115 ) 115 )
116 ) { 116 ) {
117 if (token) { 117 if (token) {
118 routeContent.date += "T00:00:00Z"; 118 routeContent.date += 'T00:00:00Z';
119 const success = await API.post_map_summary(token, mapID, routeContent); 119 const success = await API.post_map_summary(token, mapID, routeContent);
120 if (success) { 120 if (success) {
121 navigate(0); 121 navigate(0);
122 } else { 122 } else {
123 alert("Error. Check logs."); 123 alert('Error. Check logs.');
124 } 124 }
125 } 125 }
126 } 126 }
@@ -129,7 +129,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
129 const _delete_map_summary_route = async () => { 129 const _delete_map_summary_route = async () => {
130 if ( 130 if (
131 await confirm( 131 await confirm(
132 "Delete Map Summary Route", 132 'Delete Map Summary Route',
133 `Are you sure you want to submit this to the database?\n 133 `Are you sure you want to submit this to the database?\n
134 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}` 134 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}`
135 ) 135 )
@@ -143,7 +143,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
143 if (success) { 143 if (success) {
144 navigate(0); 144 navigate(0);
145 } else { 145 } else {
146 alert("Error. Check logs."); 146 alert('Error. Check logs.');
147 } 147 }
148 } 148 }
149 } 149 }
@@ -154,14 +154,14 @@ const ModMenu: React.FC<ModMenuProps> = ({
154 // add route 154 // add route
155 setRouteContent({ 155 setRouteContent({
156 id: 0, 156 id: 0,
157 name: "", 157 name: '',
158 score: 0, 158 score: 0,
159 date: "", 159 date: '',
160 showcase: "", 160 showcase: '',
161 description: "No description available.", 161 description: 'No description available.',
162 category_id: 1, 162 category_id: 1,
163 }); 163 });
164 setMd("No description available."); 164 setMd('No description available.');
165 } 165 }
166 if (menu === 2) { 166 if (menu === 2) {
167 // edit route 167 // edit route
@@ -169,7 +169,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
169 id: data.summary.routes[selectedRun].route_id, 169 id: data.summary.routes[selectedRun].route_id,
170 name: data.summary.routes[selectedRun].history.runner_name, 170 name: data.summary.routes[selectedRun].history.runner_name,
171 score: data.summary.routes[selectedRun].history.score_count, 171 score: data.summary.routes[selectedRun].history.score_count,
172 date: data.summary.routes[selectedRun].history.date.split("T")[0], 172 date: data.summary.routes[selectedRun].history.date.split('T')[0],
173 showcase: data.summary.routes[selectedRun].showcase, 173 showcase: data.summary.routes[selectedRun].showcase,
174 description: data.summary.routes[selectedRun].description, 174 description: data.summary.routes[selectedRun].description,
175 category_id: data.summary.routes[selectedRun].category.id, 175 category_id: data.summary.routes[selectedRun].category.id,
@@ -179,20 +179,20 @@ const ModMenu: React.FC<ModMenuProps> = ({
179 }, [menu, data.summary.routes, selectedRun]); 179 }, [menu, data.summary.routes, selectedRun]);
180 180
181 React.useEffect(() => { 181 React.useEffect(() => {
182 const modview = document.querySelector("div#modview") as HTMLElement; 182 const modview = document.querySelector('div#modview') as HTMLElement;
183 if (modview) { 183 if (modview) {
184 showButton 184 showButton
185 ? (modview.style.transform = "translateY(-68%)") 185 ? (modview.style.transform = 'translateY(-68%)')
186 : (modview.style.transform = "translateY(0%)"); 186 : (modview.style.transform = 'translateY(0%)');
187 } 187 }
188 188
189 const modview_block = document.querySelector( 189 const modview_block = document.querySelector(
190 "#modview_block" 190 '#modview_block'
191 ) as HTMLElement; 191 ) as HTMLElement;
192 if (modview_block) { 192 if (modview_block) {
193 showButton 193 showButton
194 ? (modview_block.style.display = "none") 194 ? (modview_block.style.display = 'none')
195 : (modview_block.style.display = "block"); 195 : (modview_block.style.display = 'block');
196 } 196 }
197 }, [showButton]); 197 }, [showButton]);
198 198
@@ -240,11 +240,9 @@ const ModMenu: React.FC<ModMenuProps> = ({
240 <input 240 <input
241 type="file" 241 type="file"
242 accept="image/*" 242 accept="image/*"
243 onChange={(e) => { 243 onChange={e => {
244 if (e.target.files) { 244 if (e.target.files) {
245 compressImage(e.target.files[0]).then((d) => 245 compressImage(e.target.files[0]).then(d => setImage(d));
246 setImage(d)
247 );
248 } 246 }
249 }} 247 }}
250 /> 248 />
@@ -275,7 +273,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
275 <input 273 <input
276 type="text" 274 type="text"
277 value={routeContent.name} 275 value={routeContent.name}
278 onChange={(e) => { 276 onChange={e => {
279 setRouteContent({ 277 setRouteContent({
280 ...routeContent, 278 ...routeContent,
281 name: e.target.value, 279 name: e.target.value,
@@ -288,7 +286,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
288 <input 286 <input
289 type="number" 287 type="number"
290 value={routeContent.score} 288 value={routeContent.score}
291 onChange={(e) => { 289 onChange={e => {
292 setRouteContent({ 290 setRouteContent({
293 ...routeContent, 291 ...routeContent,
294 score: parseInt(e.target.value), 292 score: parseInt(e.target.value),
@@ -301,7 +299,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
301 <input 299 <input
302 type="date" 300 type="date"
303 value={routeContent.date} 301 value={routeContent.date}
304 onChange={(e) => { 302 onChange={e => {
305 setRouteContent({ 303 setRouteContent({
306 ...routeContent, 304 ...routeContent,
307 date: e.target.value, 305 date: e.target.value,
@@ -314,7 +312,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
314 <input 312 <input
315 type="text" 313 type="text"
316 value={routeContent.showcase} 314 value={routeContent.showcase}
317 onChange={(e) => { 315 onChange={e => {
318 setRouteContent({ 316 setRouteContent({
319 ...routeContent, 317 ...routeContent,
320 showcase: e.target.value, 318 showcase: e.target.value,
@@ -324,12 +322,12 @@ const ModMenu: React.FC<ModMenuProps> = ({
324 </div> 322 </div>
325 <div 323 <div
326 id="modview-route-description" 324 id="modview-route-description"
327 style={{ height: "180px", gridColumn: "1 / span 5" }} 325 style={{ height: '180px', gridColumn: '1 / span 5' }}
328 > 326 >
329 <span>Description:</span> 327 <span>Description:</span>
330 <textarea 328 <textarea
331 value={routeContent.description} 329 value={routeContent.description}
332 onChange={(e) => { 330 onChange={e => {
333 setRouteContent({ 331 setRouteContent({
334 ...routeContent, 332 ...routeContent,
335 description: e.target.value, 333 description: e.target.value,
@@ -339,7 +337,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
339 /> 337 />
340 </div> 338 </div>
341 <button 339 <button
342 style={{ gridColumn: "2 / span 3", height: "40px" }} 340 style={{ gridColumn: '2 / span 3', height: '40px' }}
343 onClick={_edit_map_summary_route} 341 onClick={_edit_map_summary_route}
344 > 342 >
345 Apply 343 Apply
@@ -380,7 +378,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
380 <div id="modview-route-category"> 378 <div id="modview-route-category">
381 <span>Category:</span> 379 <span>Category:</span>
382 <select 380 <select
383 onChange={(e) => { 381 onChange={e => {
384 setRouteContent({ 382 setRouteContent({
385 ...routeContent, 383 ...routeContent,
386 category_id: parseInt(e.target.value), 384 category_id: parseInt(e.target.value),
@@ -393,8 +391,8 @@ const ModMenu: React.FC<ModMenuProps> = ({
393 <option value="2" key="2"> 391 <option value="2" key="2">
394 No SLA 392 No SLA
395 </option> 393 </option>
396 {data.map.game_name === "Portal 2 - Cooperative" ? ( 394 {data.map.game_name === 'Portal 2 - Cooperative' ? (
397 "" 395 ''
398 ) : ( 396 ) : (
399 <option value="3" key="3"> 397 <option value="3" key="3">
400 Inbounds SLA 398 Inbounds SLA
@@ -410,7 +408,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
410 <input 408 <input
411 type="text" 409 type="text"
412 value={routeContent.name} 410 value={routeContent.name}
413 onChange={(e) => { 411 onChange={e => {
414 setRouteContent({ 412 setRouteContent({
415 ...routeContent, 413 ...routeContent,
416 name: e.target.value, 414 name: e.target.value,
@@ -423,7 +421,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
423 <input 421 <input
424 type="number" 422 type="number"
425 value={routeContent.score} 423 value={routeContent.score}
426 onChange={(e) => { 424 onChange={e => {
427 setRouteContent({ 425 setRouteContent({
428 ...routeContent, 426 ...routeContent,
429 score: parseInt(e.target.value), 427 score: parseInt(e.target.value),
@@ -436,7 +434,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
436 <input 434 <input
437 type="date" 435 type="date"
438 value={routeContent.date} 436 value={routeContent.date}
439 onChange={(e) => { 437 onChange={e => {
440 setRouteContent({ 438 setRouteContent({
441 ...routeContent, 439 ...routeContent,
442 date: e.target.value, 440 date: e.target.value,
@@ -449,7 +447,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
449 <input 447 <input
450 type="text" 448 type="text"
451 value={routeContent.showcase} 449 value={routeContent.showcase}
452 onChange={(e) => { 450 onChange={e => {
453 setRouteContent({ 451 setRouteContent({
454 ...routeContent, 452 ...routeContent,
455 showcase: e.target.value, 453 showcase: e.target.value,
@@ -459,12 +457,12 @@ const ModMenu: React.FC<ModMenuProps> = ({
459 </div> 457 </div>
460 <div 458 <div
461 id="modview-route-description" 459 id="modview-route-description"
462 style={{ height: "180px", gridColumn: "1 / span 5" }} 460 style={{ height: '180px', gridColumn: '1 / span 5' }}
463 > 461 >
464 <span>Description:</span> 462 <span>Description:</span>
465 <textarea 463 <textarea
466 value={routeContent.description} 464 value={routeContent.description}
467 onChange={(e) => { 465 onChange={e => {
468 setRouteContent({ 466 setRouteContent({
469 ...routeContent, 467 ...routeContent,
470 description: e.target.value, 468 description: e.target.value,
@@ -474,7 +472,7 @@ const ModMenu: React.FC<ModMenuProps> = ({
474 /> 472 />
475 </div> 473 </div>
476 <button 474 <button
477 style={{ gridColumn: "2 / span 3", height: "40px" }} 475 style={{ gridColumn: '2 / span 3', height: '40px' }}
478 onClick={_create_map_summary_route} 476 onClick={_create_map_summary_route}
479 > 477 >
480 Apply 478 Apply
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx
index b899965..add36ca 100644
--- a/frontend/src/components/RankingEntry.tsx
+++ b/frontend/src/components/RankingEntry.tsx
@@ -1,46 +1,66 @@
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 {
4 RankingType,
5 SteamRanking,
6 SteamRankingType,
7} from '@customTypes/Ranking';
4 8
5enum RankingCategories { 9enum RankingCategories {
6 rankings_overall, 10 rankings_overall,
7 rankings_multiplayer, 11 rankings_multiplayer,
8 rankings_singleplayer 12 rankings_singleplayer,
9} 13}
10 14
11interface RankingEntryProps { 15interface RankingEntryProps {
12 curRankingData: RankingType | SteamRankingType; 16 curRankingData: RankingType | SteamRankingType;
13 currentLeaderboardType: RankingCategories 17 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} 18}
45 19
20const RankingEntry: React.FC<RankingEntryProps> = prop => {
21 if ('placement' in prop.curRankingData) {
22 return (
23 <div className="leaderboard-entry">
24 <span>{prop.curRankingData.placement}</span>
25 <div>
26 <Link to={`/users/${prop.curRankingData.user.steam_id}`}>
27 <img src={prop.curRankingData.user.avatar_link}></img>
28 <span>{prop.curRankingData.user.user_name}</span>
29 </Link>
30 </div>
31 <span>{prop.curRankingData.total_score}</span>
32 </div>
33 );
34 } else {
35 return (
36 <div className="leaderboard-entry">
37 <span>
38 {prop.currentLeaderboardType ==
39 RankingCategories.rankings_singleplayer
40 ? prop.curRankingData.sp_rank
41 : prop.currentLeaderboardType ==
42 RankingCategories.rankings_multiplayer
43 ? prop.curRankingData.mp_rank
44 : prop.curRankingData.overall_rank}
45 </span>
46 <div>
47 <Link to={`/users/${prop.curRankingData.steam_id}`}>
48 <img src={prop.curRankingData.avatar_link}></img>
49 <span>{prop.curRankingData.user_name}</span>
50 </Link>
51 </div>
52 <span>
53 {prop.currentLeaderboardType ==
54 RankingCategories.rankings_singleplayer
55 ? prop.curRankingData.sp_score
56 : prop.currentLeaderboardType ==
57 RankingCategories.rankings_multiplayer
58 ? prop.curRankingData.mp_score
59 : prop.curRankingData.overall_score}
60 </span>
61 </div>
62 );
63 }
64};
65
46export default RankingEntry; 66export default RankingEntry;
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..71b79be 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,23 +1,38 @@
1import React from 'react'; 1import React 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 {
5 BookIcon,
6 FlagIcon,
7 HelpIcon,
8 HomeIcon,
9 LogoIcon,
10 PortalIcon,
11 SearchIcon,
12 UploadIcon,
13} from '@images/Images';
5import Login from '@components/Login'; 14import Login from '@components/Login';
6import { UserProfile } from '@customTypes/Profile'; 15import { UserProfile } from '@customTypes/Profile';
7import { Search } from '@customTypes/Search'; 16import { Search } from '@customTypes/Search';
8import { API } from '@api/Api'; 17import { API } from '@api/Api';
9import "@css/Sidebar.css"; 18import '@css/Sidebar.css';
10 19
11interface SidebarProps { 20interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 21 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile; 22 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 23 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15 onUploadRun: () => void; 24 onUploadRun: () => void;
16}; 25}
17
18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => {
19 26
20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 27const Sidebar: React.FC<SidebarProps> = ({
28 setToken,
29 profile,
30 setProfile,
31 onUploadRun,
32}) => {
33 const [searchData, setSearchData] = React.useState<Search | undefined>(
34 undefined
35 );
21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 36 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
22 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 37 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true);
23 38
@@ -25,71 +40,86 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
25 const path = location.pathname; 40 const path = location.pathname;
26 41
27 const handle_sidebar_click = (clicked_sidebar_idx: number) => { 42 const handle_sidebar_click = (clicked_sidebar_idx: number) => {
28 const btn = document.querySelectorAll("button.sidebar-button"); 43 const btn = document.querySelectorAll('button.sidebar-button');
29 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } 44 if (isSidebarOpen) {
45 setSidebarOpen(false);
46 _handle_sidebar_hide();
47 }
30 // clusterfuck 48 // clusterfuck
31 btn.forEach((e, i) => { 49 btn.forEach((e, i) => {
32 btn[i].classList.remove("sidebar-button-selected") 50 btn[i].classList.remove('sidebar-button-selected');
33 btn[i].classList.add("sidebar-button-deselected") 51 btn[i].classList.add('sidebar-button-deselected');
34 }) 52 });
35 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected") 53 btn[clicked_sidebar_idx].classList.add('sidebar-button-selected');
36 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected") 54 btn[clicked_sidebar_idx].classList.remove('sidebar-button-deselected');
37 }; 55 };
38 56
39 const _handle_sidebar_hide = () => { 57 const _handle_sidebar_hide = () => {
40 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> 58 var btn = document.querySelectorAll(
41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> 59 'button.sidebar-button'
42 const side = document.querySelector("#sidebar-list") as HTMLElement; 60 ) as NodeListOf<HTMLElement>;
43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement; 61 const span = document.querySelectorAll(
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement; 62 'button.sidebar-button>span'
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement; 63 ) as NodeListOf<HTMLElement>;
64 const side = document.querySelector('#sidebar-list') as HTMLElement;
65 const searchbar = document.querySelector('#searchbar') as HTMLInputElement;
66 const uploadRunBtn = document.querySelector(
67 '#upload-run'
68 ) as HTMLInputElement;
69 const uploadRunSpan = document.querySelector(
70 '#upload-run>span'
71 ) as HTMLInputElement;
46 72
47 if (isSidebarOpen) { 73 if (isSidebarOpen) {
48 if (profile) { 74 if (profile) {
49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 75 const login = document.querySelectorAll(
50 login.style.opacity = "1" 76 '.login>button'
51 uploadRunBtn.style.width = "310px" 77 )[1] as HTMLElement;
52 uploadRunBtn.style.padding = "0.4em 0 0 11px" 78 login.style.opacity = '1';
53 uploadRunSpan.style.opacity = "0" 79 uploadRunBtn.style.width = '310px';
80 uploadRunBtn.style.padding = '0.4em 0 0 11px';
81 uploadRunSpan.style.opacity = '0';
54 setTimeout(() => { 82 setTimeout(() => {
55 uploadRunSpan.style.opacity = "1" 83 uploadRunSpan.style.opacity = '1';
56 }, 100) 84 }, 100);
57 } 85 }
58 setSidebarOpen(false); 86 setSidebarOpen(false);
59 side.style.width = "320px" 87 side.style.width = '320px';
60 btn.forEach((e, i) => { 88 btn.forEach((e, i) => {
61 e.style.width = "310px" 89 e.style.width = '310px';
62 e.style.padding = "0.4em 0 0 11px" 90 e.style.padding = '0.4em 0 0 11px';
63 setTimeout(() => { 91 setTimeout(() => {
64 span[i].style.opacity = "1" 92 span[i].style.opacity = '1';
65 }, 100) 93 }, 100);
66 }); 94 });
67 side.style.zIndex = "2" 95 side.style.zIndex = '2';
68 } else { 96 } else {
69 if (profile) { 97 if (profile) {
70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement; 98 const login = document.querySelectorAll(
71 login.style.opacity = "0" 99 '.login>button'
72 uploadRunBtn.style.width = "40px" 100 )[1] as HTMLElement;
73 uploadRunBtn.style.padding = "0.4em 0 0 5px" 101 login.style.opacity = '0';
74 uploadRunSpan.style.opacity = "0" 102 uploadRunBtn.style.width = '40px';
103 uploadRunBtn.style.padding = '0.4em 0 0 5px';
104 uploadRunSpan.style.opacity = '0';
75 } 105 }
76 setSidebarOpen(true); 106 setSidebarOpen(true);
77 side.style.width = "40px"; 107 side.style.width = '40px';
78 searchbar.focus(); 108 searchbar.focus();
79 btn.forEach((e, i) => { 109 btn.forEach((e, i) => {
80 e.style.width = "40px" 110 e.style.width = '40px';
81 e.style.padding = "0.4em 0 0 5px" 111 e.style.padding = '0.4em 0 0 5px';
82 span[i].style.opacity = "0" 112 span[i].style.opacity = '0';
83 }) 113 });
84 setTimeout(() => { 114 setTimeout(() => {
85 side.style.zIndex = "0" 115 side.style.zIndex = '0';
86 }, 300); 116 }, 300);
87 } 117 }
88 }; 118 };
89 119
90 const _handle_sidebar_lock = () => { 120 const _handle_sidebar_lock = () => {
91 if (!isSidebarLocked) { 121 if (!isSidebarLocked) {
92 _handle_sidebar_hide() 122 _handle_sidebar_hide();
93 setIsSidebarLocked(true); 123 setIsSidebarLocked(true);
94 setTimeout(() => setIsSidebarLocked(false), 300); 124 setTimeout(() => setIsSidebarLocked(false), 300);
95 } 125 }
@@ -101,98 +131,148 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
101 }; 131 };
102 132
103 React.useEffect(() => { 133 React.useEffect(() => {
104 if (path === "/") { handle_sidebar_click(1) } 134 if (path === '/') {
105 else if (path.includes("games")) { handle_sidebar_click(2) } 135 handle_sidebar_click(1);
106 else if (path.includes("rankings")) { handle_sidebar_click(3) } 136 } else if (path.includes('games')) {
137 handle_sidebar_click(2);
138 } else if (path.includes('rankings')) {
139 handle_sidebar_click(3);
140 }
107 // else if (path.includes("news")) { handle_sidebar_click(4) } 141 // else if (path.includes("news")) { handle_sidebar_click(4) }
108 // else if (path.includes("scorelog")) { handle_sidebar_click(5) } 142 // else if (path.includes("scorelog")) { handle_sidebar_click(5) }
109 else if (path.includes("profile")) { handle_sidebar_click(4) } 143 else if (path.includes('profile')) {
110 else if (path.includes("rules")) { handle_sidebar_click(5) } 144 handle_sidebar_click(4);
111 else if (path.includes("about")) { handle_sidebar_click(6) } 145 } else if (path.includes('rules')) {
146 handle_sidebar_click(5);
147 } else if (path.includes('about')) {
148 handle_sidebar_click(6);
149 }
112 }, [path]); 150 }, [path]);
113 151
114 return ( 152 return (
115 <div id='sidebar'> 153 <div id="sidebar">
116 <Link to="/" tabIndex={-1}> 154 <Link to="/" tabIndex={-1}>
117 <div id='logo'> {/* logo */} 155 <div id="logo">
118 <img src={LogoIcon} alt="" height={"80px"} /> 156 {' '}
119 <div id='logo-text'> 157 {/* logo */}
120 <span><b>PORTAL 2</b></span><br /> 158 <img src={LogoIcon} alt="" height={'80px'} />
159 <div id="logo-text">
160 <span>
161 <b>PORTAL 2</b>
162 </span>
163 <br />
121 <span>Least Portals Hub</span> 164 <span>Least Portals Hub</span>
122 </div> 165 </div>
123 </div> 166 </div>
124 </Link> 167 </Link>
125 <div id='sidebar-list'> {/* List */} 168 <div id="sidebar-list">
126 <div id='sidebar-toplist'> {/* Top */} 169 {' '}
127 170 {/* List */}
128 <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button> 171 <div id="sidebar-toplist">
129 172 {' '}
173 {/* Top */}
174 <button
175 className="sidebar-button"
176 onClick={() => _handle_sidebar_lock()}
177 >
178 <img src={SearchIcon} alt="" />
179 <span>Search</span>
180 </button>
130 <span></span> 181 <span></span>
131
132 <Link to="/" tabIndex={-1}> 182 <Link to="/" tabIndex={-1}>
133 <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button> 183 <button className="sidebar-button">
184 <img src={HomeIcon} alt="homepage" />
185 <span>Home&nbsp;Page</span>
186 </button>
134 </Link> 187 </Link>
135
136 <Link to="/games" tabIndex={-1}> 188 <Link to="/games" tabIndex={-1}>
137 <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> 189 <button className="sidebar-button">
190 <img src={PortalIcon} alt="games" />
191 <span>Games</span>
192 </button>
138 </Link> 193 </Link>
139
140 <Link to="/rankings" tabIndex={-1}> 194 <Link to="/rankings" tabIndex={-1}>
141 <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> 195 <button className="sidebar-button">
196 <img src={FlagIcon} alt="rankings" />
197 <span>Rankings</span>
198 </button>
142 </Link> 199 </Link>
143
144 {/* <Link to="/news" tabIndex={-1}> 200 {/* <Link to="/news" tabIndex={-1}>
145 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> 201 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button>
146 </Link> */} 202 </Link> */}
147
148 {/* <Link to="/scorelog" tabIndex={-1}> 203 {/* <Link to="/scorelog" tabIndex={-1}>
149 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button> 204 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button>
150 </Link> */} 205 </Link> */}
151 </div> 206 </div>
152 <div id='sidebar-bottomlist'> 207 <div id="sidebar-bottomlist">
153 <span></span> 208 <span></span>
154 209
155 { 210 {profile && profile.profile ? (
156 profile && profile.profile ? 211 <button
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button> 212 id="upload-run"
158 : 213 className="submit-run-button"
159 <span></span> 214 onClick={() => onUploadRun()}
160 } 215 >
216 <img src={UploadIcon} alt="upload" />
217 <span>Upload&nbsp;Record</span>
218 </button>
219 ) : (
220 <span></span>
221 )}
161 222
162 <Login setToken={setToken} profile={profile} setProfile={setProfile} /> 223 <Login
224 setToken={setToken}
225 profile={profile}
226 setProfile={setProfile}
227 />
163 228
164 <Link to="/rules" tabIndex={-1}> 229 <Link to="/rules" tabIndex={-1}>
165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button> 230 <button className="sidebar-button">
231 <img src={BookIcon} alt="rules" />
232 <span>Leaderboard&nbsp;Rules</span>
233 </button>
166 </Link> 234 </Link>
167 235
168 <Link to="/about" tabIndex={-1}> 236 <Link to="/about" tabIndex={-1}>
169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button> 237 <button className="sidebar-button">
238 <img src={HelpIcon} alt="about" />
239 <span>About&nbsp;LPHUB</span>
240 </button>
170 </Link> 241 </Link>
171 </div> 242 </div>
172 </div> 243 </div>
173 <div> 244 <div>
174 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> 245 <input
175 246 type="text"
176 <div id='search-data'> 247 id="searchbar"
248 placeholder="Search for map or a player..."
249 onChange={e => _handle_search_change(e.target.value)}
250 />
177 251
252 <div id="search-data">
178 {searchData?.maps.map((q, index) => ( 253 {searchData?.maps.map((q, index) => (
179 <Link to={`/maps/${q.id}`} className='search-map' key={index}> 254 <Link to={`/maps/${q.id}`} className="search-map" key={index}>
180 <span>{q.game}</span> 255 <span>{q.game}</span>
181 <span>{q.chapter}</span> 256 <span>{q.chapter}</span>
182 <span>{q.map}</span> 257 <span>{q.map}</span>
183 </Link> 258 </Link>
184 ))} 259 ))}
185 {searchData?.players.map((q, index) => 260 {searchData?.players.map((q, index) => (
186 ( 261 <Link
187 <Link to={ 262 to={
188 profile && q.steam_id === profile.steam_id ? `/profile` : 263 profile && q.steam_id === profile.steam_id
189 `/users/${q.steam_id}` 264 ? `/profile`
190 } className='search-player' key={index}> 265 : `/users/${q.steam_id}`
191 <img src={q.avatar_link} alt='pfp'></img> 266 }
192 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> 267 className="search-player"
268 key={index}
269 >
270 <img src={q.avatar_link} alt="pfp"></img>
271 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>
272 {q.user_name}
273 </span>
193 </Link> 274 </Link>
194 ))} 275 ))}
195
196 </div> 276 </div>
197 </div> 277 </div>
198 </div> 278 </div>
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 4bcaa6a..1ba166a 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -2,49 +2,67 @@ import 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" 5import '@css/Maps.css';
6 6
7interface SummaryProps { 7interface SummaryProps {
8 selectedRun: number 8 selectedRun: number;
9 setSelectedRun: (x: number) => void; 9 setSelectedRun: (x: number) => void;
10 data: MapSummary; 10 data: MapSummary;
11} 11}
12 12
13const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { 13const Summary: React.FC<SummaryProps> = ({
14 14 selectedRun,
15 setSelectedRun,
16 data,
17}) => {
15 const [selectedCategory, setSelectedCategory] = React.useState<number>(1); 18 const [selectedCategory, setSelectedCategory] = React.useState<number>(1);
16 const [historySelected, setHistorySelected] = React.useState<boolean>(false); 19 const [historySelected, setHistorySelected] = React.useState<boolean>(false);
17 20
18 function _select_run(idx: number, category_id: number) { 21 function _select_run(idx: number, category_id: number) {
19 let r = document.querySelectorAll("button.record"); 22 let r = document.querySelectorAll('button.record');
20 r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); 23 r.forEach(e => ((e as HTMLElement).style.backgroundColor = '#2b2e46'));
21 (r[idx] as HTMLElement).style.backgroundColor = "#161723" 24 (r[idx] as HTMLElement).style.backgroundColor = '#161723';
22
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(
28 e => e.category.id < category_id
29 ).length; // lethimcook
26 setSelectedRun(idx); 30 setSelectedRun(idx);
27 } 31 }
28 }; 32 }
29 33
30 function _get_youtube_id(url: string): string { 34 function _get_youtube_id(url: string): string {
31 const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); 35 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]; 36 return urlArray[2] !== undefined
33 }; 37 ? urlArray[2].split(/[^0-9a-z_-]/i)[0]
38 : urlArray[0];
39 }
34 40
35 function _category_change() { 41 function _category_change() {
36 const btn = document.querySelectorAll("#section3 #category span button"); 42 const btn = document.querySelectorAll('#section3 #category span button');
37 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 43 btn.forEach(e => {
44 (e as HTMLElement).style.backgroundColor = '#2b2e46';
45 });
38 // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories 46 // 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; 47 const idx =
40 (btn[idx] as HTMLElement).style.backgroundColor = "#202232"; 48 selectedCategory === 1
41 }; 49 ? 0
50 : data.map.is_coop
51 ? selectedCategory - 3
52 : selectedCategory - 1;
53 (btn[idx] as HTMLElement).style.backgroundColor = '#202232';
54 }
42 55
43 function _history_change() { 56 function _history_change() {
44 const btn = document.querySelectorAll("#section3 #history span button"); 57 const btn = document.querySelectorAll('#section3 #history span button');
45 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 58 btn.forEach(e => {
46 (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; 59 (e as HTMLElement).style.backgroundColor = '#2b2e46';
47 }; 60 });
61 (historySelected
62 ? (btn[1] as HTMLElement)
63 : (btn[0] as HTMLElement)
64 ).style.backgroundColor = '#202232';
65 }
48 66
49 React.useEffect(() => { 67 React.useEffect(() => {
50 _history_change(); 68 _history_change();
@@ -61,119 +79,188 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data })
61 79
62 return ( 80 return (
63 <> 81 <>
64 <section id='section3' className='summary1'> 82 <section id="section3" className="summary1">
65 <div id='category' 83 <div
66 style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> 84 id="category"
67 <img src={data.map.image} alt="" id='category-image'></img> 85 style={data.map.image === '' ? { backgroundColor: '#202232' } : {}}
68 <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> 86 >
69 {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> 87 <img src={data.map.image} alt="" id="category-image"></img>
70 {data.map.is_coop ? // TODO: make this part dynamic 88 <p>
71 ( 89 <span className="portal-count">
72 <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> 90 {data.summary.routes[selectedRun].history.score_count}
73 <button onClick={() => setSelectedCategory(1)}>CM</button> 91 </span>
74 <button onClick={() => setSelectedCategory(4)}>Any%</button> 92 {data.summary.routes[selectedRun].history.score_count === 1
75 <button onClick={() => setSelectedCategory(5)}>All Courses</button> 93 ? ` portal`
76 </span> 94 : ` portals`}
77 ) 95 </p>
78 : 96 {data.map.is_coop ? ( // TODO: make this part dynamic
79 ( 97 <span style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
80 <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}> 98 <button onClick={() => setSelectedCategory(1)}>CM</button>
81 99 <button onClick={() => setSelectedCategory(4)}>Any%</button>
82 <button onClick={() => setSelectedCategory(1)}>CM</button> 100 <button onClick={() => setSelectedCategory(5)}>
83 <button onClick={() => setSelectedCategory(2)}>NoSLA</button> 101 All Courses
84 <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button> 102 </button>
85 <button onClick={() => setSelectedCategory(4)}>Any%</button> 103 </span>
86 </span> 104 ) : (
87 ) 105 <span style={{ gridTemplateColumns: '1fr 1fr 1fr 1fr' }}>
88 } 106 <button onClick={() => setSelectedCategory(1)}>CM</button>
89 107 <button onClick={() => setSelectedCategory(2)}>NoSLA</button>
108 <button onClick={() => setSelectedCategory(3)}>
109 Inbounds SLA
110 </button>
111 <button onClick={() => setSelectedCategory(4)}>Any%</button>
112 </span>
113 )}
90 </div> 114 </div>
91 115
92 <div id='history'> 116 <div id="history">
93 117 <div style={{ display: historySelected ? 'none' : 'block' }}>
94 <div style={{ display: historySelected ? "none" : "block" }}> 118 {data.summary.routes.filter(e => e.category.id === selectedCategory)
95 {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : 119 .length === 0 ? (
120 <h5>There are no records for this map.</h5>
121 ) : (
96 <> 122 <>
97 <div className='record-top'> 123 <div className="record-top">
98 <span>Date</span> 124 <span>Date</span>
99 <span>Record</span> 125 <span>Record</span>
100 <span>First Completion</span> 126 <span>First Completion</span>
101 </div> 127 </div>
102 <hr /> 128 <hr />
103 <div id='records'> 129 <div id="records">
104
105 {data.summary.routes 130 {data.summary.routes
106 .filter(e => e.category.id === selectedCategory) 131 .filter(e => e.category.id === selectedCategory)
107 .map((r, index) => ( 132 .map((r, index) => (
108 <button className='record' key={index} onClick={() => { 133 <button
109 _select_run(index, r.category.id); 134 className="record"
110 }}> 135 key={index}
111 <span>{new Date(r.history.date).toLocaleDateString( 136 onClick={() => {
112 "en-US", { month: 'long', day: 'numeric', year: 'numeric' } 137 _select_run(index, r.category.id);
113 )}</span> 138 }}
139 >
140 <span>
141 {new Date(r.history.date).toLocaleDateString(
142 'en-US',
143 { month: 'long', day: 'numeric', year: 'numeric' }
144 )}
145 </span>
114 <span>{r.history.score_count}</span> 146 <span>{r.history.score_count}</span>
115 <span>{r.history.runner_name}</span> 147 <span>{r.history.runner_name}</span>
116 </button> 148 </button>
117 ))} 149 ))}
118 </div> 150 </div>
119 </> 151 </>
120 } 152 )}
121 </div> 153 </div>
122 154
123 <div style={{ display: historySelected ? "block" : "none" }}> 155 <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> : 156 {data.summary.routes.filter(e => e.category.id === selectedCategory)
125 <div id='graph'> 157 .length === 0 ? (
158 <h5>There are no records for this map.</h5>
159 ) : (
160 <div id="graph">
126 {/* <div>{graph(1)}</div> 161 {/* <div>{graph(1)}</div>
127 <div>{graph(2)}</div> 162 <div>{graph(2)}</div>
128 <div>{graph(3)}</div> */} 163 <div>{graph(3)}</div> */}
129 </div> 164 </div>
130 } 165 )}
131 </div> 166 </div>
132 <span> 167 <span>
133 <button onClick={() => setHistorySelected(false)}>List</button> 168 <button onClick={() => setHistorySelected(false)}>List</button>
134 <button onClick={() => setHistorySelected(true)}>Graph</button> 169 <button onClick={() => setHistorySelected(true)}>Graph</button>
135 </span> 170 </span>
136 </div> 171 </div>
137 172 </section>
138 173 <section id="section4" className="summary1">
139 </section > 174 <div id="difficulty">
140 <section id='section4' className='summary1'>
141 <div id='difficulty'>
142 <span>Difficulty</span> 175 <span>Difficulty</span>
143 {data.summary.routes[selectedRun].rating === 0 && (<span>N/A</span>)} 176 {data.summary.routes[selectedRun].rating === 0 && <span>N/A</span>}
144 {data.summary.routes[selectedRun].rating === 1 && (<span style={{ color: "lime" }}>Very easy</span>)} 177 {data.summary.routes[selectedRun].rating === 1 && (
145 {data.summary.routes[selectedRun].rating === 2 && (<span style={{ color: "green" }}>Easy</span>)} 178 <span style={{ color: 'lime' }}>Very easy</span>
146 {data.summary.routes[selectedRun].rating === 3 && (<span style={{ color: "yellow" }}>Medium</span>)} 179 )}
147 {data.summary.routes[selectedRun].rating === 4 && (<span style={{ color: "orange" }}>Hard</span>)} 180 {data.summary.routes[selectedRun].rating === 2 && (
148 {data.summary.routes[selectedRun].rating === 5 && (<span style={{ color: "red" }}>Very hard</span>)} 181 <span style={{ color: 'green' }}>Easy</span>
182 )}
183 {data.summary.routes[selectedRun].rating === 3 && (
184 <span style={{ color: 'yellow' }}>Medium</span>
185 )}
186 {data.summary.routes[selectedRun].rating === 4 && (
187 <span style={{ color: 'orange' }}>Hard</span>
188 )}
189 {data.summary.routes[selectedRun].rating === 5 && (
190 <span style={{ color: 'red' }}>Very hard</span>
191 )}
149 <div> 192 <div>
150 {data.summary.routes[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} 193 {data.summary.routes[selectedRun].rating === 1 ? (
151 {data.summary.routes[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} 194 <div
152 {data.summary.routes[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} 195 className="difficulty-rating"
153 {data.summary.routes[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} 196 style={{ backgroundColor: 'lime' }}
154 {data.summary.routes[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} 197 ></div>
198 ) : (
199 <div className="difficulty-rating"></div>
200 )}
201 {data.summary.routes[selectedRun].rating === 2 ? (
202 <div
203 className="difficulty-rating"
204 style={{ backgroundColor: 'green' }}
205 ></div>
206 ) : (
207 <div className="difficulty-rating"></div>
208 )}
209 {data.summary.routes[selectedRun].rating === 3 ? (
210 <div
211 className="difficulty-rating"
212 style={{ backgroundColor: 'yellow' }}
213 ></div>
214 ) : (
215 <div className="difficulty-rating"></div>
216 )}
217 {data.summary.routes[selectedRun].rating === 4 ? (
218 <div
219 className="difficulty-rating"
220 style={{ backgroundColor: 'orange' }}
221 ></div>
222 ) : (
223 <div className="difficulty-rating"></div>
224 )}
225 {data.summary.routes[selectedRun].rating === 5 ? (
226 <div
227 className="difficulty-rating"
228 style={{ backgroundColor: 'red' }}
229 ></div>
230 ) : (
231 <div className="difficulty-rating"></div>
232 )}
155 </div> 233 </div>
156 </div> 234 </div>
157 <div id='count'> 235 <div id="count">
158 <span>Completion Count</span> 236 <span>Completion Count</span>
159 <div>{data.summary.routes[selectedRun].completion_count}</div> 237 <div>{data.summary.routes[selectedRun].completion_count}</div>
160 </div> 238 </div>
161 </section> 239 </section>
162 240
163 <section id='section5' className='summary1'> 241 <section id="section5" className="summary1">
164 <div id='description'> 242 <div id="description">
165 {data.summary.routes[selectedRun].showcase !== "" ? 243 {data.summary.routes[selectedRun].showcase !== '' ? (
166 <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> 244 <iframe
167 : ""} 245 title="Showcase video"
246 src={
247 'https://www.youtube.com/embed/' +
248 _get_youtube_id(data.summary.routes[selectedRun].showcase)
249 }
250 >
251 {' '}
252 </iframe>
253 ) : (
254 ''
255 )}
168 <h3>Route Description</h3> 256 <h3>Route Description</h3>
169 <span id='description-text'> 257 <span id="description-text">
170 <ReactMarkdown> 258 <ReactMarkdown>
171 {data.summary.routes[selectedRun].description} 259 {data.summary.routes[selectedRun].description}
172 </ReactMarkdown> 260 </ReactMarkdown>
173 </span> 261 </span>
174 </div> 262 </div>
175 </section> 263 </section>
176
177 </> 264 </>
178 ); 265 );
179}; 266};
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index c02fdb8..227a564 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -8,7 +8,7 @@ import { API } from '@api/Api';
8import { useNavigate } from 'react-router-dom'; 8import { useNavigate } from 'react-router-dom';
9import useMessage from '@hooks/UseMessage'; 9import useMessage from '@hooks/UseMessage';
10import useConfirm from '@hooks/UseConfirm'; 10import useConfirm from '@hooks/UseConfirm';
11import useMessageLoad from "@hooks/UseMessageLoad"; 11import useMessageLoad from '@hooks/UseMessageLoad';
12import { MapNames } from '@customTypes/MapNames'; 12import { MapNames } from '@customTypes/MapNames';
13 13
14interface UploadRunDialogProps { 14interface UploadRunDialogProps {
@@ -18,21 +18,27 @@ interface UploadRunDialogProps {
18 games: Game[]; 18 games: Game[];
19} 19}
20 20
21const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { 21const UploadRunDialog: React.FC<UploadRunDialogProps> = ({
22 22 token,
23 open,
24 onClose,
25 games,
26}) => {
23 const { message, MessageDialogComponent } = useMessage(); 27 const { message, MessageDialogComponent } = useMessage();
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 28 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 29 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
30 useMessageLoad();
26 31
27 const navigate = useNavigate(); 32 const navigate = useNavigate();
28 33
29 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 34 const [uploadRunContent, setUploadRunContent] =
30 host_demo: null, 35 React.useState<UploadRunContent>({
31 partner_demo: null, 36 host_demo: null,
32 }); 37 partner_demo: null,
38 });
33 39
34 const [selectedGameID, setSelectedGameID] = React.useState<number>(0); 40 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
35 const [selectedGameName, setSelectedGameName] = React.useState<string>(""); 41 const [selectedGameName, setSelectedGameName] = React.useState<string>('');
36 42
37 // dropdowns 43 // dropdowns
38 const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false); 44 const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false);
@@ -41,7 +47,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
41 const [loading, setLoading] = React.useState<boolean>(false); 47 const [loading, setLoading] = React.useState<boolean>(false);
42 48
43 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); 49 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false);
44 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); 50 const [dragHightlightPartner, setDragHighlightPartner] =
51 React.useState<boolean>(false);
45 52
46 const fileInputRef = React.useRef<HTMLInputElement>(null); 53 const fileInputRef = React.useRef<HTMLInputElement>(null);
47 const fileInputRefPartner = React.useRef<HTMLInputElement>(null); 54 const fileInputRefPartner = React.useRef<HTMLInputElement>(null);
@@ -52,9 +59,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
52 } else { 59 } else {
53 fileInputRefPartner.current?.click(); 60 fileInputRefPartner.current?.click();
54 } 61 }
55 } 62 };
56 63
57 const _handle_drag_over = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 64 const _handle_drag_over = (
65 e: React.DragEvent<HTMLDivElement>,
66 host: boolean
67 ) => {
58 e.preventDefault(); 68 e.preventDefault();
59 e.stopPropagation(); 69 e.stopPropagation();
60 if (host) { 70 if (host) {
@@ -62,9 +72,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
62 } else { 72 } else {
63 setDragHighlightPartner(true); 73 setDragHighlightPartner(true);
64 } 74 }
65 } 75 };
66 76
67 const _handle_drag_leave = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 77 const _handle_drag_leave = (
78 e: React.DragEvent<HTMLDivElement>,
79 host: boolean
80 ) => {
68 e.preventDefault(); 81 e.preventDefault();
69 e.stopPropagation(); 82 e.stopPropagation();
70 if (host) { 83 if (host) {
@@ -72,7 +85,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
72 } else { 85 } else {
73 setDragHighlightPartner(false); 86 setDragHighlightPartner(false);
74 } 87 }
75 } 88 };
76 89
77 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 90 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => {
78 e.preventDefault(); 91 e.preventDefault();
@@ -80,7 +93,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
80 setDragHighlight(true); 93 setDragHighlight(true);
81 94
82 _handle_file_change(e.dataTransfer.files, host); 95 _handle_file_change(e.dataTransfer.files, host);
83 } 96 };
84 97
85 const _handle_dropdowns = (dropdown: number) => { 98 const _handle_dropdowns = (dropdown: number) => {
86 setDropdown1Vis(false); 99 setDropdown1Vis(false);
@@ -89,9 +102,9 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
89 setDropdown1Vis(!dropdown1Vis); 102 setDropdown1Vis(!dropdown1Vis);
90 } else if (dropdown == 2) { 103 } else if (dropdown == 2) {
91 setDropdown2Vis(!dropdown2Vis); 104 setDropdown2Vis(!dropdown2Vis);
92 document.querySelector("#dropdown2")?.scrollTo(0, 0); 105 document.querySelector('#dropdown2')?.scrollTo(0, 0);
93 } 106 }
94 } 107 };
95 108
96 const _handle_game_select = async (game_id: string, game_name: string) => { 109 const _handle_game_select = async (game_id: string, game_name: string) => {
97 setLoading(true); 110 setLoading(true);
@@ -120,62 +133,85 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
120 if (token) { 133 if (token) {
121 if (games[selectedGameID].is_coop) { 134 if (games[selectedGameID].is_coop) {
122 if (uploadRunContent.host_demo === null) { 135 if (uploadRunContent.host_demo === null) {
123 await message("Error", "You must select a host demo to upload.") 136 await message('Error', 'You must select a host demo to upload.');
124 return 137 return;
125 } else if (uploadRunContent.partner_demo === null) { 138 } else if (uploadRunContent.partner_demo === null) {
126 await message("Error", "You must select a partner demo to upload.") 139 await message('Error', 'You must select a partner demo to upload.');
127 return 140 return;
128 } 141 }
129 } else { 142 } else {
130 if (uploadRunContent.host_demo === null) { 143 if (uploadRunContent.host_demo === null) {
131 await message("Error", "You must select a demo to upload.") 144 await message('Error', 'You must select a demo to upload.');
132 return 145 return;
133 } 146 }
134 } 147 }
135 const demo = SourceDemoParser.default() 148 const demo = SourceDemoParser.default()
136 .setOptions({ packets: true, header: true }) 149 .setOptions({ packets: true, header: true })
137 .parse(await uploadRunContent.host_demo.arrayBuffer()); 150 .parse(await uploadRunContent.host_demo.arrayBuffer());
138 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => { 151 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>(msg => {
139 return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate; 152 return (
140 }) 153 msg instanceof NetMessages.SvcUserMessage &&
154 msg.userMessage instanceof ScoreboardTempUpdate
155 );
156 });
141 157
142 if (!scoreboard) { 158 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.") 159 await message(
144 return 160 'Error',
161 "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."
162 );
163 return;
145 } 164 }
146 165
147 if (!demo.mapName || !MapNames[demo.mapName]) { 166 if (!demo.mapName || !MapNames[demo.mapName]) {
148 await message("Error", "Error while processing demo: Invalid map name.") 167 await message(
149 return 168 'Error',
169 'Error while processing demo: Invalid map name.'
170 );
171 return;
150 } 172 }
151 173
152 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { 174 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) {
153 await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") 175 await message(
154 return 176 'Error',
177 'Error while processing demo: Invalid cooperative demo in singleplayer submission.'
178 );
179 return;
155 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { 180 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) {
156 await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") 181 await message(
157 return 182 'Error',
183 'Error while processing demo: Invalid singleplayer demo in cooperative submission.'
184 );
185 return;
158 } 186 }
159 187
160 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 188 const { portalScore, timeScore } =
189 scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
161 190
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?`); 191 const userConfirmed = await confirm(
192 'Upload Record',
193 `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`
194 );
163 195
164 if (!userConfirmed) { 196 if (!userConfirmed) {
165 return; 197 return;
166 } 198 }
167 199
168 messageLoad("Uploading..."); 200 messageLoad('Uploading...');
169 const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); 201 const [success, response] = await API.post_record(
202 token,
203 uploadRunContent,
204 MapNames[demo.mapName]
205 );
170 messageLoadClose(); 206 messageLoadClose();
171 await message("Upload Record", response); 207 await message('Upload Record', response);
172 if (success) { 208 if (success) {
173 setUploadRunContent({ 209 setUploadRunContent({
174 host_demo: null, 210 host_demo: null,
175 partner_demo: null, 211 partner_demo: null,
176 }); 212 });
177 onClose(success); 213 onClose(success);
178 navigate("/profile"); 214 navigate('/profile');
179 } 215 }
180 } 216 }
181 }; 217 };
@@ -184,7 +220,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
184 if (open) { 220 if (open) {
185 setDragHighlightPartner(false); 221 setDragHighlightPartner(false);
186 setDragHighlight(false); 222 setDragHighlight(false);
187 _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. 223 _handle_game_select('1', 'Portal 2 - Singleplayer'); // a different approach?.
188 } 224 }
189 }, [open]); 225 }, [open]);
190 226
@@ -196,84 +232,191 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
196 {MessageDialogLoadComponent} 232 {MessageDialogLoadComponent}
197 {ConfirmDialogComponent} 233 {ConfirmDialogComponent}
198 234
199 <div id='upload-run-menu'> 235 <div id="upload-run-menu">
200 <div id='upload-run-menu-add'> 236 <div id="upload-run-menu-add">
201 <div id='upload-run-route-category'> 237 <div id="upload-run-route-category">
202 <div style={{ padding: "15px 0px" }} className='upload-run-dropdown-container upload-run-item'> 238 <div
203 <h3 style={{ margin: "0px 0px" }}>Select Game</h3> 239 style={{ padding: '15px 0px' }}
204 <div onClick={() => _handle_dropdowns(1)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> 240 className="upload-run-dropdown-container upload-run-item"
205 <div className='dropdown-cur'>{selectedGameName}</div> 241 >
206 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> 242 <h3 style={{ margin: '0px 0px' }}>Select Game</h3>
243 <div
244 onClick={() => _handle_dropdowns(1)}
245 style={{
246 display: 'flex',
247 alignItems: 'center',
248 cursor: 'pointer',
249 justifyContent: 'space-between',
250 margin: '10px 0px',
251 }}
252 >
253 <div className="dropdown-cur">{selectedGameName}</div>
254 <i
255 style={{
256 rotate: '-90deg',
257 transform: 'translate(-5px, 10px)',
258 }}
259 className="triangle"
260 ></i>
207 </div> 261 </div>
208 <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> 262 <div
209 {games.map((game) => ( 263 style={{ top: '110px' }}
210 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> 264 className={
265 dropdown1Vis
266 ? 'upload-run-dropdown'
267 : 'upload-run-dropdown hidden'
268 }
269 >
270 {games.map(game => (
271 <div
272 onClick={() => {
273 _handle_game_select(game.id.toString(), game.name);
274 _handle_dropdowns(1);
275 }}
276 key={game.id}
277 >
278 {game.name}
279 </div>
211 ))} 280 ))}
212 </div> 281 </div>
213 </div> 282 </div>
214 283
215 { 284 {!loading && (
216 !loading && 285 <>
217 ( 286 <div>
218 <> 287 <h3 style={{ margin: '10px 0px' }}>Host Demo</h3>
219 288 <div
220 <div> 289 onClick={() => {
221 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> 290 _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" : ""}`}> 291 }}
223 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> 292 onDragOver={e => {
224 {!uploadRunContent.host_demo ? 293 _handle_drag_over(e, true);
294 }}
295 onDrop={e => {
296 _handle_drop(e, true);
297 }}
298 onDragLeave={e => {
299 _handle_drag_leave(e, true);
300 }}
301 className={`upload-run-drag-area ${dragHightlight ? 'upload-run-drag-area-highlight' : ''} ${uploadRunContent.host_demo ? 'upload-run-drag-area-hidden' : ''}`}
302 >
303 <input
304 ref={fileInputRef}
305 type="file"
306 name="host_demo"
307 id="host_demo"
308 accept=".dem"
309 onChange={e =>
310 _handle_file_change(e.target.files, true)
311 }
312 />
313 {!uploadRunContent.host_demo ? (
314 <div>
315 <span>Drag and drop</span>
225 <div> 316 <div>
226 <span>Drag and drop</span> 317 <span
227 <div> 318 style={{
228 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 319 fontFamily: 'BarlowSemiCondensed-Regular',
229 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 320 }}
230 </div> 321 >
322 Or click here
323 </span>
324 <br />
325 <button
326 style={{
327 borderRadius: '24px',
328 padding: '5px 8px',
329 margin: '5px 0px',
330 }}
331 >
332 Upload
333 </button>
231 </div> 334 </div>
232 : null} 335 </div>
233 336 ) : 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 337
338 <span className="upload-run-demo-name">
339 {uploadRunContent.host_demo?.name}
340 </span>
262 </div> 341 </div>
263 342 {games[selectedGameID].is_coop && (
264 </> 343 <>
265 ) 344 <div>
266 } 345 <h3 style={{ margin: '10px 0px' }}>Partner Demo</h3>
346 <div
347 onClick={() => {
348 _handle_file_click(false);
349 }}
350 onDragOver={e => {
351 _handle_drag_over(e, false);
352 }}
353 onDrop={e => {
354 _handle_drop(e, false);
355 }}
356 onDragLeave={e => {
357 _handle_drag_leave(e, false);
358 }}
359 className={`upload-run-drag-area ${dragHightlightPartner ? 'upload-run-drag-area-highlight-partner' : ''} ${uploadRunContent.partner_demo ? 'upload-run-drag-area-hidden' : ''}`}
360 >
361 <input
362 ref={fileInputRefPartner}
363 type="file"
364 name="partner_demo"
365 id="partner_demo"
366 accept=".dem"
367 onChange={e =>
368 _handle_file_change(e.target.files, false)
369 }
370 />{' '}
371 {!uploadRunContent.partner_demo ? (
372 <div>
373 <span>Drag and drop</span>
374 <div>
375 <span
376 style={{
377 fontFamily: 'BarlowSemiCondensed-Regular',
378 }}
379 >
380 Or click here
381 </span>
382 <br />
383 <button
384 style={{
385 borderRadius: '24px',
386 padding: '5px 8px',
387 margin: '5px 0px',
388 }}
389 >
390 Upload
391 </button>
392 </div>
393 </div>
394 ) : null}
395 <span className="upload-run-demo-name">
396 {uploadRunContent.partner_demo?.name}
397 </span>
398 </div>
399 </div>
400 </>
401 )}
402 </div>
403 <div className="search-container"></div>
404 </>
405 )}
267 </div> 406 </div>
268 <div className='upload-run-buttons-container'> 407 <div className="upload-run-buttons-container">
269 <button onClick={_upload_run}>Submit</button> 408 <button onClick={_upload_run}>Submit</button>
270 <button onClick={() => { 409 <button
271 onClose(false); 410 onClick={() => {
272 setUploadRunContent({ 411 onClose(false);
273 host_demo: null, 412 setUploadRunContent({
274 partner_demo: null, 413 host_demo: null,
275 }); 414 partner_demo: null,
276 }}>Cancel</button> 415 });
416 }}
417 >
418 Cancel
419 </button>
277 </div> 420 </div>
278 </div> 421 </div>
279 </div> 422 </div>
@@ -281,10 +424,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
281 ); 424 );
282 } 425 }
283 426
284 return ( 427 return <></>;
285 <></>
286 );
287
288}; 428};
289 429
290export default UploadRunDialog; 430export default UploadRunDialog;
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx
index e86d70d..7b4bffa 100644
--- a/frontend/src/hooks/UseConfirm.tsx
+++ b/frontend/src/hooks/UseConfirm.tsx
@@ -2,39 +2,46 @@ import 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..e0afa59 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..ea0b5d8 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..9972662 100644
--- a/frontend/src/images/Images.tsx
+++ b/frontend/src/images/Images.tsx
@@ -1,5 +1,5 @@
1import logo from "./png/logo.png" 1import logo from './png/logo.png';
2import login from "./png/login.png" 2import login from './png/login.png';
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';
@@ -20,7 +20,7 @@ import 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;
@@ -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/index.tsx b/frontend/src/index.tsx
index eec2ff4..2eef1bc 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,6 +1,6 @@
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
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index a8b7826..a5d34f6 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -5,36 +5,34 @@ import { Helmet } from 'react-helmet';
5import '@css/About.css'; 5import '@css/About.css';
6 6
7const About: React.FC = () => { 7const About: React.FC = () => {
8 const [aboutText, setAboutText] = React.useState<string>('');
8 9
9 const [aboutText, setAboutText] = React.useState<string>(""); 10 React.useEffect(() => {
11 const fetchReadme = async () => {
12 try {
13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md'
15 );
16 if (!response.ok) {
17 throw new Error('Failed to fetch README');
18 }
19 const readmeText = await response.text();
20 setAboutText(readmeText);
21 } catch (error) {
22 console.error('Error fetching README:', error);
23 }
24 };
25 fetchReadme();
26 }, []);
10 27
11 React.useEffect(() => { 28 return (
12 const fetchReadme = async () => { 29 <div id="about">
13 try { 30 <Helmet>
14 const response = await fetch( 31 <title>LPHUB | About</title>
15 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' 32 </Helmet>
16 ); 33 <ReactMarkdown>{aboutText}</ReactMarkdown>
17 if (!response.ok) { 34 </div>
18 throw new Error('Failed to fetch README'); 35 );
19 }
20 const readmeText = await response.text();
21 setAboutText(readmeText);
22 } catch (error) {
23 console.error('Error fetching README:', error);
24 }
25 };
26 fetchReadme();
27 }, []);
28
29
30 return (
31 <div id="about">
32 <Helmet>
33 <title>LPHUB | About</title>
34 </Helmet>
35 <ReactMarkdown>{aboutText}</ReactMarkdown>
36 </div>
37 );
38}; 36};
39 37
40export default About; 38export default About;
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index 15cc891..ae0a2d6 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -3,44 +3,45 @@ import { Helmet } from 'react-helmet';
3 3
4import GameEntry from '@components/GameEntry'; 4import GameEntry from '@components/GameEntry';
5import { Game } from '@customTypes/Game'; 5import { Game } from '@customTypes/Game';
6import "@css/Maps.css" 6import '@css/Maps.css';
7 7
8interface GamesProps { 8interface GamesProps {
9 games: Game[]; 9 games: Game[];
10} 10}
11 11
12const Games: React.FC<GamesProps> = ({ games }) => { 12const Games: React.FC<GamesProps> = ({ games }) => {
13 const _page_load = () => {
14 const loaders = document.querySelectorAll('.loader');
15 loaders.forEach(loader => {
16 (loader as HTMLElement).style.display = 'none';
17 });
18 };
13 19
14 const _page_load = () => { 20 React.useEffect(() => {
15 const loaders = document.querySelectorAll(".loader"); 21 document
16 loaders.forEach((loader) => { 22 .querySelectorAll('.games-page-item-body')
17 (loader as HTMLElement).style.display = "none"; 23 .forEach((game, index) => {
18 }); 24 game.innerHTML = '';
19 } 25 });
26 _page_load();
27 }, []);
20 28
21 React.useEffect(() => { 29 return (
22 document.querySelectorAll(".games-page-item-body").forEach((game, index) => { 30 <div className="games-page">
23 game.innerHTML = ""; 31 <Helmet>
24 }); 32 <title>LPHUB | Games</title>
25 _page_load(); 33 </Helmet>
26 }, []); 34 <section>
27 35 <div className="games-page-content">
28 return ( 36 <div className="games-page-item-content">
29 <div className='games-page'> 37 {games.map((game, index) => (
30 <Helmet> 38 <GameEntry game={game} key={index} />
31 <title>LPHUB | Games</title> 39 ))}
32 </Helmet> 40 </div>
33 <section>
34 <div className='games-page-content'>
35 <div className='games-page-item-content'>
36 {games.map((game, index) => (
37 <GameEntry game={game} key={index} />
38 ))}
39 </div>
40 </div>
41 </section>
42 </div> 41 </div>
43 ); 42 </section>
43 </div>
44 );
44}; 45};
45 46
46export default Games; 47export default Games;
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 4f46af5..859af52 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -2,21 +2,30 @@ import React from 'react';
2import { Helmet } from 'react-helmet'; 2import { Helmet } from 'react-helmet';
3 3
4const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
5 5 return (
6 return ( 6 <main>
7 <main> 7 <Helmet>
8 <Helmet> 8 <title>LPHUB | Homepage</title>
9 <title>LPHUB | Homepage</title> 9 </Helmet>
10 </Helmet> 10 <section>
11 <section> 11 <p />
12 <p /> 12 <h1>Welcome to Least Portals Hub!</h1>
13 <h1>Welcome to Least Portals Hub!</h1> 13 <p>
14 <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> 14 At the moment, LPHUB is in beta state. This means that the site has
15 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> 15 only the core functionalities enabled for providing both collaborative
16 <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> 16 information and competitive leaderboards.
17 </section> 17 </p>
18 </main> 18 <p>
19 ); 19 The website should feel intuitive to navigate around. For any type of
20 feedback, reach us at LPHUB Discord server.
21 </p>
22 <p>
23 By using LPHUB, you agree that you have read the 'Leaderboard Rules'
24 and the 'About LPHUB' pages.
25 </p>
26 </section>
27 </main>
28 );
20}; 29};
21 30
22export default Homepage; 31export default Homepage;
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index 04938cf..5138964 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -1,11 +1,11 @@
1import React, { useEffect } from "react"; 1import React, { useEffect } from 'react';
2import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; 2import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
3import { Helmet } from "react-helmet"; 3import { Helmet } from 'react-helmet';
4 4
5import "@css/Maplist.css"; 5import '@css/Maplist.css';
6import { API } from "@api/Api"; 6import { API } from '@api/Api';
7import { Game } from "@customTypes/Game"; 7import { Game } from '@customTypes/Game';
8import { GameChapter, GamesChapters } from "@customTypes/Chapters"; 8import { GameChapter, GamesChapters } from '@customTypes/Chapters';
9 9
10const Maplist: React.FC = () => { 10const Maplist: React.FC = () => {
11 const [game, setGame] = React.useState<Game | null>(null); 11 const [game, setGame] = React.useState<Game | null>(null);
@@ -19,41 +19,41 @@ const Maplist: React.FC = () => {
19 const [curChapter, setCurChapter] = React.useState<GameChapter>(); 19 const [curChapter, setCurChapter] = React.useState<GameChapter>();
20 const [numChapters, setNumChapters] = React.useState<number>(0); 20 const [numChapters, setNumChapters] = React.useState<number>(0);
21 21
22 const [dropdownActive, setDropdownActive] = React.useState("none"); 22 const [dropdownActive, setDropdownActive] = React.useState('none');
23 23
24 const params = useParams<{ id: string, chapter: string }>(); 24 const params = useParams<{ id: string; chapter: string }>();
25 const location = useLocation(); 25 const location = useLocation();
26 const navigate = useNavigate(); 26 const navigate = useNavigate();
27 27
28 function _update_currently_selected(catNum2: number) { 28 function _update_currently_selected(catNum2: number) {
29 setCurrentlySelected(catNum2); 29 setCurrentlySelected(catNum2);
30 navigate("/games/" + game?.id + "?cat=" + catNum2); 30 navigate('/games/' + game?.id + '?cat=' + catNum2);
31 setHasClicked(true); 31 setHasClicked(true);
32 } 32 }
33 33
34 const _fetch_chapters = async (chapter_id: string) => { 34 const _fetch_chapters = async (chapter_id: string) => {
35 const chapters = await API.get_chapters(chapter_id); 35 const chapters = await API.get_chapters(chapter_id);
36 setCurChapter(chapters); 36 setCurChapter(chapters);
37 } 37 };
38 38
39 const _handle_dropdown_click = () => { 39 const _handle_dropdown_click = () => {
40 if (dropdownActive == "none") { 40 if (dropdownActive == 'none') {
41 setDropdownActive("block"); 41 setDropdownActive('block');
42 } else { 42 } else {
43 setDropdownActive("none"); 43 setDropdownActive('none');
44 } 44 }
45 } 45 };
46 46
47 // im sorry but im too lazy to fix this right now 47 // im sorry but im too lazy to fix this right now
48 useEffect(() => { 48 useEffect(() => {
49 // gameID 49 // gameID
50 const gameId = parseFloat(params.id || ""); 50 const gameId = parseFloat(params.id || '');
51 setId(gameId); 51 setId(gameId);
52 52
53 // location query params 53 // location query params
54 const queryParams = new URLSearchParams(location.search); 54 const queryParams = new URLSearchParams(location.search);
55 if (queryParams.get("chapter")) { 55 if (queryParams.get('chapter')) {
56 let cat = parseFloat(queryParams.get("chapter") || ""); 56 let cat = parseFloat(queryParams.get('chapter') || '');
57 if (gameId == 2) { 57 if (gameId == 2) {
58 cat += 10; 58 cat += 10;
59 } 59 }
@@ -62,7 +62,7 @@ const Maplist: React.FC = () => {
62 62
63 const _fetch_game = async () => { 63 const _fetch_game = async () => {
64 const games = await API.get_games(); 64 const games = await API.get_games();
65 const foundGame = games.find((game) => game.id === gameId); 65 const foundGame = games.find(game => game.id === gameId);
66 // console.log(foundGame) 66 // console.log(foundGame)
67 if (foundGame) { 67 if (foundGame) {
68 setGame(foundGame); 68 setGame(foundGame);
@@ -74,7 +74,7 @@ const Maplist: React.FC = () => {
74 const games_chapters = await API.get_games_chapters(gameId.toString()); 74 const games_chapters = await API.get_games_chapters(gameId.toString());
75 setGameChapters(games_chapters); 75 setGameChapters(games_chapters);
76 setNumChapters(games_chapters.chapters.length); 76 setNumChapters(games_chapters.chapters.length);
77 } 77 };
78 78
79 setLoad(true); 79 setLoad(true);
80 _fetch_game(); 80 _fetch_game();
@@ -83,21 +83,19 @@ const Maplist: React.FC = () => {
83 83
84 useEffect(() => { 84 useEffect(() => {
85 const queryParams = new URLSearchParams(location.search); 85 const queryParams = new URLSearchParams(location.search);
86 if (gameChapters != undefined && !queryParams.get("chapter")) { 86 if (gameChapters != undefined && !queryParams.get('chapter')) {
87 _fetch_chapters(gameChapters!.chapters[0].id.toString()); 87 _fetch_chapters(gameChapters!.chapters[0].id.toString());
88 } 88 }
89 }, [gameChapters]) 89 }, [gameChapters]);
90
91
92 90
93 return ( 91 return (
94 <main> 92 <main>
95 <Helmet> 93 <Helmet>
96 <title>LPHUB | Maplist</title> 94 <title>LPHUB | Maplist</title>
97 </Helmet> 95 </Helmet>
98 <section style={{ marginTop: "20px" }}> 96 <section style={{ marginTop: '20px' }}>
99 <Link to="/games"> 97 <Link to="/games">
100 <button className="nav-button" style={{ borderRadius: "20px" }}> 98 <button className="nav-button" style={{ borderRadius: '20px' }}>
101 <i className="triangle"></i> 99 <i className="triangle"></i>
102 <span>Games List</span> 100 <span>Games List</span>
103 </button> 101 </button>
@@ -117,7 +115,7 @@ const Maplist: React.FC = () => {
117 <h2 className="portal-count"> 115 <h2 className="portal-count">
118 { 116 {
119 game?.category_portals.find( 117 game?.category_portals.find(
120 (obj) => obj.category.id === catNum + 1 118 obj => obj.category.id === catNum + 1
121 )?.portal_count 119 )?.portal_count
122 } 120 }
123 </h2> 121 </h2>
@@ -125,7 +123,19 @@ const Maplist: React.FC = () => {
125 </div> 123 </div>
126 <div className="game-header-categories"> 124 <div className="game-header-categories">
127 {game?.category_portals.map((cat, index) => ( 125 {game?.category_portals.map((cat, index) => (
128 <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => { setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id) }}> 126 <button
127 key={index}
128 className={
129 currentlySelected == cat.category.id ||
130 (cat.category.id - 1 == catNum && !hasClicked)
131 ? 'game-cat-button selected'
132 : 'game-cat-button'
133 }
134 onClick={() => {
135 setCatNum(cat.category.id - 1);
136 _update_currently_selected(cat.category.id);
137 }}
138 >
129 <span>{cat.category.name}</span> 139 <span>{cat.category.name}</span>
130 </button> 140 </button>
131 ))} 141 ))}
@@ -136,45 +146,88 @@ const Maplist: React.FC = () => {
136 <div> 146 <div>
137 <section className="chapter-select-container"> 147 <section className="chapter-select-container">
138 <div> 148 <div>
139 <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> 149 <span
150 style={{
151 fontSize: '18px',
152 transform: 'translateY(5px)',
153 display: 'block',
154 marginTop: '10px',
155 }}
156 >
157 {curChapter?.chapter.name.split(' - ')[0]}
158 </span>
140 </div> 159 </div>
141 <div onClick={_handle_dropdown_click} className="dropdown"> 160 <div onClick={_handle_dropdown_click} className="dropdown">
142 <span>{curChapter?.chapter.name.split(" - ")[1]}</span> 161 <span>{curChapter?.chapter.name.split(' - ')[1]}</span>
143 <i className="triangle"></i> 162 <i className="triangle"></i>
144 </div> 163 </div>
145 <div className="dropdown-elements" style={{ display: dropdownActive }}> 164 <div
165 className="dropdown-elements"
166 style={{ display: dropdownActive }}
167 >
146 {gameChapters?.chapters.map((chapter, i) => { 168 {gameChapters?.chapters.map((chapter, i) => {
147 return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div> 169 return (
148 }) 170 <div
149 171 className="dropdown-element"
150 } 172 onClick={() => {
173 _fetch_chapters(chapter.id.toString());
174 _handle_dropdown_click();
175 }}
176 >
177 {chapter.name}
178 </div>
179 );
180 })}
151 </div> 181 </div>
152 </section> 182 </section>
153 <section className="maplist"> 183 <section className="maplist">
154 {curChapter?.maps.map((map, i) => { 184 {curChapter?.maps.map((map, i) => {
155 return <div className="maplist-entry"> 185 return (
156 <Link to={`/maps/${map.id}`}> 186 <div className="maplist-entry">
157 <span>{map.name}</span> 187 <Link to={`/maps/${map.id}`}>
158 <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}> 188 <span>{map.name}</span>
159 <div className="blur map"> 189 <div
160 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( 190 className="map-entry-image"
161 (obj) => obj.category.id === catNum + 1 191 style={{ backgroundImage: `url(${map.image})` }}
162 )?.portal_count}</span> 192 >
163 <span>portals</span> 193 <div className="blur map">
194 <span>
195 {map.is_disabled
196 ? map.category_portals[0].portal_count
197 : map.category_portals.find(
198 obj => obj.category.id === catNum + 1
199 )?.portal_count}
200 </span>
201 <span>portals</span>
202 </div>
164 </div> 203 </div>
165 </div> 204 <div className="difficulty-bar">
166 <div className="difficulty-bar"> 205 {/* <span>Difficulty:</span> */}
167 {/* <span>Difficulty:</span> */} 206 <div
168 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}> 207 className={
169 <div className="difficulty-point"></div> 208 map.difficulty == 0
170 <div className="difficulty-point"></div> 209 ? 'one'
171 <div className="difficulty-point"></div> 210 : map.difficulty == 1
172 <div className="difficulty-point"></div> 211 ? 'two'
173 <div className="difficulty-point"></div> 212 : map.difficulty == 2
213 ? 'three'
214 : map.difficulty == 3
215 ? 'four'
216 : map.difficulty == 4
217 ? 'five'
218 : 'one'
219 }
220 >
221 <div className="difficulty-point"></div>
222 <div className="difficulty-point"></div>
223 <div className="difficulty-point"></div>
224 <div className="difficulty-point"></div>
225 <div className="difficulty-point"></div>
226 </div>
174 </div> 227 </div>
175 </div> 228 </Link>
176 </Link> 229 </div>
177 </div> 230 );
178 })} 231 })}
179 </section> 232 </section>
180 </div> 233 </div>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index fb13563..51a2020 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -9,26 +9,31 @@ import Discussions from '@components/Discussions';
9import ModMenu from '@components/ModMenu'; 9import ModMenu from '@components/ModMenu';
10import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map'; 10import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map';
11import { API } from '@api/Api'; 11import { API } from '@api/Api';
12import "@css/Maps.css"; 12import '@css/Maps.css';
13 13
14interface MapProps { 14interface MapProps {
15 token?: string; 15 token?: string;
16 isModerator: boolean; 16 isModerator: boolean;
17}; 17}
18 18
19const Maps: React.FC<MapProps> = ({ token, isModerator }) => { 19const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
20
21 const [selectedRun, setSelectedRun] = React.useState<number>(0); 20 const [selectedRun, setSelectedRun] = React.useState<number>(0);
22 21
23 const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); 22 const [mapSummaryData, setMapSummaryData] = React.useState<
24 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); 23 MapSummary | undefined
25 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined); 24 >(undefined);
25 const [mapLeaderboardData, setMapLeaderboardData] = React.useState<
26 MapLeaderboard | undefined
27 >(undefined);
28 const [mapDiscussionsData, setMapDiscussionsData] = React.useState<
29 MapDiscussions | undefined
30 >(undefined);
26 31
27 const [navState, setNavState] = React.useState<number>(0); 32 const [navState, setNavState] = React.useState<number>(0);
28 33
29 const location = useLocation(); 34 const location = useLocation();
30 35
31 const mapID = location.pathname.split("/")[2]; 36 const mapID = location.pathname.split('/')[2];
32 37
33 const _fetch_map_summary = async () => { 38 const _fetch_map_summary = async () => {
34 const mapSummary = await API.get_map_summary(mapID); 39 const mapSummary = await API.get_map_summary(mapID);
@@ -36,7 +41,7 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
36 }; 41 };
37 42
38 const _fetch_map_leaderboards = async () => { 43 const _fetch_map_leaderboards = async () => {
39 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); 44 const mapLeaderboards = await API.get_map_leaderboard(mapID, '1');
40 setMapLeaderboardData(mapLeaderboards); 45 setMapLeaderboardData(mapLeaderboards);
41 }; 46 };
42 47
@@ -56,19 +61,36 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
56 return ( 61 return (
57 <> 62 <>
58 <main> 63 <main>
59 <section id='section1' className='summary1'> 64 <section id="section1" className="summary1">
60 <div> 65 <div>
61 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 66 <Link to="/games">
67 <button
68 className="nav-button"
69 style={{ borderRadius: '20px 20px 20px 20px' }}
70 >
71 <i className="triangle"></i>
72 <span>Games List</span>
73 </button>
74 </Link>
62 </div> 75 </div>
63 </section> 76 </section>
64 77
65 <section id='section2' className='summary1'> 78 <section id="section2" className="summary1">
66 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> 79 <button className="nav-button">
67 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 80 <img src={PortalIcon} alt="" />
68 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> 81 <span>Summary</span>
82 </button>
83 <button className="nav-button">
84 <img src={FlagIcon} alt="" />
85 <span>Leaderboards</span>
86 </button>
87 <button className="nav-button">
88 <img src={ChatIcon} alt="" />
89 <span>Discussions</span>
90 </button>
69 </section> 91 </section>
70 92
71 <section id='section6' className='summary2' /> 93 <section id="section6" className="summary2" />
72 </main> 94 </main>
73 </> 95 </>
74 ); 96 );
@@ -80,29 +102,80 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
80 <title>LPHUB | {mapSummaryData.map.map_name}</title> 102 <title>LPHUB | {mapSummaryData.map.map_name}</title>
81 <meta name="description" content={mapSummaryData.map.map_name} /> 103 <meta name="description" content={mapSummaryData.map.map_name} />
82 </Helmet> 104 </Helmet>
83 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 105 {isModerator && (
84 106 <ModMenu
85 <div id='background-image'> 107 token={token}
108 data={mapSummaryData}
109 selectedRun={selectedRun}
110 mapID={mapID}
111 />
112 )}
113
114 <div id="background-image">
86 <img src={mapSummaryData.map.image} alt="" /> 115 <img src={mapSummaryData.map.image} alt="" />
87 </div> 116 </div>
88 <main> 117 <main>
89 <section id='section1' className='summary1'> 118 <section id="section1" className="summary1">
90 <div> 119 <div>
91 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 120 <Link to="/games">
92 <Link to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> 121 <button
93 <br /><span><b>{mapSummaryData.map.map_name}</b></span> 122 className="nav-button"
123 style={{ borderRadius: '20px 0px 0px 20px' }}
124 >
125 <i className="triangle"></i>
126 <span>Games List</span>
127 </button>
128 </Link>
129 <Link
130 to={`/games/${mapSummaryData.map.is_coop ? '2' : '1'}?chapter=${mapSummaryData.map.chapter_name.split(' ')[1]}`}
131 >
132 <button
133 className="nav-button"
134 style={{ borderRadius: '0px 20px 20px 0px', marginLeft: '2px' }}
135 >
136 <i className="triangle"></i>
137 <span>{mapSummaryData.map.chapter_name}</span>
138 </button>
139 </Link>
140 <br />
141 <span>
142 <b>{mapSummaryData.map.map_name}</b>
143 </span>
94 </div> 144 </div>
95 </section> 145 </section>
96 146
97 <section id='section2' className='summary1'> 147 <section id="section2" className="summary1">
98 <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> 148 <button className="nav-button" onClick={() => setNavState(0)}>
99 <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 149 <img src={PortalIcon} alt="" />
100 <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> 150 <span>Summary</span>
151 </button>
152 <button className="nav-button" onClick={() => setNavState(1)}>
153 <img src={FlagIcon} alt="" />
154 <span>Leaderboards</span>
155 </button>
156 <button className="nav-button" onClick={() => setNavState(2)}>
157 <img src={ChatIcon} alt="" />
158 <span>Discussions</span>
159 </button>
101 </section> 160 </section>
102 161
103 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 162 {navState === 0 && (
163 <Summary
164 selectedRun={selectedRun}
165 setSelectedRun={setSelectedRun}
166 data={mapSummaryData}
167 />
168 )}
104 {navState === 1 && <Leaderboards mapID={mapID} />} 169 {navState === 1 && <Leaderboards mapID={mapID} />}
105 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 170 {navState === 2 && (
171 <Discussions
172 data={mapDiscussionsData}
173 token={token}
174 isModerator={isModerator}
175 mapID={mapID}
176 onRefresh={() => _fetch_map_discussions()}
177 />
178 )}
106 </main> 179 </main>
107 </> 180 </>
108 ); 181 );
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 48233bf..7e3d603 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -2,16 +2,28 @@ import React from 'react';
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from 'react-router-dom';
3import { Helmet } from 'react-helmet'; 3import { Helmet } from 'react-helmet';
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; 5import {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16 DeleteIcon,
17} from '@images/Images';
6import { UserProfile } from '@customTypes/Profile'; 18import { UserProfile } from '@customTypes/Profile';
7import { Game, GameChapters } from '@customTypes/Game'; 19import { Game, GameChapters } from '@customTypes/Game';
8import { Map } from '@customTypes/Map'; 20import { Map } from '@customTypes/Map';
9import { ticks_to_time } from '@utils/Time'; 21import { ticks_to_time } from '@utils/Time';
10import "@css/Profile.css"; 22import '@css/Profile.css';
11import { API } from '@api/Api'; 23import { API } from '@api/Api';
12import useConfirm from '@hooks/UseConfirm'; 24import useConfirm from '@hooks/UseConfirm';
13import useMessage from '@hooks/UseMessage'; 25import useMessage from '@hooks/UseMessage';
14import useMessageLoad from "@hooks/UseMessageLoad"; 26import useMessageLoad from '@hooks/UseMessageLoad';
15 27
16interface ProfileProps { 28interface ProfileProps {
17 profile?: UserProfile; 29 profile?: UserProfile;
@@ -20,17 +32,25 @@ interface ProfileProps {
20 onDeleteRecord: () => void; 32 onDeleteRecord: () => void;
21} 33}
22 34
23const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { 35const Profile: React.FC<ProfileProps> = ({
36 profile,
37 token,
38 gameData,
39 onDeleteRecord,
40}) => {
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 41 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { message, MessageDialogComponent } = useMessage(); 42 const { message, MessageDialogComponent } = useMessage();
26 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 43 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
44 useMessageLoad();
27 const [navState, setNavState] = React.useState(0); 45 const [navState, setNavState] = React.useState(0);
28 const [pageNumber, setPageNumber] = React.useState(1); 46 const [pageNumber, setPageNumber] = React.useState(1);
29 const [pageMax, setPageMax] = React.useState(0); 47 const [pageMax, setPageMax] = React.useState(0);
30 48
31 const [game, setGame] = React.useState("0") 49 const [game, setGame] = React.useState('0');
32 const [chapter, setChapter] = React.useState("0") 50 const [chapter, setChapter] = React.useState('0');
33 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 51 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
52 null
53 );
34 const [maps, setMaps] = React.useState<Map[]>([]); 54 const [maps, setMaps] = React.useState<Map[]>([]);
35 55
36 const navigate = useNavigate(); 56 const navigate = useNavigate();
@@ -42,17 +62,17 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
42 }; 62 };
43 63
44 const _get_game_chapters = async () => { 64 const _get_game_chapters = async () => {
45 if (game && game !== "0") { 65 if (game && game !== '0') {
46 const gameChapters = await API.get_games_chapters(game); 66 const gameChapters = await API.get_games_chapters(game);
47 setChapterData(gameChapters); 67 setChapterData(gameChapters);
48 } else if (game && game === "0") { 68 } else if (game && game === '0') {
49 setPageMax(Math.ceil(profile!.records.length / 20)); 69 setPageMax(Math.ceil(profile!.records.length / 20));
50 setPageNumber(1); 70 setPageNumber(1);
51 } 71 }
52 }; 72 };
53 73
54 const _get_game_maps = async () => { 74 const _get_game_maps = async () => {
55 if (chapter === "0") { 75 if (chapter === '0') {
56 const gameMaps = await API.get_game_maps(game); 76 const gameMaps = await API.get_game_maps(game);
57 setMaps(gameMaps); 77 setMaps(gameMaps);
58 setPageMax(Math.ceil(gameMaps.length / 20)); 78 setPageMax(Math.ceil(gameMaps.length / 20));
@@ -66,28 +86,31 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
66 }; 86 };
67 87
68 const _delete_submission = async (map_id: number, record_id: number) => { 88 const _delete_submission = async (map_id: number, record_id: number) => {
69 const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?"); 89 const userConfirmed = await confirm(
90 'Delete Record',
91 'Are you sure you want to delete this record?'
92 );
70 93
71 if (!userConfirmed) { 94 if (!userConfirmed) {
72 return; 95 return;
73 } 96 }
74 97
75 messageLoad("Deleting..."); 98 messageLoad('Deleting...');
76 99
77 const api_success = await API.delete_map_record(token!, map_id, record_id); 100 const api_success = await API.delete_map_record(token!, map_id, record_id);
78 messageLoadClose(); 101 messageLoadClose();
79 if (api_success) { 102 if (api_success) {
80 await message("Delete Record", "Successfully deleted record."); 103 await message('Delete Record', 'Successfully deleted record.');
81 onDeleteRecord(); 104 onDeleteRecord();
82 } else { 105 } else {
83 await message("Delete Record", "Could not delete record."); 106 await message('Delete Record', 'Could not delete record.');
84 } 107 }
85 }; 108 };
86 109
87 React.useEffect(() => { 110 React.useEffect(() => {
88 if (!profile) { 111 if (!profile) {
89 navigate("/"); 112 navigate('/');
90 }; 113 }
91 }, [profile]); 114 }, [profile]);
92 115
93 React.useEffect(() => { 116 React.useEffect(() => {
@@ -97,16 +120,14 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
97 }, [profile, game]); 120 }, [profile, game]);
98 121
99 React.useEffect(() => { 122 React.useEffect(() => {
100 if (profile && game !== "0") { 123 if (profile && game !== '0') {
101 _get_game_maps(); 124 _get_game_maps();
102 } 125 }
103 }, [profile, game, chapter, chapterData]) 126 }, [profile, game, chapter, chapterData]);
104 127
105 if (!profile) { 128 if (!profile) {
106 return ( 129 return <></>;
107 <></> 130 }
108 );
109 };
110 131
111 return ( 132 return (
112 <div> 133 <div>
@@ -119,230 +140,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
119 {ConfirmDialogComponent} 140 {ConfirmDialogComponent}
120 141
121 <main> 142 <main>
122 <section id='section1' className='profile'> 143 <section id="section1" className="profile">
123 144 {profile.profile ? (
124 {profile.profile 145 <div id="profile-image" onClick={_update_profile}>
125 ? ( 146 <img src={profile.avatar_link} alt="profile-image"></img>
126 <div id='profile-image' onClick={_update_profile}> 147 <span>Refresh</span>
127 <img src={profile.avatar_link} alt="profile-image"></img> 148 </div>
128 <span>Refresh</span> 149 ) : (
129 </div> 150 <div>
130 ) : ( 151 <img src={profile.avatar_link} alt="profile-image"></img>
131 <div> 152 </div>
132 <img src={profile.avatar_link} alt="profile-image"></img> 153 )}
133 </div>
134 )}
135 154
136 <div id='profile-top'> 155 <div id="profile-top">
137 <div> 156 <div>
138 <div>{profile.user_name}</div> 157 <div>{profile.user_name}</div>
139 <div> 158 <div>
140 {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} 159 {profile.country_code === 'XX' ? (
160 ''
161 ) : (
162 <img
163 src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`}
164 alt={profile.country_code}
165 />
166 )}
141 </div> 167 </div>
142 <div> 168 <div>
143 {profile.titles.map(e => ( 169 {profile.titles.map(e => (
144 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 170 <span
171 className="titles"
172 style={{ backgroundColor: `#${e.color}` }}
173 >
145 {e.name} 174 {e.name}
146 </span> 175 </span>
147 ))} 176 ))}
148 </div> 177 </div>
149 </div> 178 </div>
150 <div> 179 <div>
151 {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 180 {profile.links.steam === '-' ? (
152 {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 181 ''
153 {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 182 ) : (
154 {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 183 <a href={profile.links.steam}>
184 <img src={SteamIcon} alt="Steam" />
185 </a>
186 )}
187 {profile.links.twitch === '-' ? (
188 ''
189 ) : (
190 <a href={profile.links.twitch}>
191 <img src={TwitchIcon} alt="Twitch" />
192 </a>
193 )}
194 {profile.links.youtube === '-' ? (
195 ''
196 ) : (
197 <a href={profile.links.youtube}>
198 <img src={YouTubeIcon} alt="Youtube" />
199 </a>
200 )}
201 {profile.links.p2sr === '-' ? (
202 ''
203 ) : (
204 <a href={profile.links.p2sr}>
205 <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} />
206 </a>
207 )}
155 </div> 208 </div>
156
157 </div> 209 </div>
158 <div id='profile-bottom'> 210 <div id="profile-bottom">
159 <div> 211 <div>
160 <span>Overall</span> 212 <span>Overall</span>
161 <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} 213 <span>
162 <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> 214 {profile.rankings.overall.rank === 0
215 ? 'N/A '
216 : '#' + profile.rankings.overall.rank + ' '}
217 <span>
218 ({profile.rankings.overall.completion_count}/
219 {profile.rankings.overall.completion_total})
220 </span>
163 </span> 221 </span>
164 </div> 222 </div>
165 <div> 223 <div>
166 <span>Singleplayer</span> 224 <span>Singleplayer</span>
167 <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} 225 <span>
168 <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> 226 {profile.rankings.singleplayer.rank === 0
227 ? 'N/A '
228 : '#' + profile.rankings.singleplayer.rank + ' '}
229 <span>
230 ({profile.rankings.singleplayer.completion_count}/
231 {profile.rankings.singleplayer.completion_total})
232 </span>
169 </span> 233 </span>
170 </div> 234 </div>
171 <div> 235 <div>
172 <span>Cooperative</span> 236 <span>Cooperative</span>
173 <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} 237 <span>
174 <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> 238 {profile.rankings.cooperative.rank === 0
239 ? 'N/A '
240 : '#' + profile.rankings.cooperative.rank + ' '}
241 <span>
242 ({profile.rankings.cooperative.completion_count}/
243 {profile.rankings.cooperative.completion_total})
244 </span>
175 </span> 245 </span>
176 </div> 246 </div>
177 </div> 247 </div>
178 </section> 248 </section>
179 249
180 250 <section id="section2" className="profile">
181 <section id='section2' className='profile'> 251 <button onClick={() => setNavState(0)}>
182 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 252 <img src={FlagIcon} alt="" />
183 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 253 &nbsp;Player Records
254 </button>
255 <button onClick={() => setNavState(1)}>
256 <img src={StatisticsIcon} alt="" />
257 &nbsp;Statistics
258 </button>
184 </section> 259 </section>
185 260
186 261 <section id="section3" className="profile1">
187 262 <div id="profileboard-nav">
188 263 {gameData === null ? (
189 264 <select>error</select>
190 <section id='section3' className='profile1'> 265 ) : (
191 <div id='profileboard-nav'> 266 <select
192 {gameData === null ? <select>error</select> : 267 id="select-game"
193
194 <select id='select-game'
195 onChange={() => { 268 onChange={() => {
196 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 269 setGame(
197 setChapter("0"); 270 (document.querySelector('#select-game') as HTMLInputElement)
198 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 271 .value
272 );
273 setChapter('0');
274 const chapterSelect = document.querySelector(
275 '#select-chapter'
276 ) as HTMLSelectElement;
199 if (chapterSelect) { 277 if (chapterSelect) {
200 chapterSelect.value = "0"; 278 chapterSelect.value = '0';
201 } 279 }
202 }}> 280 }}
203 <option value={0} key={0}>All Scores</option> 281 >
282 <option value={0} key={0}>
283 All Scores
284 </option>
204 {gameData.map((e, i) => ( 285 {gameData.map((e, i) => (
205 <option value={e.id} key={i + 1}>{e.name}</option> 286 <option value={e.id} key={i + 1}>
206 ))}</select> 287 {e.name}
207 } 288 </option>
289 ))}
290 </select>
291 )}
208 292
209 {game === "0" ? 293 {game === '0' ? (
210 <select disabled> 294 <select disabled>
211 <option>All Chapters</option> 295 <option>All Chapters</option>
212 </select> 296 </select>
213 : chapterData === null ? <select></select> : 297 ) : chapterData === null ? (
214 298 <select></select>
215 <select id='select-chapter' 299 ) : (
216 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 300 <select
217 <option value="0" key="0">All Chapters</option> 301 id="select-chapter"
218 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 302 onChange={() =>
219 <option value={e.id} key={i + 1}>{e.name}</option> 303 setChapter(
220 ))}</select> 304 (
221 } 305 document.querySelector(
306 '#select-chapter'
307 ) as HTMLInputElement
308 ).value
309 )
310 }
311 >
312 <option value="0" key="0">
313 All Chapters
314 </option>
315 {chapterData.chapters
316 .filter(e => e.is_disabled === false)
317 .map((e, i) => (
318 <option value={e.id} key={i + 1}>
319 {e.name}
320 </option>
321 ))}
322 </select>
323 )}
222 </div> 324 </div>
223 <div id='profileboard-top'> 325 <div id="profileboard-top">
224 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 326 <span>
225 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 327 <span>Map Name</span>
226 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 328 <img src={SortIcon} alt="" />
227 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 329 </span>
330 <span style={{ justifyContent: 'center' }}>
331 <span>Portals</span>
332 <img src={SortIcon} alt="" />
333 </span>
334 <span style={{ justifyContent: 'center' }}>
335 <span>WRΔ </span>
336 <img src={SortIcon} alt="" />
337 </span>
338 <span style={{ justifyContent: 'center' }}>
339 <span>Time</span>
340 <img src={SortIcon} alt="" />
341 </span>
228 <span> </span> 342 <span> </span>
229 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 343 <span>
230 <span><span>Date</span><img src={SortIcon} alt="" /></span> 344 <span>Rank</span>
231 <div id='page-number'> 345 <img src={SortIcon} alt="" />
346 </span>
347 <span>
348 <span>Date</span>
349 <img src={SortIcon} alt="" />
350 </span>
351 <div id="page-number">
232 <div> 352 <div>
233 <button onClick={() => { 353 <button
234 if (pageNumber !== 1) { 354 onClick={() => {
235 setPageNumber(prevPageNumber => prevPageNumber - 1); 355 if (pageNumber !== 1) {
236 const records = document.querySelectorAll(".profileboard-record"); 356 setPageNumber(prevPageNumber => prevPageNumber - 1);
237 records.forEach((r) => { 357 const records = document.querySelectorAll(
238 (r as HTMLInputElement).style.height = "44px"; 358 '.profileboard-record'
239 }); 359 );
240 } 360 records.forEach(r => {
241 }} 361 (r as HTMLInputElement).style.height = '44px';
242 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 362 });
243 <span>{pageNumber}/{pageMax}</span> 363 }
244 <button onClick={() => { 364 }}
245 if (pageNumber !== pageMax) { 365 >
246 setPageNumber(prevPageNumber => prevPageNumber + 1); 366 <i
247 const records = document.querySelectorAll(".profileboard-record"); 367 className="triangle"
248 records.forEach((r) => { 368 style={{ position: 'relative', left: '-5px' }}
249 (r as HTMLInputElement).style.height = "44px"; 369 ></i>{' '}
250 }); 370 </button>
251 } 371 <span>
252 }} 372 {pageNumber}/{pageMax}
253 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 373 </span>
374 <button
375 onClick={() => {
376 if (pageNumber !== pageMax) {
377 setPageNumber(prevPageNumber => prevPageNumber + 1);
378 const records = document.querySelectorAll(
379 '.profileboard-record'
380 );
381 records.forEach(r => {
382 (r as HTMLInputElement).style.height = '44px';
383 });
384 }
385 }}
386 >
387 <i
388 className="triangle"
389 style={{
390 position: 'relative',
391 left: '5px',
392 transform: 'rotate(180deg)',
393 }}
394 ></i>{' '}
395 </button>
254 </div> 396 </div>
255 </div> 397 </div>
256 </div> 398 </div>
257 <hr /> 399 <hr />
258 <div id='profileboard-records'> 400 <div id="profileboard-records">
259 401 {game === '0' ? (
260 {game === "0" 402 profile.records
261 ? ( 403 .sort((a, b) => a.map_id - b.map_id)
262 404 .map((r, index) =>
263 profile.records.sort((a, b) => a.map_id - b.map_id) 405 Math.ceil((index + 1) / 20) === pageNumber ? (
264 .map((r, index) => ( 406 <button className="profileboard-record" key={index}>
265 407 {r.scores.map((e, i) => (
266 Math.ceil((index + 1) / 20) === pageNumber ? ( 408 <>
267 <button className="profileboard-record" key={index}> 409 {i !== 0 ? (
268 {r.scores.map((e, i) => (<> 410 <hr style={{ gridColumn: '1 / span 8' }} />
269 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 411 ) : (
270 412 ''
271 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 413 )}
272 414
273 <span style={{ display: "grid" }}>{e.score_count}</span> 415 <Link to={`/maps/${r.map_id}`}>
274 416 <span>{r.map_name}</span>
275 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 417 </Link>
276 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 418
277 <span> </span> 419 <span style={{ display: 'grid' }}>
278 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 420 {e.score_count}
279 <span>{e.date.split("T")[0]}</span>
280 <span style={{ flexDirection: "row-reverse" }}>
281
282 <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button>
283 <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button>
284 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button>
285 {i === 0 && r.scores.length > 1 ? <button onClick={() => {
286 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" ||
287 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ?
288 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` :
289 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px"
290 }
291 }><img src={HistoryIcon} alt="history" /></button> : ""}
292
293 </span> 421 </span>
294 </>))}
295
296 </button>
297 ) : ""
298 ))) : maps ?
299 422
300 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 423 <span style={{ display: 'grid' }}>
301 .map((r, index) => { 424 {e.score_count - r.map_wr_count > 0
302 if (Math.ceil((index + 1) / 20) === pageNumber) { 425 ? `+${e.score_count - r.map_wr_count}`
303 let record = profile.records.find((e) => e.map_id === r.id); 426 : `-`}
304 return record === undefined ? ( 427 </span>
305 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 428 <span style={{ display: 'grid' }}>
306 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 429 {ticks_to_time(e.score_time)}
307 <span style={{ display: "grid" }}>N/A</span> 430 </span>
308 <span style={{ display: "grid" }}>N/A</span>
309 <span>N/A</span>
310 <span> </span> 431 <span> </span>
311 <span>N/A</span> 432 {i === 0 ? (
312 <span>N/A</span> 433 <span>#{r.placement}</span>
313 <span style={{ flexDirection: "row-reverse" }}></span> 434 ) : (
314 </button>
315 ) : (
316 <button className="profileboard-record" key={index}>
317 {record.scores.map((e, i) => (<>
318 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
319 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link>
320 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span>
321 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span>
322 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
323 <span> </span> 435 <span> </span>
324 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 436 )}
325 <span>{record!.scores[i].date.split("T")[0]}</span> 437 <span>{e.date.split('T')[0]}</span>
326 <span style={{ flexDirection: "row-reverse" }}> 438 <span style={{ flexDirection: 'row-reverse' }}>
327 439 <button
328 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 440 style={{ marginRight: '10px' }}
329 <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> 441 onClick={() => {
330 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 442 message(
331 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 443 'Demo Information',
332 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 444 `Demo ID: ${e.demo_id}`
333 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 445 );
334 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 446 }}
335 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 447 >
448 <img src={ThreedotIcon} alt="demo_id" />
449 </button>
450 <button
451 onClick={() => {
452 _delete_submission(r.map_id, e.record_id);
453 }}
454 >
455 <img src={DeleteIcon}></img>
456 </button>
457 <button
458 onClick={() =>
459 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
336 } 460 }
337 }><img src={HistoryIcon} alt="history" /></button> : ""} 461 >
338 462 <img src={DownloadIcon} alt="download" />
463 </button>
464 {i === 0 && r.scores.length > 1 ? (
465 <button
466 onClick={() => {
467 (
468 document.querySelectorAll(
469 '.profileboard-record'
470 )[index % 20] as HTMLInputElement
471 ).style.height === '44px' ||
472 (
473 document.querySelectorAll(
474 '.profileboard-record'
475 )[index % 20] as HTMLInputElement
476 ).style.height === ''
477 ? ((
478 document.querySelectorAll(
479 '.profileboard-record'
480 )[index % 20] as HTMLInputElement
481 ).style.height =
482 `${r.scores.length * 46}px`)
483 : ((
484 document.querySelectorAll(
485 '.profileboard-record'
486 )[index % 20] as HTMLInputElement
487 ).style.height = '44px');
488 }}
489 >
490 <img src={HistoryIcon} alt="history" />
491 </button>
492 ) : (
493 ''
494 )}
495 </span>
496 </>
497 ))}
498 </button>
499 ) : (
500 ''
501 )
502 )
503 ) : maps ? (
504 maps
505 .filter(e => e.is_disabled === false)
506 .sort((a, b) => a.id - b.id)
507 .map((r, index) => {
508 if (Math.ceil((index + 1) / 20) === pageNumber) {
509 let record = profile.records.find(e => e.map_id === r.id);
510 return record === undefined ? (
511 <button
512 className="profileboard-record"
513 key={index}
514 style={{ backgroundColor: '#1b1b20' }}
515 >
516 <Link to={`/maps/${r.id}`}>
517 <span>{r.name}</span>
518 </Link>
519 <span style={{ display: 'grid' }}>N/A</span>
520 <span style={{ display: 'grid' }}>N/A</span>
521 <span>N/A</span>
522 <span> </span>
523 <span>N/A</span>
524 <span>N/A</span>
525 <span style={{ flexDirection: 'row-reverse' }}></span>
526 </button>
527 ) : (
528 <button className="profileboard-record" key={index}>
529 {record.scores.map((e, i) => (
530 <>
531 {i !== 0 ? (
532 <hr style={{ gridColumn: '1 / span 8' }} />
533 ) : (
534 ''
535 )}
536 <Link to={`/maps/${r.id}`}>
537 <span>{r.name}</span>
538 </Link>
539 <span style={{ display: 'grid' }}>
540 {record!.scores[i].score_count}
339 </span> 541 </span>
340 </>))} 542 <span style={{ display: 'grid' }}>
341 </button> 543 {record!.scores[i].score_count -
342 544 record!.map_wr_count >
343 ) 545 0
344 } else { return null } 546 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
345 }) : (<>{console.warn(maps)}</>)} 547 : `-`}
548 </span>
549 <span style={{ display: 'grid' }}>
550 {ticks_to_time(record!.scores[i].score_time)}
551 </span>
552 <span> </span>
553 {i === 0 ? (
554 <span>#{record!.placement}</span>
555 ) : (
556 <span> </span>
557 )}
558 <span>{record!.scores[i].date.split('T')[0]}</span>
559 <span style={{ flexDirection: 'row-reverse' }}>
560 <button
561 onClick={() => {
562 message(
563 'Demo Information',
564 `Demo ID: ${e.demo_id}`
565 );
566 }}
567 >
568 <img src={ThreedotIcon} alt="demo_id" />
569 </button>
570 <button
571 onClick={() => {
572 _delete_submission(r.id, e.record_id);
573 }}
574 >
575 <img src={DeleteIcon}></img>
576 </button>
577 <button
578 onClick={() =>
579 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
580 }
581 >
582 <img src={DownloadIcon} alt="download" />
583 </button>
584 {i === 0 && record!.scores.length > 1 ? (
585 <button
586 onClick={() => {
587 (
588 document.querySelectorAll(
589 '.profileboard-record'
590 )[index % 20] as HTMLInputElement
591 ).style.height === '44px' ||
592 (
593 document.querySelectorAll(
594 '.profileboard-record'
595 )[index % 20] as HTMLInputElement
596 ).style.height === ''
597 ? ((
598 document.querySelectorAll(
599 '.profileboard-record'
600 )[index % 20] as HTMLInputElement
601 ).style.height =
602 `${record!.scores.length * 46}px`)
603 : ((
604 document.querySelectorAll(
605 '.profileboard-record'
606 )[index % 20] as HTMLInputElement
607 ).style.height = '44px');
608 }}
609 >
610 <img src={HistoryIcon} alt="history" />
611 </button>
612 ) : (
613 ''
614 )}
615 </span>
616 </>
617 ))}
618 </button>
619 );
620 } else {
621 return null;
622 }
623 })
624 ) : (
625 <>{console.warn(maps)}</>
626 )}
346 </div> 627 </div>
347 </section> 628 </section>
348 </main> 629 </main>
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index 71aa427..885638d 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -1,147 +1,200 @@
1import React, { useEffect } from "react"; 1import React, { useEffect } from 'react';
2import { Helmet } from "react-helmet"; 2import { Helmet } from 'react-helmet';
3 3
4import RankingEntry from "@components/RankingEntry"; 4import RankingEntry from '@components/RankingEntry';
5import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; 5import {
6import { API } from "@api/Api"; 6 Ranking,
7 SteamRanking,
8 RankingType,
9 SteamRankingType,
10} from '@customTypes/Ranking';
11import { API } from '@api/Api';
7 12
8import "@css/Rankings.css"; 13import '@css/Rankings.css';
9 14
10const Rankings: React.FC = () => { 15const Rankings: React.FC = () => {
11 const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); 16 const [leaderboardData, setLeaderboardData] = React.useState<
12 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); 17 Ranking | SteamRanking
13 enum LeaderboardTypes { 18 >();
14 official, 19 const [currentLeaderboard, setCurrentLeaderboard] = React.useState<
15 unofficial 20 RankingType[] | SteamRankingType[]
21 >();
22 enum LeaderboardTypes {
23 official,
24 unofficial,
25 }
26 const [currentRankingType, setCurrentRankingType] =
27 React.useState<LeaderboardTypes>(LeaderboardTypes.official);
28
29 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false);
30
31 enum RankingCategories {
32 rankings_overall,
33 rankings_multiplayer,
34 rankings_singleplayer,
35 }
36 const [currentLeaderboardType, setCurrentLeaderboardType] =
37 React.useState<RankingCategories>(RankingCategories.rankings_singleplayer);
38 const [load, setLoad] = React.useState<boolean>(false);
39
40 const _fetch_rankings = async () => {
41 setLeaderboardLoad(false);
42 const rankings = await API.get_official_rankings();
43 setLeaderboardData(rankings);
44 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
45 setCurrentLeaderboard(rankings.rankings_singleplayer);
46 } else if (
47 currentLeaderboardType == RankingCategories.rankings_multiplayer
48 ) {
49 setCurrentLeaderboard(rankings.rankings_multiplayer);
50 } else {
51 setCurrentLeaderboard(rankings.rankings_overall);
16 } 52 }
17 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); 53 setLoad(true);
18 54 setLeaderboardLoad(true);
19 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); 55 };
20 56
21 enum RankingCategories { 57 const __dev_fetch_unofficial_rankings = async () => {
22 rankings_overall, 58 try {
23 rankings_multiplayer, 59 setLeaderboardLoad(false);
24 rankings_singleplayer 60 const rankings = await API.get_unofficial_rankings();
25 } 61 setLeaderboardData(rankings);
26 const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); 62 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
27 const [load, setLoad] = React.useState<boolean>(false); 63 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer))
28 64 setCurrentLeaderboard(rankings.rankings_singleplayer);
29 const _fetch_rankings = async () => { 65 } else if (
30 setLeaderboardLoad(false); 66 currentLeaderboardType == RankingCategories.rankings_multiplayer
31 const rankings = await API.get_official_rankings(); 67 ) {
32 setLeaderboardData(rankings); 68 setCurrentLeaderboard(rankings.rankings_multiplayer);
33 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 69 } else {
34 setCurrentLeaderboard(rankings.rankings_singleplayer) 70 setCurrentLeaderboard(rankings.rankings_overall);
35 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { 71 }
36 setCurrentLeaderboard(rankings.rankings_multiplayer) 72 setLeaderboardLoad(true);
37 } else { 73 } catch (e) {
38 setCurrentLeaderboard(rankings.rankings_overall) 74 console.log(e);
39 }
40 setLoad(true);
41 setLeaderboardLoad(true);
42 } 75 }
43 76 };
44 const __dev_fetch_unofficial_rankings = async () => { 77
45 try { 78 const _set_current_leaderboard = (ranking_cat: RankingCategories) => {
46 setLeaderboardLoad(false); 79 if (ranking_cat == RankingCategories.rankings_singleplayer) {
47 const rankings = await API.get_unofficial_rankings(); 80 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
48 setLeaderboardData(rankings); 81 } else if (ranking_cat == RankingCategories.rankings_multiplayer) {
49 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 82 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
50 // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) 83 } else {
51 setCurrentLeaderboard(rankings.rankings_singleplayer) 84 setCurrentLeaderboard(leaderboardData!.rankings_overall);
52 } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) {
53 setCurrentLeaderboard(rankings.rankings_multiplayer)
54 } else {
55 setCurrentLeaderboard(rankings.rankings_overall)
56 }
57 setLeaderboardLoad(true);
58 } catch (e) {
59 console.log(e)
60 }
61 } 85 }
62 86
63 const _set_current_leaderboard = (ranking_cat: RankingCategories) => { 87 setCurrentLeaderboardType(ranking_cat);
64 if (ranking_cat == RankingCategories.rankings_singleplayer) { 88 };
65 setCurrentLeaderboard(leaderboardData!.rankings_singleplayer);
66 } else if (ranking_cat == RankingCategories.rankings_multiplayer) {
67 setCurrentLeaderboard(leaderboardData!.rankings_multiplayer);
68 } else {
69 setCurrentLeaderboard(leaderboardData!.rankings_overall);
70 }
71 89
72 setCurrentLeaderboardType(ranking_cat); 90 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => {
91 if (leaderboard_type == LeaderboardTypes.official) {
92 _fetch_rankings();
93 } else {
73 } 94 }
95 };
74 96
75 const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { 97 useEffect(() => {
76 if (leaderboard_type == LeaderboardTypes.official) { 98 _fetch_rankings();
77 _fetch_rankings(); 99 if (load) {
78 } else { 100 _set_current_leaderboard(RankingCategories.rankings_singleplayer);
79
80 }
81 } 101 }
82 102 }, [load]);
83 useEffect(() => { 103
84 _fetch_rankings(); 104 return (
85 if (load) { 105 <main>
86 _set_current_leaderboard(RankingCategories.rankings_singleplayer); 106 <Helmet>
87 } 107 <title>LPHUB | Rankings</title>
88 }, [load]) 108 </Helmet>
89 109 <section className="nav-container nav-1">
90 return ( 110 <div>
91 <main> 111 <button
92 <Helmet> 112 onClick={() => {
93 <title>LPHUB | Rankings</title> 113 _fetch_rankings();
94 </Helmet> 114 setCurrentRankingType(LeaderboardTypes.official);
95 <section className="nav-container nav-1"> 115 }}
96 <div> 116 className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? 'selected' : ''}`}
97 <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> 117 >
98 <span>Official (LPHUB)</span> 118 <span>Official (LPHUB)</span>
99 </button> 119 </button>
100 <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> 120 <button
101 <span>Unofficial (Steam)</span> 121 onClick={() => {
102 </button> 122 __dev_fetch_unofficial_rankings();
103 </div> 123 setCurrentRankingType(LeaderboardTypes.unofficial);
104 </section> 124 }}
105 <section className="nav-container nav-2"> 125 className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? 'selected' : ''}`}
106 <div> 126 >
107 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> 127 <span>Unofficial (Steam)</span>
108 <span>Singleplayer</span> 128 </button>
109 </button> 129 </div>
110 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> 130 </section>
111 <span>Cooperative</span> 131 <section className="nav-container nav-2">
112 </button> 132 <div>
113 <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> 133 <button
114 <span>Overall</span> 134 onClick={() =>
115 </button> 135 _set_current_leaderboard(RankingCategories.rankings_singleplayer)
116 </div> 136 }
117 </section> 137 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? 'selected' : ''}`}
118 138 >
119 {load ? 139 <span>Singleplayer</span>
120 <section className="rankings-leaderboard"> 140 </button>
121 <div className="ranks-container"> 141 <button
122 <div className="leaderboard-entry header"> 142 onClick={() =>
123 <span>Rank</span> 143 _set_current_leaderboard(RankingCategories.rankings_multiplayer)
124 <span>Player</span> 144 }
125 <span>Portals</span> 145 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? 'selected' : ''}`}
126 </div> 146 >
127 147 <span>Cooperative</span>
128 <div className="splitter"></div> 148 </button>
129 149 <button
130 {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { 150 onClick={() =>
131 return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> 151 _set_current_leaderboard(RankingCategories.rankings_overall)
132 }) 152 }
133 } 153 className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? 'selected' : ''}`}
134 154 >
135 {leaderboardLoad ? null : 155 <span>Overall</span>
136 <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> 156 </button>
137 <span className="loader"></span> 157 </div>
138 </div> 158 </section>
139 } 159
140 </div> 160 {load ? (
141 </section> 161 <section className="rankings-leaderboard">
142 : null} 162 <div className="ranks-container">
143 </main> 163 <div className="leaderboard-entry header">
144 ) 164 <span>Rank</span>
145} 165 <span>Player</span>
166 <span>Portals</span>
167 </div>
168
169 <div className="splitter"></div>
170
171 {leaderboardLoad &&
172 currentLeaderboard?.map((curRankingData, i) => {
173 return (
174 <RankingEntry
175 currentLeaderboardType={currentLeaderboardType}
176 curRankingData={curRankingData}
177 key={i}
178 ></RankingEntry>
179 );
180 })}
181
182 {leaderboardLoad ? null : (
183 <div
184 style={{
185 display: 'flex',
186 justifyContent: 'center',
187 margin: '30px 0px',
188 }}
189 >
190 <span className="loader"></span>
191 </div>
192 )}
193 </div>
194 </section>
195 ) : null}
196 </main>
197 );
198};
146 199
147export default Rankings; 200export default Rankings;
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index 9f57b7e..7a774bc 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -5,37 +5,35 @@ import { Helmet } from 'react-helmet';
5import '@css/Rules.css'; 5import '@css/Rules.css';
6 6
7const Rules: React.FC = () => { 7const Rules: React.FC = () => {
8 const [rulesText, setRulesText] = React.useState<string>('');
8 9
9 const [rulesText, setRulesText] = React.useState<string>(""); 10 React.useEffect(() => {
11 const fetchRules = async () => {
12 try {
13 const response = await fetch(
14 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md'
15 );
16 if (!response.ok) {
17 throw new Error('Failed to fetch README');
18 }
19 const rulesText = await response.text();
20 setRulesText(rulesText);
21 } catch (error) {
22 console.error('Error fetching Rules:', error);
23 }
24 // setRulesText(rulesText)
25 };
26 fetchRules();
27 }, []);
10 28
11 React.useEffect(() => { 29 return (
12 const fetchRules = async () => { 30 <main>
13 try { 31 <Helmet>
14 const response = await fetch( 32 <title>LPHUB | Rules</title>
15 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' 33 </Helmet>
16 ); 34 <ReactMarkdown>{rulesText}</ReactMarkdown>
17 if (!response.ok) { 35 </main>
18 throw new Error('Failed to fetch README'); 36 );
19 }
20 const rulesText = await response.text();
21 setRulesText(rulesText);
22 } catch (error) {
23 console.error('Error fetching Rules:', error);
24 }
25 // setRulesText(rulesText)
26 };
27 fetchRules();
28 }, []);
29
30
31 return (
32 <main>
33 <Helmet>
34 <title>LPHUB | Rules</title>
35 </Helmet>
36 <ReactMarkdown>{rulesText}</ReactMarkdown>
37 </main>
38 );
39}; 37};
40 38
41export default Rules; 39export default Rules;
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index d43c0c6..b6dfbd3 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -2,13 +2,24 @@ import React from 'react';
2import { Link, useLocation, useNavigate } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from 'react-router-dom';
3import { Helmet } from 'react-helmet'; 3import { Helmet } from 'react-helmet';
4 4
5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; 5import {
6 SteamIcon,
7 TwitchIcon,
8 YouTubeIcon,
9 PortalIcon,
10 FlagIcon,
11 StatisticsIcon,
12 SortIcon,
13 ThreedotIcon,
14 DownloadIcon,
15 HistoryIcon,
16} from '@images/Images';
6import { UserProfile } from '@customTypes/Profile'; 17import { UserProfile } from '@customTypes/Profile';
7import { Game, GameChapters } from '@customTypes/Game'; 18import { Game, GameChapters } from '@customTypes/Game';
8import { Map } from '@customTypes/Map'; 19import { Map } from '@customTypes/Map';
9import { API } from '@api/Api'; 20import { API } from '@api/Api';
10import { ticks_to_time } from '@utils/Time'; 21import { ticks_to_time } from '@utils/Time';
11import "@css/Profile.css"; 22import '@css/Profile.css';
12import useMessage from '@hooks/UseMessage'; 23import useMessage from '@hooks/UseMessage';
13 24
14interface UserProps { 25interface UserProps {
@@ -18,7 +29,6 @@ interface UserProps {
18} 29}
19 30
20const User: React.FC<UserProps> = ({ token, profile, gameData }) => { 31const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
21
22 const { message, MessageDialogComponent } = useMessage(); 32 const { message, MessageDialogComponent } = useMessage();
23 33
24 const [user, setUser] = React.useState<UserProfile | undefined>(undefined); 34 const [user, setUser] = React.useState<UserProfile | undefined>(undefined);
@@ -27,18 +37,20 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
27 const [pageNumber, setPageNumber] = React.useState(1); 37 const [pageNumber, setPageNumber] = React.useState(1);
28 const [pageMax, setPageMax] = React.useState(0); 38 const [pageMax, setPageMax] = React.useState(0);
29 39
30 const [game, setGame] = React.useState("0"); 40 const [game, setGame] = React.useState('0');
31 const [chapter, setChapter] = React.useState("0"); 41 const [chapter, setChapter] = React.useState('0');
32 const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); 42 const [chapterData, setChapterData] = React.useState<GameChapters | null>(
43 null
44 );
33 const [maps, setMaps] = React.useState<Map[]>([]); 45 const [maps, setMaps] = React.useState<Map[]>([]);
34 46
35 const location = useLocation(); 47 const location = useLocation();
36 const navigate = useNavigate(); 48 const navigate = useNavigate();
37 49
38 const _fetch_user = async () => { 50 const _fetch_user = async () => {
39 const userID = location.pathname.split("/")[2]; 51 const userID = location.pathname.split('/')[2];
40 if (token && profile && profile.profile && profile.steam_id === userID) { 52 if (token && profile && profile.profile && profile.steam_id === userID) {
41 navigate("/profile"); 53 navigate('/profile');
42 return; 54 return;
43 } 55 }
44 const userData = await API.get_user(userID); 56 const userData = await API.get_user(userID);
@@ -46,7 +58,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
46 }; 58 };
47 59
48 const _get_game_chapters = async () => { 60 const _get_game_chapters = async () => {
49 if (game !== "0") { 61 if (game !== '0') {
50 const gameChapters = await API.get_games_chapters(game); 62 const gameChapters = await API.get_games_chapters(game);
51 setChapterData(gameChapters); 63 setChapterData(gameChapters);
52 } else { 64 } else {
@@ -56,7 +68,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
56 }; 68 };
57 69
58 const _get_game_maps = async () => { 70 const _get_game_maps = async () => {
59 if (chapter === "0") { 71 if (chapter === '0') {
60 const gameMaps = await API.get_game_maps(game); 72 const gameMaps = await API.get_game_maps(game);
61 setMaps(gameMaps); 73 setMaps(gameMaps);
62 setPageMax(Math.ceil(gameMaps.length / 20)); 74 setPageMax(Math.ceil(gameMaps.length / 20));
@@ -80,16 +92,14 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
80 }, [user, game, location]); 92 }, [user, game, location]);
81 93
82 React.useEffect(() => { 94 React.useEffect(() => {
83 if (user && game !== "0") { 95 if (user && game !== '0') {
84 _get_game_maps(); 96 _get_game_maps();
85 } 97 }
86 }, [user, game, chapter, location]) 98 }, [user, game, chapter, location]);
87 99
88 if (!user) { 100 if (!user) {
89 return ( 101 return <></>;
90 <></> 102 }
91 );
92 };
93 103
94 return ( 104 return (
95 <main> 105 <main>
@@ -98,218 +108,461 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
98 <meta name="description" content={user.user_name} /> 108 <meta name="description" content={user.user_name} />
99 </Helmet> 109 </Helmet>
100 {MessageDialogComponent} 110 {MessageDialogComponent}
101 <section id='section1' className='profile'> 111 <section id="section1" className="profile">
102 <div> 112 <div>
103 <img src={user.avatar_link} alt="profile-image"></img> 113 <img src={user.avatar_link} alt="profile-image"></img>
104 </div> 114 </div>
105 <div id='profile-top'> 115 <div id="profile-top">
106 <div> 116 <div>
107 <div>{user.user_name}</div> 117 <div>{user.user_name}</div>
108 <div> 118 <div>
109 {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} 119 {user.country_code === 'XX' ? (
120 ''
121 ) : (
122 <img
123 src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`}
124 alt={user.country_code}
125 />
126 )}
110 </div> 127 </div>
111 <div> 128 <div>
112 {user.titles.map(e => ( 129 {user.titles.map(e => (
113 <span className="titles" style={{ backgroundColor: `#${e.color}` }}> 130 <span
131 className="titles"
132 style={{ backgroundColor: `#${e.color}` }}
133 >
114 {e.name} 134 {e.name}
115 </span> 135 </span>
116 ))} 136 ))}
117 </div> 137 </div>
118 </div> 138 </div>
119 <div> 139 <div>
120 {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} 140 {user.links.steam === '-' ? (
121 {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} 141 ''
122 {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} 142 ) : (
123 {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} 143 <a href={user.links.steam}>
144 <img src={SteamIcon} alt="Steam" />
145 </a>
146 )}
147 {user.links.twitch === '-' ? (
148 ''
149 ) : (
150 <a href={user.links.twitch}>
151 <img src={TwitchIcon} alt="Twitch" />
152 </a>
153 )}
154 {user.links.youtube === '-' ? (
155 ''
156 ) : (
157 <a href={user.links.youtube}>
158 <img src={YouTubeIcon} alt="Youtube" />
159 </a>
160 )}
161 {user.links.p2sr === '-' ? (
162 ''
163 ) : (
164 <a href={user.links.p2sr}>
165 <img src={PortalIcon} alt="P2SR" style={{ padding: '0' }} />
166 </a>
167 )}
124 </div> 168 </div>
125
126 </div> 169 </div>
127 <div id='profile-bottom'> 170 <div id="profile-bottom">
128 <div> 171 <div>
129 <span>Overall</span> 172 <span>Overall</span>
130 <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} 173 <span>
131 <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> 174 {user.rankings.overall.rank === 0
175 ? 'N/A '
176 : '#' + user.rankings.overall.rank + ' '}
177 <span>
178 ({user.rankings.overall.completion_count}/
179 {user.rankings.overall.completion_total})
180 </span>
132 </span> 181 </span>
133 </div> 182 </div>
134 <div> 183 <div>
135 <span>Singleplayer</span> 184 <span>Singleplayer</span>
136 <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} 185 <span>
137 <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> 186 {user.rankings.singleplayer.rank === 0
187 ? 'N/A '
188 : '#' + user.rankings.singleplayer.rank + ' '}
189 <span>
190 ({user.rankings.singleplayer.completion_count}/
191 {user.rankings.singleplayer.completion_total})
192 </span>
138 </span> 193 </span>
139 </div> 194 </div>
140 <div> 195 <div>
141 <span>Cooperative</span> 196 <span>Cooperative</span>
142 <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} 197 <span>
143 <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> 198 {user.rankings.cooperative.rank === 0
199 ? 'N/A '
200 : '#' + user.rankings.cooperative.rank + ' '}
201 <span>
202 ({user.rankings.cooperative.completion_count}/
203 {user.rankings.cooperative.completion_total})
204 </span>
144 </span> 205 </span>
145 </div> 206 </div>
146 </div> 207 </div>
147 </section> 208 </section>
148 209
149 210 <section id="section2" className="profile">
150 <section id='section2' className='profile'> 211 <button onClick={() => setNavState(0)}>
151 <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" />&nbsp;Player Records</button> 212 <img src={FlagIcon} alt="" />
152 <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" />&nbsp;Statistics</button> 213 &nbsp;Player Records
214 </button>
215 <button onClick={() => setNavState(1)}>
216 <img src={StatisticsIcon} alt="" />
217 &nbsp;Statistics
218 </button>
153 </section> 219 </section>
154 220
155 221 <section id="section3" className="profile1">
156 222 <div id="profileboard-nav">
157 223 {gameData === null ? (
158 224 <select>error</select>
159 <section id='section3' className='profile1'> 225 ) : (
160 <div id='profileboard-nav'> 226 <select
161 {gameData === null ? <select>error</select> : 227 id="select-game"
162
163 <select id='select-game'
164 onChange={() => { 228 onChange={() => {
165 setGame((document.querySelector('#select-game') as HTMLInputElement).value); 229 setGame(
166 setChapter("0"); 230 (document.querySelector('#select-game') as HTMLInputElement)
167 const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; 231 .value
232 );
233 setChapter('0');
234 const chapterSelect = document.querySelector(
235 '#select-chapter'
236 ) as HTMLSelectElement;
168 if (chapterSelect) { 237 if (chapterSelect) {
169 chapterSelect.value = "0"; 238 chapterSelect.value = '0';
170 } 239 }
171 }}> 240 }}
172 <option value={0} key={0}>All Scores</option> 241 >
242 <option value={0} key={0}>
243 All Scores
244 </option>
173 {gameData.map((e, i) => ( 245 {gameData.map((e, i) => (
174 <option value={e.id} key={i + 1}>{e.name}</option> 246 <option value={e.id} key={i + 1}>
175 ))}</select> 247 {e.name}
176 } 248 </option>
249 ))}
250 </select>
251 )}
177 252
178 {game === "0" ? 253 {game === '0' ? (
179 <select disabled> 254 <select disabled>
180 <option>All Chapters</option> 255 <option>All Chapters</option>
181 </select> 256 </select>
182 : chapterData === null ? <select></select> : 257 ) : chapterData === null ? (
183 258 <select></select>
184 <select id='select-chapter' 259 ) : (
185 onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> 260 <select
186 <option value="0" key="0">All Chapters</option> 261 id="select-chapter"
187 {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( 262 onChange={() =>
188 <option value={e.id} key={i + 1}>{e.name}</option> 263 setChapter(
189 ))}</select> 264 (
190 } 265 document.querySelector(
266 '#select-chapter'
267 ) as HTMLInputElement
268 ).value
269 )
270 }
271 >
272 <option value="0" key="0">
273 All Chapters
274 </option>
275 {chapterData.chapters
276 .filter(e => e.is_disabled === false)
277 .map((e, i) => (
278 <option value={e.id} key={i + 1}>
279 {e.name}
280 </option>
281 ))}
282 </select>
283 )}
191 </div> 284 </div>
192 <div id='profileboard-top'> 285 <div id="profileboard-top">
193 <span><span>Map Name</span><img src={SortIcon} alt="" /></span> 286 <span>
194 <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> 287 <span>Map Name</span>
195 <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> 288 <img src={SortIcon} alt="" />
196 <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> 289 </span>
290 <span style={{ justifyContent: 'center' }}>
291 <span>Portals</span>
292 <img src={SortIcon} alt="" />
293 </span>
294 <span style={{ justifyContent: 'center' }}>
295 <span>WRΔ </span>
296 <img src={SortIcon} alt="" />
297 </span>
298 <span style={{ justifyContent: 'center' }}>
299 <span>Time</span>
300 <img src={SortIcon} alt="" />
301 </span>
197 <span> </span> 302 <span> </span>
198 <span><span>Rank</span><img src={SortIcon} alt="" /></span> 303 <span>
199 <span><span>Date</span><img src={SortIcon} alt="" /></span> 304 <span>Rank</span>
200 <div id='page-number'> 305 <img src={SortIcon} alt="" />
306 </span>
307 <span>
308 <span>Date</span>
309 <img src={SortIcon} alt="" />
310 </span>
311 <div id="page-number">
201 <div> 312 <div>
202 <button onClick={() => { 313 <button
203 if (pageNumber !== 1) { 314 onClick={() => {
204 setPageNumber(prevPageNumber => prevPageNumber - 1); 315 if (pageNumber !== 1) {
205 const records = document.querySelectorAll(".profileboard-record"); 316 setPageNumber(prevPageNumber => prevPageNumber - 1);
206 records.forEach((r) => { 317 const records = document.querySelectorAll(
207 (r as HTMLInputElement).style.height = "44px"; 318 '.profileboard-record'
208 }); 319 );
209 } 320 records.forEach(r => {
210 }} 321 (r as HTMLInputElement).style.height = '44px';
211 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 322 });
212 <span>{pageNumber}/{pageMax}</span> 323 }
213 <button onClick={() => { 324 }}
214 if (pageNumber !== pageMax) { 325 >
215 setPageNumber(prevPageNumber => prevPageNumber + 1); 326 <i
216 const records = document.querySelectorAll(".profileboard-record"); 327 className="triangle"
217 records.forEach((r) => { 328 style={{ position: 'relative', left: '-5px' }}
218 (r as HTMLInputElement).style.height = "44px"; 329 ></i>{' '}
219 }); 330 </button>
220 } 331 <span>
221 }} 332 {pageNumber}/{pageMax}
222 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 333 </span>
334 <button
335 onClick={() => {
336 if (pageNumber !== pageMax) {
337 setPageNumber(prevPageNumber => prevPageNumber + 1);
338 const records = document.querySelectorAll(
339 '.profileboard-record'
340 );
341 records.forEach(r => {
342 (r as HTMLInputElement).style.height = '44px';
343 });
344 }
345 }}
346 >
347 <i
348 className="triangle"
349 style={{
350 position: 'relative',
351 left: '5px',
352 transform: 'rotate(180deg)',
353 }}
354 ></i>{' '}
355 </button>
223 </div> 356 </div>
224 </div> 357 </div>
225 </div> 358 </div>
226 <hr /> 359 <hr />
227 <div id='profileboard-records'> 360 <div id="profileboard-records">
228 361 {game === '0' ? (
229 {game === "0" 362 user.records
230 ? ( 363 .sort((a, b) => a.map_id - b.map_id)
231 364 .map((r, index) =>
232 user.records.sort((a, b) => a.map_id - b.map_id) 365 Math.ceil((index + 1) / 20) === pageNumber ? (
233 .map((r, index) => ( 366 <button className="profileboard-record" key={index}>
234 367 {r.scores.map((e, i) => (
235 Math.ceil((index + 1) / 20) === pageNumber ? ( 368 <>
236 <button className="profileboard-record" key={index}> 369 {i !== 0 ? (
237 {r.scores.map((e, i) => (<> 370 <hr style={{ gridColumn: '1 / span 8' }} />
238 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 371 ) : (
239 372 ''
240 <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> 373 )}
241 374
242 <span style={{ display: "grid" }}>{e.score_count}</span> 375 <Link to={`/maps/${r.map_id}`}>
243 376 <span>{r.map_name}</span>
244 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> 377 </Link>
245 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 378
379 <span style={{ display: 'grid' }}>{e.score_count}</span>
380
381 <span style={{ display: 'grid' }}>
382 {e.score_count - r.map_wr_count > 0
383 ? `+${e.score_count - r.map_wr_count}`
384 : `-`}
385 </span>
386 <span style={{ display: 'grid' }}>
387 {ticks_to_time(e.score_time)}
388 </span>
246 <span> </span> 389 <span> </span>
247 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 390 {i === 0 ? <span>#{r.placement}</span> : <span> </span>}
248 <span>{e.date.split("T")[0]}</span> 391 <span>{e.date.split('T')[0]}</span>
249 <span style={{ flexDirection: "row-reverse" }}> 392 <span style={{ flexDirection: 'row-reverse' }}>
250 393 <button
251 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 394 onClick={() => {
252 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 395 message(
253 {i === 0 && r.scores.length > 1 ? <button onClick={() => { 396 'Demo Information',
254 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 397 `Demo ID: ${e.demo_id}`
255 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 398 );
256 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : 399 }}
257 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 400 >
258 } 401 <img src={ThreedotIcon} alt="demo_id" />
259 }><img src={HistoryIcon} alt="history" /></button> : ""} 402 </button>
260 403 <button
404 onClick={() =>
405 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
406 }
407 >
408 <img src={DownloadIcon} alt="download" />
409 </button>
410 {i === 0 && r.scores.length > 1 ? (
411 <button
412 onClick={() => {
413 (
414 document.querySelectorAll(
415 '.profileboard-record'
416 )[index % 20] as HTMLInputElement
417 ).style.height === '44px' ||
418 (
419 document.querySelectorAll(
420 '.profileboard-record'
421 )[index % 20] as HTMLInputElement
422 ).style.height === ''
423 ? ((
424 document.querySelectorAll(
425 '.profileboard-record'
426 )[index % 20] as HTMLInputElement
427 ).style.height =
428 `${r.scores.length * 46}px`)
429 : ((
430 document.querySelectorAll(
431 '.profileboard-record'
432 )[index % 20] as HTMLInputElement
433 ).style.height = '44px');
434 }}
435 >
436 <img src={HistoryIcon} alt="history" />
437 </button>
438 ) : (
439 ''
440 )}
261 </span> 441 </span>
262 </>))} 442 </>
263 443 ))}
444 </button>
445 ) : (
446 ''
447 )
448 )
449 ) : maps ? (
450 maps
451 .filter(e => e.is_disabled === false)
452 .sort((a, b) => a.id - b.id)
453 .map((r, index) => {
454 if (Math.ceil((index + 1) / 20) === pageNumber) {
455 let record = user.records.find(e => e.map_id === r.id);
456 return record === undefined ? (
457 <button
458 className="profileboard-record"
459 key={index}
460 style={{ backgroundColor: '#1b1b20' }}
461 >
462 <Link to={`/maps/${r.id}`}>
463 <span>{r.name}</span>
464 </Link>
465 <span style={{ display: 'grid' }}>N/A</span>
466 <span style={{ display: 'grid' }}>N/A</span>
467 <span>N/A</span>
468 <span> </span>
469 <span>N/A</span>
470 <span>N/A</span>
471 <span style={{ flexDirection: 'row-reverse' }}></span>
264 </button> 472 </button>
265 ) : "" 473 ) : (
266 ))) : maps ? 474 <button className="profileboard-record" key={index}>
267 475 {record.scores.map((e, i) => (
268 maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) 476 <>
269 .map((r, index) => { 477 {i !== 0 ? (
270 if (Math.ceil((index + 1) / 20) === pageNumber) { 478 <hr style={{ gridColumn: '1 / span 8' }} />
271 let record = user.records.find((e) => e.map_id === r.id); 479 ) : (
272 return record === undefined ? ( 480 ''
273 <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> 481 )}
274 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 482 <Link to={`/maps/${r.id}`}>
275 <span style={{ display: "grid" }}>N/A</span> 483 <span>{r.name}</span>
276 <span style={{ display: "grid" }}>N/A</span> 484 </Link>
277 <span>N/A</span> 485 <span style={{ display: 'grid' }}>
278 <span> </span> 486 {record!.scores[i].score_count}
279 <span>N/A</span> 487 </span>
280 <span>N/A</span> 488 <span style={{ display: 'grid' }}>
281 <span style={{ flexDirection: "row-reverse" }}></span> 489 {record!.scores[i].score_count -
282 </button> 490 record!.map_wr_count >
283 ) : ( 491 0
284 <button className="profileboard-record" key={index}> 492 ? `+${record!.scores[i].score_count - record!.map_wr_count}`
285 {record.scores.map((e, i) => (<> 493 : `-`}
286 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 494 </span>
287 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 495 <span style={{ display: 'grid' }}>
288 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 496 {ticks_to_time(record!.scores[i].score_time)}
289 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> 497 </span>
290 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
291 <span> </span> 498 <span> </span>
292 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 499 {i === 0 ? (
293 <span>{record!.scores[i].date.split("T")[0]}</span> 500 <span>#{record!.placement}</span>
294 <span style={{ flexDirection: "row-reverse" }}> 501 ) : (
295 502 <span> </span>
296 <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 503 )}
297 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 504 <span>{record!.scores[i].date.split('T')[0]}</span>
298 {i === 0 && record!.scores.length > 1 ? <button onClick={() => { 505 <span style={{ flexDirection: 'row-reverse' }}>
299 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || 506 <button
300 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? 507 onClick={() => {
301 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : 508 message(
302 (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" 509 'Demo Information',
303 } 510 `Demo ID: ${e.demo_id}`
304 }><img src={HistoryIcon} alt="history" /></button> : ""} 511 );
305 512 }}
513 >
514 <img src={ThreedotIcon} alt="demo_id" />
515 </button>
516 <button
517 onClick={() =>
518 (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`)
519 }
520 >
521 <img src={DownloadIcon} alt="download" />
522 </button>
523 {i === 0 && record!.scores.length > 1 ? (
524 <button
525 onClick={() => {
526 (
527 document.querySelectorAll(
528 '.profileboard-record'
529 )[index % 20] as HTMLInputElement
530 ).style.height === '44px' ||
531 (
532 document.querySelectorAll(
533 '.profileboard-record'
534 )[index % 20] as HTMLInputElement
535 ).style.height === ''
536 ? ((
537 document.querySelectorAll(
538 '.profileboard-record'
539 )[index % 20] as HTMLInputElement
540 ).style.height =
541 `${record!.scores.length * 46}px`)
542 : ((
543 document.querySelectorAll(
544 '.profileboard-record'
545 )[index % 20] as HTMLInputElement
546 ).style.height = '44px');
547 }}
548 >
549 <img src={HistoryIcon} alt="history" />
550 </button>
551 ) : (
552 ''
553 )}
306 </span> 554 </span>
307 </>))} 555 </>
308 </button> 556 ))}
309 557 </button>
310 ) 558 );
311 } else { return null } 559 } else {
312 }) : (<>{console.warn(maps)}</>)} 560 return null;
561 }
562 })
563 ) : (
564 <>{console.warn(maps)}</>
565 )}
313 </div> 566 </div>
314 </section> 567 </section>
315 </main> 568 </main>
diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts
index 8265915..9bf083a 100644
--- a/frontend/src/react-app-env.d.ts
+++ b/frontend/src/react-app-env.d.ts
@@ -1,2 +1,2 @@
1declare module "*.png"; 1declare module '*.png';
2declare module "*.css"; 2declare module '*.css';
diff --git a/frontend/src/types/Chapters.ts b/frontend/src/types/Chapters.ts
index 1d48306..8924b97 100644
--- a/frontend/src/types/Chapters.ts
+++ b/frontend/src/types/Chapters.ts
@@ -1,19 +1,19 @@
1import type { Game } from "@customTypes/Game"; 1import 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..be2cd73 100644
--- a/frontend/src/types/Game.ts
+++ b/frontend/src/types/Game.ts
@@ -1,37 +1,36 @@
1import type { Map } from '@customTypes/Map'; 1import type { Map } from '@customTypes/Map';
2 2
3
4export interface Game { 3export interface Game {
5 id: number; 4 id: number;
6 name: string; 5 name: string;
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 89c66d5..6bc6369 100644
--- a/frontend/src/types/Map.ts
+++ b/frontend/src/types/Map.ts
@@ -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,22 +27,24 @@ 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';
46 placement: number; 48 placement: number;
47 record_id: number; 49 record_id: number;
48 score_count: number; 50 score_count: number;
@@ -50,10 +52,10 @@ 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';
57 placement: number; 59 placement: number;
58 record_id: number; 60 record_id: number;
59 score_count: number; 61 score_count: number;
@@ -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;
@@ -79,11 +80,11 @@ interface MapSummaryMap {
79 map_name: string; 80 map_name: string;
80 is_coop: boolean; 81 is_coop: boolean;
81 is_disabled: boolean; 82 is_disabled: boolean;
82}; 83}
83 84
84interface MapSummaryDetails { 85interface MapSummaryDetails {
85 routes: MapSummaryDetailsRoute[]; 86 routes: MapSummaryDetailsRoute[];
86}; 87}
87 88
88interface MapSummaryDetailsRoute { 89interface MapSummaryDetailsRoute {
89 route_id: number; 90 route_id: number;
@@ -93,16 +94,15 @@ interface MapSummaryDetailsRoute {
93 completion_count: number; 94 completion_count: number;
94 description: string; 95 description: string;
95 showcase: string; 96 showcase: string;
96}; 97}
97 98
98interface MapSummaryDetailsRouteHistory { 99interface MapSummaryDetailsRouteHistory {
99 runner_name: string; 100 runner_name: string;
100 score_count: number; 101 score_count: number;
101 date: string; 102 date: string;
102}; 103}
103 104
104export interface MapDeleteEndpoint { 105export interface MapDeleteEndpoint {
105 map_id: number; 106 map_id: number;
106 record_id: number; 107 record_id: number;
107} 108}
108
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..8051ae5 100644
--- a/frontend/src/types/Profile.ts
+++ b/frontend/src/types/Profile.ts
@@ -1,10 +1,10 @@
1import type { Pagination } from "@customTypes/Pagination"; 1import type { Pagination } from '@customTypes/Pagination';
2 2
3export interface UserShort { 3export 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..06a5ca4 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..48b9169 100644
--- a/frontend/src/types/Search.ts
+++ b/frontend/src/types/Search.ts
@@ -1,13 +1,13 @@
1import type { UserShort } from "@customTypes/Profile"; 1import type { UserShort } from '@customTypes/Profile';
2 2
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..affcd36 100644
--- a/frontend/src/utils/Jwt.ts
+++ b/frontend/src/utils/Jwt.ts
@@ -1,5 +1,7 @@
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 }
@@ -19,9 +21,11 @@ export function get_user_id_from_token(token: string | undefined): string | unde
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 }
@@ -41,4 +45,4 @@ export function get_user_mod_from_token(token: string | undefined): boolean | un
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..34830b0 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}