aboutsummaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2025-07-24 14:40:22 +0300
committerArda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>2025-07-24 14:40:22 +0300
commitb0d199936b546c75d4b19d99591237f0bf97fe55 (patch)
treee9391880e7db2bd1ea8ff25d91aeea8dd98f186e /frontend/src
parentfix/frontend: fixed sidebar title size, removed unnecessary imports (diff)
parentfeat/backend: add newrelic integration (#274) (diff)
downloadlphub-b0d199936b546c75d4b19d99591237f0bf97fe55.tar.gz
lphub-b0d199936b546c75d4b19d99591237f0bf97fe55.tar.bz2
lphub-b0d199936b546c75d4b19d99591237f0bf97fe55.zip
Merge branch 'main' into css-overhaulcss-overhaul
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/App.tsx11
-rw-r--r--frontend/src/api/Api.ts4
-rw-r--r--frontend/src/api/Maps.ts14
-rw-r--r--frontend/src/components/Leaderboards.tsx21
-rw-r--r--frontend/src/components/Summary.tsx36
-rw-r--r--frontend/src/components/UploadRunDialog.tsx106
-rw-r--r--frontend/src/pages/About.tsx4
-rw-r--r--frontend/src/pages/Games.tsx4
-rw-r--r--frontend/src/pages/Homepage.tsx6
-rw-r--r--frontend/src/pages/Maplist.tsx50
-rw-r--r--frontend/src/pages/Maps.tsx41
-rw-r--r--frontend/src/pages/Profile.tsx9
-rw-r--r--frontend/src/pages/Rankings.tsx30
-rw-r--r--frontend/src/pages/Rules.tsx4
-rw-r--r--frontend/src/pages/User.tsx9
-rw-r--r--frontend/src/types/Content.ts1
-rw-r--r--frontend/src/types/Map.ts1
-rw-r--r--frontend/src/types/MapNames.ts127
18 files changed, 330 insertions, 148 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e4bde75..bdd3adc 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,6 @@
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";
3 4
4import { UserProfile } from '@customTypes/Profile'; 5import { UserProfile } from '@customTypes/Profile';
5import Sidebar from './components/Sidebar'; 6import Sidebar from './components/Sidebar';
@@ -66,14 +67,12 @@ const App: React.FC = () => {
66 _fetch_games(); 67 _fetch_games();
67 }, []); 68 }, []);
68 69
69 if (!games) {
70 return (
71 <></>
72 )
73 };
74
75 return ( 70 return (
76 <> 71 <>
72 <Helmet>
73 <title>LPHUB</title>
74 <meta name="description" content="Least Portals Hub" />
75 </Helmet>
77 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { 76 <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => {
78 setUploadRunDialog(false); 77 setUploadRunDialog(false);
79 if (updateProfile) { 78 if (updateProfile) {
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts
index 2e55ab4..862e688 100644
--- a/frontend/src/api/Api.ts
+++ b/frontend/src/api/Api.ts
@@ -29,13 +29,13 @@ export const API = {
29 get_unofficial_rankings: () => get_unofficial_rankings(), 29 get_unofficial_rankings: () => get_unofficial_rankings(),
30 // Maps 30 // Maps
31 get_map_summary: (map_id: string) => get_map_summary(map_id), 31 get_map_summary: (map_id: string) => get_map_summary(map_id),
32 get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), 32 get_map_leaderboard: (map_id: string, page: string) => get_map_leaderboard(map_id, page),
33 get_map_discussions: (map_id: string) => get_map_discussions(map_id), 33 get_map_discussions: (map_id: string) => get_map_discussions(map_id),
34 get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), 34 get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id),
35 35
36 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), 36 post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content),
37 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), 37 post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment),
38 post_record: (token: string, run: UploadRunContent) => post_record(token, run), 38 post_record: (token: string, run: UploadRunContent, map_id: number) => post_record(token, run, map_id),
39 39
40 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), 40 delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id),
41 41
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts
index 89657b5..aa967ce 100644
--- a/frontend/src/api/Maps.ts
+++ b/frontend/src/api/Maps.ts
@@ -8,8 +8,8 @@ export const get_map_summary = async (map_id: string): Promise<MapSummary> => {
8 return response.data.data; 8 return response.data.data;
9}; 9};
10 10
11export const get_map_leaderboard = async (map_id: string): Promise<MapLeaderboard | undefined> => { 11export const get_map_leaderboard = async (map_id: string, page: string): Promise<MapLeaderboard | undefined> => {
12 const response = await axios.get(url(`maps/${map_id}/leaderboards`)); 12 const response = await axios.get(url(`maps/${map_id}/leaderboards?page=${page}`));
13 if (!response.data.success) { 13 if (!response.data.success) {
14 return undefined; 14 return undefined;
15 } 15 }
@@ -73,9 +73,9 @@ export const delete_map_discussion = async (token: string, map_id: string, discu
73 return response.data.success; 73 return response.data.success;
74}; 74};
75 75
76export const post_record = async (token: string, run: UploadRunContent): Promise<[boolean, string]> => { 76export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => {
77 if (run.partner_demo) { 77 if (run.partner_demo) {
78 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 78 const response = await axios.postForm(url(`maps/${map_id}/record`), {
79 "host_demo": run.host_demo, 79 "host_demo": run.host_demo,
80 "partner_demo": run.partner_demo, 80 "partner_demo": run.partner_demo,
81 }, { 81 }, {
@@ -83,16 +83,16 @@ export const post_record = async (token: string, run: UploadRunContent): Promise
83 "Authorization": token, 83 "Authorization": token,
84 } 84 }
85 }); 85 });
86 return [ response.data.success, response.data.message ]; 86 return [response.data.success, response.data.message];
87 } else { 87 } else {
88 const response = await axios.postForm(url(`maps/${run.map_id}/record`), { 88 const response = await axios.postForm(url(`maps/${map_id}/record`), {
89 "host_demo": run.host_demo, 89 "host_demo": run.host_demo,
90 }, { 90 }, {
91 headers: { 91 headers: {
92 "Authorization": token, 92 "Authorization": token,
93 } 93 }
94 }); 94 });
95 return [ response.data.success, response.data.message ]; 95 return [response.data.success, response.data.message];
96 } 96 }
97} 97}
98 98
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index 4a8b463..fb614fa 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -1,20 +1,33 @@
1import React from 'react'; 1import React from 'react';
2import { Link } 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 useMessage from "@hooks/UseMessage"; 8import useMessage from "@hooks/UseMessage";
8import "@css/Maps.css" 9import "@css/Maps.css"
9 10
10interface LeaderboardsProps { 11interface LeaderboardsProps {
11 data?: MapLeaderboard; 12 mapID: string;
12} 13}
13 14
14const Leaderboards: React.FC<LeaderboardsProps> = ({ data }) => { 15const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
16 const navigate = useNavigate();
17 const [data, setData] = React.useState<MapLeaderboard | undefined>(undefined);
18 const [pageNumber, setPageNumber] = React.useState<number>(1);
19
20 const _fetch_map_leaderboards = async () => {
21 const mapLeaderboards = await API.get_map_leaderboard(mapID, pageNumber.toString());
22 setData(mapLeaderboards);
23 };
15 24
16 const { message, MessageDialogComponent } = useMessage(); 25 const { message, MessageDialogComponent } = useMessage();
17 const [pageNumber, setPageNumber] = React.useState<number>(1); 26
27 React.useEffect(() => {
28 _fetch_map_leaderboards();
29 console.log(data);
30 }, [pageNumber, navigate])
18 31
19 if (!data) { 32 if (!data) {
20 return ( 33 return (
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 4bcaa6a..7da2f1e 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -140,20 +140,34 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data })
140 <section id='section4' className='summary1'> 140 <section id='section4' className='summary1'>
141 <div id='difficulty'> 141 <div id='difficulty'>
142 <span>Difficulty</span> 142 <span>Difficulty</span>
143 {data.summary.routes[selectedRun].rating === 0 && (<span>N/A</span>)} 143 {data.map.difficulty <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)}
144 {data.summary.routes[selectedRun].rating === 1 && (<span style={{ color: "lime" }}>Very easy</span>)} 144 {data.map.difficulty > 2 && data.map.difficulty <= 4 && (<span style={{ color: "green" }}>Easy</span>)}
145 {data.summary.routes[selectedRun].rating === 2 && (<span style={{ color: "green" }}>Easy</span>)} 145 {data.map.difficulty > 4 && data.map.difficulty <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)}
146 {data.summary.routes[selectedRun].rating === 3 && (<span style={{ color: "yellow" }}>Medium</span>)} 146 {data.map.difficulty > 6 && data.map.difficulty <= 8 && (<span style={{ color: "orange" }}>Hard</span>)}
147 {data.summary.routes[selectedRun].rating === 4 && (<span style={{ color: "orange" }}>Hard</span>)} 147 {data.map.difficulty > 8 && data.map.difficulty <= 10 && (<span style={{ color: "red" }}>Very hard</span>)}
148 {data.summary.routes[selectedRun].rating === 5 && (<span style={{ color: "red" }}>Very hard</span>)}
149 <div> 148 <div>
150 {data.summary.routes[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} 149 {data.map.difficulty <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)}
151 {data.summary.routes[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} 150 {data.map.difficulty > 2 && data.map.difficulty <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)}
152 {data.summary.routes[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} 151 {data.map.difficulty > 4 && data.map.difficulty <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)}
153 {data.summary.routes[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} 152 {data.map.difficulty > 6 && data.map.difficulty <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)}
154 {data.summary.routes[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} 153 {data.map.difficulty > 8 && data.map.difficulty <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)}
155 </div> 154 </div>
156 </div> 155 </div>
156 {/* <div id='difficulty'>
157 <span>Difficulty</span>
158 {data.summary.routes[selectedRun].rating <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)}
159 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 && (<span style={{ color: "green" }}>Easy</span>)}
160 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)}
161 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 && (<span style={{ color: "orange" }}>Hard</span>)}
162 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 && (<span style={{ color: "red" }}>Very hard</span>)}
163 <div>
164 {data.summary.routes[selectedRun].rating <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)}
165 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)}
166 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)}
167 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)}
168 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)}
169 </div>
170 </div> */}
157 <div id='count'> 171 <div id='count'>
158 <span>Completion Count</span> 172 <span>Completion Count</span>
159 <div>{data.summary.routes[selectedRun].completion_count}</div> 173 <div>{data.summary.routes[selectedRun].completion_count}</div>
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index 951944b..971a747 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -5,12 +5,12 @@ import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp';
5import btn from "@css/Button.module.css"; 5import btn from "@css/Button.module.css";
6import '@css/UploadRunDialog.css'; 6import '@css/UploadRunDialog.css';
7import { Game } from '@customTypes/Game'; 7import { Game } from '@customTypes/Game';
8import { Map } from '@customTypes/Map';
9import { API } from '@api/Api'; 8import { API } from '@api/Api';
10import { useNavigate } from 'react-router-dom'; 9import { useNavigate } from 'react-router-dom';
11import useMessage from '@hooks/UseMessage'; 10import useMessage from '@hooks/UseMessage';
12import useConfirm from '@hooks/UseConfirm'; 11import useConfirm from '@hooks/UseConfirm';
13import useMessageLoad from "@hooks/UseMessageLoad"; 12import useMessageLoad from "@hooks/UseMessageLoad";
13import { MapNames } from '@customTypes/MapNames';
14 14
15interface UploadRunDialogProps { 15interface UploadRunDialogProps {
16 token?: string; 16 token?: string;
@@ -28,19 +28,11 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
28 const navigate = useNavigate(); 28 const navigate = useNavigate();
29 29
30 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 30 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({
31 map_id: 0,
32 host_demo: null, 31 host_demo: null,
33 partner_demo: null, 32 partner_demo: null,
34 }); 33 });
35 34
36 const [currentMap, setCurrentMap] = React.useState<string>("");
37
38 const _set_current_map = (game_name: string) => {
39 setCurrentMap(game_name);
40 }
41
42 const [selectedGameID, setSelectedGameID] = React.useState<number>(0); 35 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
43 const [selectedGameMaps, setSelectedGameMaps] = React.useState<Map[]>([]);
44 const [selectedGameName, setSelectedGameName] = React.useState<string>(""); 36 const [selectedGameName, setSelectedGameName] = React.useState<string>("");
45 37
46 // dropdowns 38 // dropdowns
@@ -51,6 +43,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
51 43
52 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); 44 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false);
53 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); 45 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false);
46
54 const fileInputRef = React.useRef<HTMLInputElement>(null); 47 const fileInputRef = React.useRef<HTMLInputElement>(null);
55 const fileInputRefPartner = React.useRef<HTMLInputElement>(null); 48 const fileInputRefPartner = React.useRef<HTMLInputElement>(null);
56 49
@@ -103,14 +96,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
103 96
104 const _handle_game_select = async (game_id: string, game_name: string) => { 97 const _handle_game_select = async (game_id: string, game_name: string) => {
105 setLoading(true); 98 setLoading(true);
106 const gameMaps = await API.get_game_maps(game_id);
107 setSelectedGameMaps(gameMaps);
108 setUploadRunContent({
109 map_id: gameMaps.find((map) => !map.is_disabled)!.id, //gameMaps[0].id,
110 host_demo: null,
111 partner_demo: null,
112 });
113 _set_current_map(gameMaps.find((map) => !map.is_disabled)!.name);
114 setSelectedGameID(parseInt(game_id) - 1); 99 setSelectedGameID(parseInt(game_id) - 1);
115 setSelectedGameName(game_name); 100 setSelectedGameName(game_name);
116 setLoading(false); 101 setLoading(false);
@@ -159,6 +144,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
159 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.") 144 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.")
160 return 145 return
161 } 146 }
147
148 if (!demo.mapName || !MapNames[demo.mapName]) {
149 await message("Error", "Error while processing demo: Invalid map name.")
150 return
151 }
152
153 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) {
154 await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.")
155 return
156 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) {
157 await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.")
158 return
159 }
160
162 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 161 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
163 162
164 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?`); 163 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?`);
@@ -168,10 +167,14 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
168 } 167 }
169 168
170 messageLoad("Uploading..."); 169 messageLoad("Uploading...");
171 const [success, response] = await API.post_record(token, uploadRunContent); 170 const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]);
172 messageLoadClose(); 171 messageLoadClose();
173 await message("Upload Record", response); 172 await message("Upload Record", response);
174 if (success) { 173 if (success) {
174 setUploadRunContent({
175 host_demo: null,
176 partner_demo: null,
177 });
175 onClose(success); 178 onClose(success);
176 navigate("/profile"); 179 navigate("/profile");
177 } 180 }
@@ -180,7 +183,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
180 183
181 React.useEffect(() => { 184 React.useEffect(() => {
182 if (open) { 185 if (open) {
183
184 setDragHighlightPartner(false); 186 setDragHighlightPartner(false);
185 setDragHighlight(false); 187 setDragHighlight(false);
186 _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. 188 _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?.
@@ -204,37 +206,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
204 <div className='dropdown-cur'>{selectedGameName}</div> 206 <div className='dropdown-cur'>{selectedGameName}</div>
205 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> 207 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i>
206 </div> 208 </div>
207 <div style={{top: "110px"}} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> 209 <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}>
208 {games.map((game) => ( 210 {games.map((game) => (
209 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> 211 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div>
210 ))} 212 ))}
211 </div> 213 </div>
212 {!loading && ( 214 </div>
213 <>
214 <div style={{ padding: "25px 0px" }}>
215 <h3 style={{ margin: "0px 0px" }}>Select Map</h3>
216 <div onClick={() => _handle_dropdowns(2)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}>
217 <span style={{ userSelect: "none" }}>{currentMap}</span>
218 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i>
219 </div>
220 </div>
221 <div style={{top: "220px"}} id='dropdown2' className={dropdown2Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}>
222 {selectedGameMaps && selectedGameMaps.filter(gameMap => !gameMap.is_disabled).map((gameMap) => (
223 <div onClick={() => { setUploadRunContent({ ...uploadRunContent, map_id: gameMap.id }); _set_current_map(gameMap.name); _handle_dropdowns(2); }} key={gameMap.id}>{gameMap.name}</div>
224 ))}
225 </div>
226 </>
227
228 )}
229 </div>
230 215
231 { 216 {
232 !loading && 217 !loading &&
233 ( 218 (
234 <> 219 <>
235 220
236 <div> 221 <div>
237 <h3 style={{margin: "10px 0px"}}>Host Demo</h3> 222 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3>
238 <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" : ""}`}> 223 <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" : ""}`}>
239 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> 224 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} />
240 {!uploadRunContent.host_demo ? 225 {!uploadRunContent.host_demo ?
@@ -253,38 +238,41 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
253 games[selectedGameID].is_coop && 238 games[selectedGameID].is_coop &&
254 ( 239 (
255 <> 240 <>
256 <div> 241 <div>
257 <h3 style={{margin: "10px 0px"}}>Partner Demo</h3> 242 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
258 <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 <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" : ""}`}>
259 <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 <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 ?
260 <div>
261 <span>Drag and drop</span>
262 <div> 245 <div>
263 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 246 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
264 <button className={btn.default}>Upload</button> 247 <button className={btn.default}>Upload</button>
265 </div> 248 </div>
266 </div> 249 : null}
267 : null}
268 250
269 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> 251 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span>
252 </div>
270 </div> 253 </div>
271 </div>
272 </> 254 </>
273 ) 255 )
274 } 256 }
275 </div> 257 </div>
276 <div className='search-container'> 258 <div className='search-container'>
259
260 </div>
277 261
278 </div>
279
280 </> 262 </>
281 ) 263 )
282 } 264 }
283 </div> 265 </div>
284 <div className='upload-run-buttons-container'> 266 <div className='upload-run-buttons-container'>
285 <button className={`${btn.defaultWide}`} onClick={_upload_run}>Submit</button> 267 <button className={`${btn.defaultWide}`} onClick={_upload_run}>Submit</button>
286 <button className={`${btn.defaultWide}`} onClick={() => onClose(false)}>Cancel</button> 268 <button className={`${btn.defaultWide}`} onClick={() => {
287 </div> 269 onClose(false);
270 setUploadRunContent({
271 host_demo: null,
272 partner_demo: null,
273 });
274 }}>Cancel</button>
275 </div>
288 </div> 276 </div>
289 </div> 277 </div>
290 </> 278 </>
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx
index fe2e25a..b7bd534 100644
--- a/frontend/src/pages/About.tsx
+++ b/frontend/src/pages/About.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from 'react-markdown';
3import { Helmet } from 'react-helmet';
3 4
4import '@css/About.css'; 5import '@css/About.css';
5 6
@@ -28,6 +29,9 @@ const About: React.FC = () => {
28 29
29 return ( 30 return (
30 <main> 31 <main>
32 <Helmet>
33 <title>LPHUB | About</title>
34 </Helmet>
31 <ReactMarkdown>{aboutText}</ReactMarkdown> 35 <ReactMarkdown>{aboutText}</ReactMarkdown>
32 </main> 36 </main>
33 ); 37 );
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx
index e0320af..5e0d5bf 100644
--- a/frontend/src/pages/Games.tsx
+++ b/frontend/src/pages/Games.tsx
@@ -1,4 +1,5 @@
1import React from 'react'; 1import React from 'react';
2import { Helmet } from 'react-helmet';
2 3
3import GameEntry from '@components/GameEntry'; 4import GameEntry from '@components/GameEntry';
4import { Game } from '@customTypes/Game'; 5import { Game } from '@customTypes/Game';
@@ -11,6 +12,9 @@ interface GamesProps {
11const Games: React.FC<GamesProps> = ({ games }) => { 12const Games: React.FC<GamesProps> = ({ games }) => {
12 return ( 13 return (
13 <main> 14 <main>
15 <Helmet>
16 <title>LPHUB | Games</title>
17 </Helmet>
14 <section> 18 <section>
15 <div className={gamesCSS.content}> 19 <div className={gamesCSS.content}>
16 {games.map((game, index) => ( 20 {games.map((game, index) => (
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx
index 68562b6..4f46af5 100644
--- a/frontend/src/pages/Homepage.tsx
+++ b/frontend/src/pages/Homepage.tsx
@@ -1,11 +1,15 @@
1import React from 'react'; 1import React from 'react';
2import { Helmet } from 'react-helmet';
2 3
3const Homepage: React.FC = () => { 4const Homepage: React.FC = () => {
4 5
5 return ( 6 return (
6 <main> 7 <main>
8 <Helmet>
9 <title>LPHUB | Homepage</title>
10 </Helmet>
7 <section> 11 <section>
8 <p/> 12 <p />
9 <h1>Welcome to Least Portals Hub!</h1> 13 <h1>Welcome to Least Portals Hub!</h1>
10 <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 <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>
11 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> 15 <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p>
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx
index ecea3e1..b9e17f7 100644
--- a/frontend/src/pages/Maplist.tsx
+++ b/frontend/src/pages/Maplist.tsx
@@ -1,5 +1,6 @@
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";
3 4
4import "@css/Maplist.css"; 5import "@css/Maplist.css";
5import { API } from "@api/Api"; 6import { API } from "@api/Api";
@@ -25,9 +26,9 @@ const Maplist: React.FC = () => {
25 const navigate = useNavigate(); 26 const navigate = useNavigate();
26 27
27 function _update_currently_selected(catNum2: number) { 28 function _update_currently_selected(catNum2: number) {
28 setCurrentlySelected(catNum2); 29 setCurrentlySelected(catNum2);
29 navigate("/games/" + game?.id + "?cat=" + catNum2); 30 navigate("/games/" + game?.id + "?cat=" + catNum2);
30 setHasClicked(true); 31 setHasClicked(true);
31 } 32 }
32 33
33 const _fetch_chapters = async (chapter_id: string) => { 34 const _fetch_chapters = async (chapter_id: string) => {
@@ -52,12 +53,12 @@ const Maplist: React.FC = () => {
52 // location query params 53 // location query params
53 const queryParams = new URLSearchParams(location.search); 54 const queryParams = new URLSearchParams(location.search);
54 if (queryParams.get("chapter")) { 55 if (queryParams.get("chapter")) {
55 let cat = parseFloat(queryParams.get("chapter") || ""); 56 let cat = parseFloat(queryParams.get("chapter") || "");
56 if (gameId == 2) { 57 if (gameId == 2) {
57 cat += 10; 58 cat += 10;
58 } 59 }
59 _fetch_chapters(cat.toString()); 60 _fetch_chapters(cat.toString());
60 } 61 }
61 62
62 const _fetch_game = async () => { 63 const _fetch_game = async () => {
63 const games = await API.get_games(); 64 const games = await API.get_games();
@@ -68,7 +69,7 @@ const Maplist: React.FC = () => {
68 setLoad(false); 69 setLoad(false);
69 } 70 }
70 }; 71 };
71 72
72 const _fetch_game_chapters = async () => { 73 const _fetch_game_chapters = async () => {
73 const games_chapters = await API.get_games_chapters(gameId.toString()); 74 const games_chapters = await API.get_games_chapters(gameId.toString());
74 setGameChapters(games_chapters); 75 setGameChapters(games_chapters);
@@ -81,7 +82,7 @@ const Maplist: React.FC = () => {
81 }, []); 82 }, []);
82 83
83 useEffect(() => { 84 useEffect(() => {
84 const queryParams = new URLSearchParams(location.search); 85 const queryParams = new URLSearchParams(location.search);
85 if (gameChapters != undefined && !queryParams.get("chapter")) { 86 if (gameChapters != undefined && !queryParams.get("chapter")) {
86 _fetch_chapters(gameChapters!.chapters[0].id.toString()); 87 _fetch_chapters(gameChapters!.chapters[0].id.toString());
87 } 88 }
@@ -97,6 +98,9 @@ const Maplist: React.FC = () => {
97 98
98 return ( 99 return (
99 <main> 100 <main>
101 <Helmet>
102 <title>LPHUB | Maplist</title>
103 </Helmet>
100 <section style={{ marginTop: "20px" }}> 104 <section style={{ marginTop: "20px" }}>
101 <Link to="/games"> 105 <Link to="/games">
102 <button className="nav-button" style={{ borderRadius: "20px" }}> 106 <button className="nav-button" style={{ borderRadius: "20px" }}>
@@ -129,7 +133,7 @@ const Maplist: React.FC = () => {
129 </div> 133 </div>
130 <div className="game-header-categories"> 134 <div className="game-header-categories">
131 {game?.category_portals.map((cat, index) => ( 135 {game?.category_portals.map((cat, index) => (
132 <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)}}> 136 <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) }}>
133 <span>{cat.category.name}</span> 137 <span>{cat.category.name}</span>
134 </button> 138 </button>
135 ))} 139 ))}
@@ -140,26 +144,26 @@ const Maplist: React.FC = () => {
140 <div> 144 <div>
141 <section className="chapter-select-container"> 145 <section className="chapter-select-container">
142 <div> 146 <div>
143 <span style={{fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px"}}>{curChapter?.chapter.name.split(" - ")[0]}</span> 147 <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span>
144 </div> 148 </div>
145 <div onClick={_handle_dropdown_click} className="dropdown"> 149 <div onClick={_handle_dropdown_click} className="dropdown">
146 <span>{curChapter?.chapter.name.split(" - ")[1]}</span> 150 <span>{curChapter?.chapter.name.split(" - ")[1]}</span>
147 <i className="triangle"></i> 151 <i className="triangle"></i>
148 </div> 152 </div>
149 <div className="dropdown-elements" style={{display: dropdownActive}}> 153 <div className="dropdown-elements" style={{ display: dropdownActive }}>
150 {gameChapters?.chapters.map((chapter, i) => { 154 {gameChapters?.chapters.map((chapter, i) => {
151 return <div className="dropdown-element" onClick={() => {_fetch_chapters(chapter.id.toString()); _handle_dropdown_click()}}>{chapter.name}</div> 155 return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div>
152 }) 156 })
153 157
154 } 158 }
155 </div> 159 </div>
156 </section> 160 </section>
157 <section className="maplist"> 161 <section className="maplist">
158 {curChapter?.maps.map((map, i) => { 162 {curChapter?.maps.map((map, i) => {
159 return <div className="maplist-entry"> 163 return <div className="maplist-entry">
160 <Link to={`/maps/${map.id}`}> 164 <Link to={`/maps/${map.id}`}>
161 <span>{map.name}</span> 165 <span>{map.name}</span>
162 <div className="map-entry-image" style={{backgroundImage: `url(${map.image})`}}> 166 <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}>
163 <div className="blur map"> 167 <div className="blur map">
164 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( 168 <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find(
165 (obj) => obj.category.id === catNum + 1 169 (obj) => obj.category.id === catNum + 1
@@ -169,7 +173,7 @@ const Maplist: React.FC = () => {
169 </div> 173 </div>
170 <div className="difficulty-bar"> 174 <div className="difficulty-bar">
171 {/* <span>Difficulty:</span> */} 175 {/* <span>Difficulty:</span> */}
172 <div className={map.difficulty == 0 ? "one" : map.difficulty == 1 ? "two" : map.difficulty == 2 ? "three" : map.difficulty == 3 ? "four" : map.difficulty == 4 ? "five" : "one"}> 176 <div className={map.difficulty <= 2 ? "one" : map.difficulty <= 4 ? "two" : map.difficulty <= 6 ? "three" : map.difficulty <= 8 ? "four" : map.difficulty <= 10 ? "five" : "one"}>
173 <div className="difficulty-point"></div> 177 <div className="difficulty-point"></div>
174 <div className="difficulty-point"></div> 178 <div className="difficulty-point"></div>
175 <div className="difficulty-point"></div> 179 <div className="difficulty-point"></div>
@@ -177,9 +181,9 @@ const Maplist: React.FC = () => {
177 <div className="difficulty-point"></div> 181 <div className="difficulty-point"></div>
178 </div> 182 </div>
179 </div> 183 </div>
180 </Link> 184 </Link>
181 </div> 185 </div>
182 })} 186 })}
183 </section> 187 </section>
184 </div> 188 </div>
185 </section> 189 </section>
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx
index f1daa36..fb13563 100644
--- a/frontend/src/pages/Maps.tsx
+++ b/frontend/src/pages/Maps.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from 'react-router-dom';
3import { Helmet } from 'react-helmet';
3 4
4import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images'; 5import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images';
5import Summary from '@components/Summary'; 6import Summary from '@components/Summary';
@@ -35,7 +36,7 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
35 }; 36 };
36 37
37 const _fetch_map_leaderboards = async () => { 38 const _fetch_map_leaderboards = async () => {
38 const mapLeaderboards = await API.get_map_leaderboard(mapID); 39 const mapLeaderboards = await API.get_map_leaderboard(mapID, "1");
39 setMapLeaderboardData(mapLeaderboards); 40 setMapLeaderboardData(mapLeaderboards);
40 }; 41 };
41 42
@@ -53,26 +54,32 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
53 if (!mapSummaryData) { 54 if (!mapSummaryData) {
54 // loading placeholder 55 // loading placeholder
55 return ( 56 return (
56 <main> 57 <>
57 <section id='section1' className='summary1'> 58 <main>
58 <div> 59 <section id='section1' className='summary1'>
59 <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> 60 <div>
60 </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>
61 </section> 62 </div>
62 63 </section>
63 <section id='section2' className='summary1'> 64
64 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> 65 <section id='section2' className='summary1'>
65 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> 66 <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button>
66 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> 67 <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button>
67 </section> 68 <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button>
68 69 </section>
69 <section id='section6' className='summary2' /> 70
70 </main> 71 <section id='section6' className='summary2' />
72 </main>
73 </>
71 ); 74 );
72 } 75 }
73 76
74 return ( 77 return (
75 <> 78 <>
79 <Helmet>
80 <title>LPHUB | {mapSummaryData.map.map_name}</title>
81 <meta name="description" content={mapSummaryData.map.map_name} />
82 </Helmet>
76 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} 83 {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />}
77 84
78 <div id='background-image'> 85 <div id='background-image'>
@@ -94,7 +101,7 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => {
94 </section> 101 </section>
95 102
96 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} 103 {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />}
97 {navState === 1 && <Leaderboards data={mapLeaderboardData} />} 104 {navState === 1 && <Leaderboards mapID={mapID} />}
98 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} 105 {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />}
99 </main> 106 </main>
100 </> 107 </>
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx
index 00d8f4e..ee56999 100644
--- a/frontend/src/pages/Profile.tsx
+++ b/frontend/src/pages/Profile.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from 'react-router-dom';
3import { Helmet } from 'react-helmet';
3 4
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; 5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images';
5import { UserProfile } from '@customTypes/Profile'; 6import { UserProfile } from '@customTypes/Profile';
@@ -109,6 +110,10 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
109 110
110 return ( 111 return (
111 <div style={{position: "absolute", width: "calc(100% - 50px)", left: "350px"}}> 112 <div style={{position: "absolute", width: "calc(100% - 50px)", left: "350px"}}>
113 <Helmet>
114 <title>LPHUB | {profile.user_name}</title>
115 <meta name="description" content={profile.user_name} />
116 </Helmet>
112 {MessageDialogComponent} 117 {MessageDialogComponent}
113 {MessageDialogLoadComponent} 118 {MessageDialogLoadComponent}
114 {ConfirmDialogComponent} 119 {ConfirmDialogComponent}
@@ -267,7 +272,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
267 272
268 <span style={{ display: "grid" }}>{e.score_count}</span> 273 <span style={{ display: "grid" }}>{e.score_count}</span>
269 274
270 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : e.score_count - r.map_wr_count}</span> 275 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span>
271 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 276 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span>
272 <span> </span> 277 <span> </span>
273 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 278 {i === 0 ? <span>#{r.placement}</span> : <span> </span>}
@@ -313,7 +318,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec
313 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 318 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
314 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 319 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link>
315 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 320 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span>
316 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : record!.scores[i].score_count - record!.map_wr_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>
317 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> 322 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
318 <span> </span> 323 <span> </span>
319 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 324 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>}
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx
index cdb87a8..71aa427 100644
--- a/frontend/src/pages/Rankings.tsx
+++ b/frontend/src/pages/Rankings.tsx
@@ -1,4 +1,5 @@
1import React, { useEffect } from "react"; 1import React, { useEffect } from "react";
2import { Helmet } from "react-helmet";
2 3
3import RankingEntry from "@components/RankingEntry"; 4import RankingEntry from "@components/RankingEntry";
4import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; 5import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking";
@@ -13,9 +14,9 @@ const Rankings: React.FC = () => {
13 official, 14 official,
14 unofficial 15 unofficial
15 } 16 }
16 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); 17 const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official);
17 18
18 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); 19 const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false);
19 20
20 enum RankingCategories { 21 enum RankingCategories {
21 rankings_overall, 22 rankings_overall,
@@ -26,7 +27,7 @@ const Rankings: React.FC = () => {
26 const [load, setLoad] = React.useState<boolean>(false); 27 const [load, setLoad] = React.useState<boolean>(false);
27 28
28 const _fetch_rankings = async () => { 29 const _fetch_rankings = async () => {
29 setLeaderboardLoad(false); 30 setLeaderboardLoad(false);
30 const rankings = await API.get_official_rankings(); 31 const rankings = await API.get_official_rankings();
31 setLeaderboardData(rankings); 32 setLeaderboardData(rankings);
32 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 33 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
@@ -37,12 +38,12 @@ const Rankings: React.FC = () => {
37 setCurrentLeaderboard(rankings.rankings_overall) 38 setCurrentLeaderboard(rankings.rankings_overall)
38 } 39 }
39 setLoad(true); 40 setLoad(true);
40 setLeaderboardLoad(true); 41 setLeaderboardLoad(true);
41 } 42 }
42 43
43 const __dev_fetch_unofficial_rankings = async () => { 44 const __dev_fetch_unofficial_rankings = async () => {
44 try { 45 try {
45 setLeaderboardLoad(false); 46 setLeaderboardLoad(false);
46 const rankings = await API.get_unofficial_rankings(); 47 const rankings = await API.get_unofficial_rankings();
47 setLeaderboardData(rankings); 48 setLeaderboardData(rankings);
48 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { 49 if (currentLeaderboardType == RankingCategories.rankings_singleplayer) {
@@ -53,7 +54,7 @@ const Rankings: React.FC = () => {
53 } else { 54 } else {
54 setCurrentLeaderboard(rankings.rankings_overall) 55 setCurrentLeaderboard(rankings.rankings_overall)
55 } 56 }
56 setLeaderboardLoad(true); 57 setLeaderboardLoad(true);
57 } catch (e) { 58 } catch (e) {
58 console.log(e) 59 console.log(e)
59 } 60 }
@@ -88,12 +89,15 @@ const Rankings: React.FC = () => {
88 89
89 return ( 90 return (
90 <main> 91 <main>
92 <Helmet>
93 <title>LPHUB | Rankings</title>
94 </Helmet>
91 <section className="nav-container nav-1"> 95 <section className="nav-container nav-1">
92 <div> 96 <div>
93 <button onClick={() => {_fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official)}} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> 97 <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}>
94 <span>Official (LPHUB)</span> 98 <span>Official (LPHUB)</span>
95 </button> 99 </button>
96 <button onClick={() => {__dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial)}} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> 100 <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}>
97 <span>Unofficial (Steam)</span> 101 <span>Unofficial (Steam)</span>
98 </button> 102 </button>
99 </div> 103 </div>
@@ -128,11 +132,11 @@ const Rankings: React.FC = () => {
128 }) 132 })
129 } 133 }
130 134
131 {leaderboardLoad ? null : 135 {leaderboardLoad ? null :
132 <div style={{display: "flex", justifyContent: "center", margin: "30px 0px"}}> 136 <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}>
133 <span className="loader"></span> 137 <span className="loader"></span>
134 </div> 138 </div>
135 } 139 }
136 </div> 140 </div>
137 </section> 141 </section>
138 : null} 142 : null}
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx
index b5625ce..9f57b7e 100644
--- a/frontend/src/pages/Rules.tsx
+++ b/frontend/src/pages/Rules.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from 'react-markdown';
3import { Helmet } from 'react-helmet';
3 4
4import '@css/Rules.css'; 5import '@css/Rules.css';
5 6
@@ -29,6 +30,9 @@ const Rules: React.FC = () => {
29 30
30 return ( 31 return (
31 <main> 32 <main>
33 <Helmet>
34 <title>LPHUB | Rules</title>
35 </Helmet>
32 <ReactMarkdown>{rulesText}</ReactMarkdown> 36 <ReactMarkdown>{rulesText}</ReactMarkdown>
33 </main> 37 </main>
34 ); 38 );
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx
index f90d1aa..d43c0c6 100644
--- a/frontend/src/pages/User.tsx
+++ b/frontend/src/pages/User.tsx
@@ -1,5 +1,6 @@
1import React from 'react'; 1import React from 'react';
2import { Link, useLocation, useNavigate } from 'react-router-dom'; 2import { Link, useLocation, useNavigate } from 'react-router-dom';
3import { Helmet } from 'react-helmet';
3 4
4import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; 5import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images';
5import { UserProfile } from '@customTypes/Profile'; 6import { UserProfile } from '@customTypes/Profile';
@@ -92,6 +93,10 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
92 93
93 return ( 94 return (
94 <main> 95 <main>
96 <Helmet>
97 <title>LPHUB | {user.user_name}</title>
98 <meta name="description" content={user.user_name} />
99 </Helmet>
95 {MessageDialogComponent} 100 {MessageDialogComponent}
96 <section id='section1' className='profile'> 101 <section id='section1' className='profile'>
97 <div> 102 <div>
@@ -236,7 +241,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
236 241
237 <span style={{ display: "grid" }}>{e.score_count}</span> 242 <span style={{ display: "grid" }}>{e.score_count}</span>
238 243
239 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : e.score_count - r.map_wr_count}</span> 244 <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span>
240 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> 245 <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span>
241 <span> </span> 246 <span> </span>
242 {i === 0 ? <span>#{r.placement}</span> : <span> </span>} 247 {i === 0 ? <span>#{r.placement}</span> : <span> </span>}
@@ -281,7 +286,7 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => {
281 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} 286 {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""}
282 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> 287 <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link>
283 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> 288 <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span>
284 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : record!.scores[i].score_count - record!.map_wr_count}</span> 289 <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span>
285 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> 290 <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span>
286 <span> </span> 291 <span> </span>
287 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} 292 {i === 0 ? <span>#{record!.placement}</span> : <span> </span>}
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts
index 42a6917..775fab4 100644
--- a/frontend/src/types/Content.ts
+++ b/frontend/src/types/Content.ts
@@ -18,7 +18,6 @@ export interface MapDiscussionCommentContent {
18}; 18};
19 19
20export interface UploadRunContent { 20export interface UploadRunContent {
21 map_id: number;
22 host_demo: File | null; 21 host_demo: File | null;
23 partner_demo: File | null; 22 partner_demo: File | null;
24}; 23};
diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts
index 89c66d5..4f8eabf 100644
--- a/frontend/src/types/Map.ts
+++ b/frontend/src/types/Map.ts
@@ -79,6 +79,7 @@ interface MapSummaryMap {
79 map_name: string; 79 map_name: string;
80 is_coop: boolean; 80 is_coop: boolean;
81 is_disabled: boolean; 81 is_disabled: boolean;
82 difficulty: number;
82}; 83};
83 84
84interface MapSummaryDetails { 85interface MapSummaryDetails {
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts
new file mode 100644
index 0000000..b6313e7
--- /dev/null
+++ b/frontend/src/types/MapNames.ts
@@ -0,0 +1,127 @@
1export const MapNames: { [key: string]: number } = {
2 "sp_a1_intro1": 1,
3 "sp_a1_intro2": 2,
4 "sp_a1_intro3": 3,
5 "sp_a1_intro4": 4,
6 "sp_a1_intro5": 5,
7 "sp_a1_intro6": 6,
8 "sp_a1_intro7": 7,
9 "sp_a1_wakeup": 8,
10 "sp_a2_intro": 9,
11
12 "sp_a2_laser_intro": 10,
13 "sp_a2_laser_stairs": 11,
14 "sp_a2_dual_lasers": 12,
15 "sp_a2_laser_over_goo": 13,
16 "sp_a2_catapult_intro": 14,
17 "sp_a2_trust_fling": 15,
18 "sp_a2_pit_flings": 16,
19 "sp_a2_fizzler_intro": 17,
20
21 "sp_a2_sphere_peek": 18,
22 "sp_a2_ricochet": 19,
23 "sp_a2_bridge_intro": 20,
24 "sp_a2_bridge_the_gap": 21,
25 "sp_a2_turret_intro": 22,
26 "sp_a2_laser_relays": 23,
27 "sp_a2_turret_blocker": 24,
28 "sp_a2_laser_vs_turret": 25,
29 "sp_a2_pull_the_rug": 26,
30
31 "sp_a2_column_blocker": 27,
32 "sp_a2_laser_chaining": 28,
33 "sp_a2_triple_laser": 29,
34 "sp_a2_bts1": 30,
35 "sp_a2_bts2": 31,
36
37 "sp_a2_bts3": 32,
38 "sp_a2_bts4": 33,
39 "sp_a2_bts5": 34,
40 "sp_a2_core": 35,
41
42 "sp_a3_01": 36,
43 "sp_a3_03": 37,
44 "sp_a3_jump_intro": 38,
45 "sp_a3_bomb_flings": 39,
46 "sp_a3_crazy_box": 40,
47 "sp_a3_transition01": 41,
48
49 "sp_a3_speed_ramp": 42,
50 "sp_a3_speed_flings": 43,
51 "sp_a3_portal_intro": 44,
52 "sp_a3_end": 45,
53
54 "sp_a4_intro": 46,
55 "sp_a4_tb_intro": 47,
56 "sp_a4_tb_trust_drop": 48,
57 "sp_a4_tb_wall_button": 49,
58 "sp_a4_tb_polarity": 50,
59 "sp_a4_tb_catch": 51,
60 "sp_a4_stop_the_box": 52,
61 "sp_a4_laser_catapult": 53,
62 "sp_a4_laser_platform": 54,
63 "sp_a4_speed_tb_catch": 55,
64 "sp_a4_jump_polarity": 56,
65
66 "sp_a4_finale1": 57,
67 "sp_a4_finale2": 58,
68 "sp_a4_finale3": 59,
69 "sp_a4_finale4": 60,
70
71 "mp_coop_start": 61,
72 "mp_coop_lobby_3": 62,
73
74 "mp_coop_doors": 63,
75 "mp_coop_race_2": 64,
76 "mp_coop_laser_2": 65,
77 "mp_coop_rat_maze": 66,
78 "mp_coop_laser_crusher": 67,
79 "mp_coop_teambts": 68,
80
81 "mp_coop_fling_3": 69,
82 "mp_coop_infinifling_train": 70,
83 "mp_coop_come_along": 71,
84 "mp_coop_fling_1": 72,
85 "mp_coop_catapult_1": 73,
86 "mp_coop_multifling_1": 74,
87 "mp_coop_fling_crushers": 75,
88 "mp_coop_fan": 76,
89
90 "mp_coop_wall_intro": 77,
91 "mp_coop_wall_2": 78,
92 "mp_coop_catapult_wall_intro": 79,
93 "mp_coop_wall_block": 80,
94 "mp_coop_catapult_2": 81,
95 "mp_coop_turret_walls": 82,
96 "mp_coop_turret_ball": 83,
97 "mp_coop_wall_5": 84,
98
99 "mp_coop_tbeam_redirect": 85,
100 "mp_coop_tbeam_drill": 86,
101 "mp_coop_tbeam_catch_grind_1": 87,
102 "mp_coop_tbeam_laser_1": 88,
103 "mp_coop_tbeam_polarity": 89,
104 "mp_coop_tbeam_polarity2": 90,
105 "mp_coop_tbeam_polarity3": 91,
106 "mp_coop_tbeam_maze": 92,
107 "mp_coop_tbeam_end": 93,
108
109 "mp_coop_paint_come_along": 94,
110 "mp_coop_paint_redirect": 95,
111 "mp_coop_paint_bridge": 96,
112 "mp_coop_paint_walljumps": 97,
113 "mp_coop_paint_speed_fling": 98,
114 "mp_coop_paint_red_racer": 99,
115 "mp_coop_paint_speed_catch": 100,
116 "mp_coop_paint_longjump_intro": 101,
117
118 "mp_coop_separation_1": 102,
119 "mp_coop_tripleaxis": 103,
120 "mp_coop_catapult_catch": 104,
121 "mp_coop_2paints_1bridge": 105,
122 "mp_coop_paint_conversion": 106,
123 "mp_coop_bridge_catch": 107,
124 "mp_coop_laser_tbeam": 108,
125 "mp_coop_paint_rat_maze": 109,
126 "mp_coop_paint_crazy_box": 110,
127};