aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ConfirmDialog.tsx51
-rw-r--r--frontend/src/components/Discussions.tsx327
-rw-r--r--frontend/src/components/GameCategory.tsx32
-rw-r--r--frontend/src/components/GameEntry.tsx38
-rw-r--r--frontend/src/components/Leaderboards.tsx295
-rw-r--r--frontend/src/components/Login.tsx128
-rw-r--r--frontend/src/components/MapEntry.tsx10
-rw-r--r--frontend/src/components/MessageDialog.tsx48
-rw-r--r--frontend/src/components/MessageDialogLoad.tsx46
-rw-r--r--frontend/src/components/ModMenu.tsx461
-rw-r--r--frontend/src/components/RankingEntry.tsx93
-rw-r--r--frontend/src/components/Sidebar.tsx381
-rw-r--r--frontend/src/components/Summary.tsx300
-rw-r--r--frontend/src/components/UploadRunDialog.tsx375
14 files changed, 1655 insertions, 930 deletions
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index 44a653b..8f2ce7a 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,31 +1,34 @@
1import React from 'react'; 1import React from "react";
2
3import "@css/Dialog.css"
4 2
5interface ConfirmDialogProps { 3interface ConfirmDialogProps {
6 title: string; 4 title: string;
7 subtitle: string; 5 subtitle: string;
8 onConfirm: () => void; 6 onConfirm: () => void;
9 onCancel: () => void; 7 onCancel: () => void;
10}; 8}
11 9
12const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { 10const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
13 return ( 11 title,
14 <div className='dimmer'> 12 subtitle,
15 <div className='dialog'> 13 onConfirm,
16 <div className='dialog-element dialog-header'> 14 onCancel,
17 <span>{title}</span> 15}) => {
18 </div> 16 return (
19 <div className='dialog-element dialog-description'> 17 <div className="fixed w-[200%] h-full bg-black bg-opacity-50 z-[4]">
20 <span>{subtitle}</span> 18 <div className="fixed z-[4] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-surface rounded-3xl overflow-hidden min-w-[350px] border border-border animate-[dialog_in_0.2s_cubic-bezier(0.075,0.82,0.165,1.1)] text-foreground font-[--font-barlow-semicondensed-regular]">
21 </div> 19 <div className="p-2 text-2xl bg-mantle">
22 <div className='dialog-element dialog-btns-container'> 20 <span>{title}</span>
23 <button onClick={onCancel}>Cancel</button> 21 </div>
24 <button onClick={onConfirm}>Confirm</button> 22 <div className="p-2">
25 </div> 23 <span>{subtitle}</span>
26 </div> 24 </div>
25 <div className="p-2 flex justify-end border-t-2 border-border bg-mantle">
26 <button className="mr-2 px-4 py-2 bg-muted text-foreground rounded hover:bg-overlay1 transition-colors" onClick={onCancel}>Cancel</button>
27 <button className="px-4 py-2 bg-primary text-background rounded hover:bg-mauve transition-colors" onClick={onConfirm}>Confirm</button>
27 </div> 28 </div>
28 ) 29 </div>
30 </div>
31 );
29}; 32};
30 33
31export default ConfirmDialog; 34export default ConfirmDialog;
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx
index 17ae586..7aa8901 100644
--- a/frontend/src/components/Discussions.tsx
+++ b/frontend/src/components/Discussions.tsx
@@ -1,34 +1,48 @@
1import React from 'react'; 1import React from "react";
2 2
3import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '@customTypes/Map'; 3import {
4import { MapDiscussionCommentContent, MapDiscussionContent } from '@customTypes/Content'; 4 MapDiscussion,
5import { time_ago } from '@utils/Time'; 5 MapDiscussions,
6import { API } from '@api/Api'; 6 MapDiscussionsDetail,
7import "@css/Maps.css" 7} from "@customTypes/Map";
8import { Link } from 'react-router-dom'; 8import { MapDiscussionContent } from "@customTypes/Content";
9import useConfirm from '@hooks/UseConfirm'; 9import { time_ago } from "@utils/Time";
10import { API } from "@api/Api";
11import "@css/Maps.css";
12import { Link } from "react-router-dom";
13import useConfirm from "@hooks/UseConfirm";
10 14
11interface DiscussionsProps { 15interface DiscussionsProps {
12 token?: string 16 token?: string;
13 data?: MapDiscussions; 17 data?: MapDiscussions;
14 isModerator: boolean; 18 isModerator: boolean;
15 mapID: string; 19 mapID: string;
16 onRefresh: () => void; 20 onRefresh: () => void;
17} 21}
18 22
19const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, mapID, onRefresh }) => { 23const Discussions: React.FC<DiscussionsProps> = ({
20 24 token,
25 data,
26 isModerator,
27 mapID,
28 onRefresh,
29}) => {
21 const { confirm, ConfirmDialogComponent } = useConfirm(); 30 const { confirm, ConfirmDialogComponent } = useConfirm();
22 31
23 const [discussionThread, setDiscussionThread] = React.useState<MapDiscussion | undefined>(undefined); 32 const [discussionThread, setDiscussionThread] = React.useState<
33 MapDiscussion | undefined
34 >(undefined);
24 const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); 35 const [discussionSearch, setDiscussionSearch] = React.useState<string>("");
25 36
26 const [createDiscussion, setCreateDiscussion] = React.useState<boolean>(false); 37 const [createDiscussion, setCreateDiscussion] =
27 const [createDiscussionContent, setCreateDiscussionContent] = React.useState<MapDiscussionContent>({ 38 React.useState<boolean>(false);
28 title: "", 39 const [createDiscussionContent, setCreateDiscussionContent] =
29 content: "", 40 React.useState<MapDiscussionContent>({
30 }); 41 title: "",
31 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState<string>(""); 42 content: "",
43 });
44 const [createDiscussionCommentContent, setCreateDiscussionCommentContent] =
45 React.useState<string>("");
32 46
33 const _open_map_discussion = async (discussion_id: number) => { 47 const _open_map_discussion = async (discussion_id: number) => {
34 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); 48 const mapDiscussion = await API.get_map_discussion(mapID, discussion_id);
@@ -45,13 +59,23 @@ const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, map
45 59
46 const _create_map_discussion_comment = async (discussion_id: number) => { 60 const _create_map_discussion_comment = async (discussion_id: number) => {
47 if (token) { 61 if (token) {
48 await API.post_map_discussion_comment(token, mapID, discussion_id, createDiscussionCommentContent); 62 await API.post_map_discussion_comment(
63 token,
64 mapID,
65 discussion_id,
66 createDiscussionCommentContent
67 );
49 await _open_map_discussion(discussion_id); 68 await _open_map_discussion(discussion_id);
50 } 69 }
51 }; 70 };
52 71
53 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { 72 const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => {
54 if (await confirm("Delete Map Discussion", `Are you sure you want to remove post: ${discussion.title}?`)) { 73 if (
74 await confirm(
75 "Delete Map Discussion",
76 `Are you sure you want to remove post: ${discussion.title}?`
77 )
78 ) {
55 if (token) { 79 if (token) {
56 await API.delete_map_discussion(token, mapID, discussion.id); 80 await API.delete_map_discussion(token, mapID, discussion.id);
57 onRefresh(); 81 onRefresh();
@@ -60,107 +84,186 @@ const Discussions: React.FC<DiscussionsProps> = ({ token, data, isModerator, map
60 }; 84 };
61 85
62 return ( 86 return (
63 <section id='section7' className='summary3'> 87 <section id="section7" className="summary3">
64 {ConfirmDialogComponent} 88 {ConfirmDialogComponent}
65 <div id='discussion-search'> 89 <div id="discussion-search">
66 <input type="text" value={discussionSearch} placeholder={"Search for posts..."} onChange={(e) => setDiscussionSearch(e.target.value)} /> 90 <input
67 <div><button onClick={() => setCreateDiscussion(true)}>New Post</button></div> 91 type="text"
92 value={discussionSearch}
93 placeholder={"Search for posts..."}
94 onChange={e => setDiscussionSearch(e.target.value)}
95 />
96 <div>
97 <button onClick={() => setCreateDiscussion(true)}>New Post</button>
98 </div>
68 </div> 99 </div>
69 100
70 { // janky ternary operators here, could divide them to more components? 101 {
71 createDiscussion ? 102 // janky ternary operators here, could divide them to more components?
72 ( 103 createDiscussion ? (
73 <div id='discussion-create'> 104 <div id="discussion-create">
74 <span>Create Post</span> 105 <span>Create Post</span>
75 <button onClick={() => setCreateDiscussion(false)}>X</button> 106 <button onClick={() => setCreateDiscussion(false)}>X</button>
76 <div style={{ gridColumn: "1 / span 2" }}> 107 <div style={{ gridColumn: "1 / span 2" }}>
77 <input id='discussion-create-title' placeholder='Title...' onChange={(e) => setCreateDiscussionContent({ 108 <input
78 ...createDiscussionContent, 109 id="discussion-create-title"
79 title: e.target.value, 110 placeholder="Title..."
80 })} /> 111 onChange={e =>
81 <input id='discussion-create-content' placeholder='Enter the content...' onChange={(e) => setCreateDiscussionContent({ 112 setCreateDiscussionContent({
82 ...createDiscussionContent, 113 ...createDiscussionContent,
83 content: e.target.value, 114 title: e.target.value,
84 })} /> 115 })
85 </div> 116 }
86 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> 117 />
87 <button id='discussion-create-button' onClick={() => _create_map_discussion()}>Post</button> 118 <input
88 </div> 119 id="discussion-create-content"
120 placeholder="Enter the content..."
121 onChange={e =>
122 setCreateDiscussionContent({
123 ...createDiscussionContent,
124 content: e.target.value,
125 })
126 }
127 />
128 </div>
129 <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}>
130 <button
131 id="discussion-create-button"
132 onClick={() => _create_map_discussion()}
133 >
134 Post
135 </button>
136 </div>
137 </div>
138 ) : discussionThread ? (
139 <div id="discussion-thread">
140 <div>
141 <span>{discussionThread.discussion.title}</span>
142 <button onClick={() => setDiscussionThread(undefined)}>X</button>
89 </div> 143 </div>
90 )
91 :
92 discussionThread ?
93 (
94 <div id='discussion-thread'>
95 <div>
96 <span>{discussionThread.discussion.title}</span>
97 <button onClick={() => setDiscussionThread(undefined)}>X</button>
98 </div>
99 144
100 <div> 145 <div>
101 <Link to={`/users/${discussionThread.discussion.creator.steam_id}`}> 146 <Link
102 <img src={discussionThread.discussion.creator.avatar_link} alt="" /> 147 to={`/users/${discussionThread.discussion.creator.steam_id}`}
103 </Link> 148 >
104 <div> 149 <img
105 <span>{discussionThread.discussion.creator.user_name}</span> 150 src={discussionThread.discussion.creator.avatar_link}
106 <span>{time_ago(new Date(discussionThread.discussion.created_at.replace("T", " ").replace("Z", "")))}</span> 151 alt=""
107 <span>{discussionThread.discussion.content}</span> 152 />
108 </div> 153 </Link>
109 {discussionThread.discussion.comments ? 154 <div>
110 discussionThread.discussion.comments.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) 155 <span>{discussionThread.discussion.creator.user_name}</span>
111 .map(e => ( 156 <span>
112 <> 157 {time_ago(
113 <Link to={`/users/${e.user.steam_id}`}> 158 new Date(
114 <img src={e.user.avatar_link} alt="" /> 159 discussionThread.discussion.created_at
115 </Link> 160 .replace("T", " ")
116 <div> 161 .replace("Z", "")
117 <span>{e.user.user_name}</span> 162 )
118 <span>{time_ago(new Date(e.date.replace("T", " ").replace("Z", "")))}</span> 163 )}
119 <span>{e.comment}</span> 164 </span>
120 </div> 165 <span>{discussionThread.discussion.content}</span>
121 </> 166 </div>
122 )) : "" 167 {discussionThread.discussion.comments
123 } 168 ? discussionThread.discussion.comments
124 </div> 169 .sort(
125 <div id='discussion-send'> 170 (a, b) =>
126 <input type="text" value={createDiscussionCommentContent} placeholder={"Message"} 171 new Date(a.date).getTime() - new Date(b.date).getTime()
127 onKeyDown={(e) => e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)} 172 )
128 onChange={(e) => setCreateDiscussionCommentContent(e.target.value)} /> 173 .map(e => (
129 <div><button onClick={() => { 174 <>
175 <Link to={`/users/${e.user.steam_id}`}>
176 <img src={e.user.avatar_link} alt="" />
177 </Link>
178 <div>
179 <span>{e.user.user_name}</span>
180 <span>
181 {time_ago(
182 new Date(
183 e.date.replace("T", " ").replace("Z", "")
184 )
185 )}
186 </span>
187 <span>{e.comment}</span>
188 </div>
189 </>
190 ))
191 : ""}
192 </div>
193 <div id="discussion-send">
194 <input
195 type="text"
196 value={createDiscussionCommentContent}
197 placeholder={"Message"}
198 onKeyDown={e =>
199 e.key === "Enter" &&
200 _create_map_discussion_comment(discussionThread.discussion.id)
201 }
202 onChange={e =>
203 setCreateDiscussionCommentContent(e.target.value)
204 }
205 />
206 <div>
207 <button
208 onClick={() => {
130 if (createDiscussionCommentContent !== "") { 209 if (createDiscussionCommentContent !== "") {
131 _create_map_discussion_comment(discussionThread.discussion.id); 210 _create_map_discussion_comment(
211 discussionThread.discussion.id
212 );
132 setCreateDiscussionCommentContent(""); 213 setCreateDiscussionCommentContent("");
133 } 214 }
134 }}>Send</button></div> 215 }}
135 </div> 216 >
136 217 Send
218 </button>
137 </div> 219 </div>
138 ) 220 </div>
139 : 221 </div>
140 ( 222 ) : data ? (
141 data ? 223 <>
142 (<> 224 {data.discussions
143 {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) 225 .filter(f => f.title.includes(discussionSearch))
144 .map((e, i) => ( 226 .sort(
145 <div id='discussion-post'> 227 (a, b) =>
146 <button key={e.id} onClick={() => _open_map_discussion(e.id)}> 228 new Date(b.updated_at).getTime() -
147 <span>{e.title}</span> 229 new Date(a.updated_at).getTime()
148 {isModerator ? 230 )
149 <button onClick={(m) => { 231 .map((e, i) => (
150 m.stopPropagation(); 232 <div id="discussion-post">
151 _delete_map_discussion(e); 233 <button key={e.id} onClick={() => _open_map_discussion(e.id)}>
152 }}>Delete Post</button> 234 <span>{e.title}</span>
153 : <span></span> 235 {isModerator ? (
154 } 236 <button
155 <span><b>{e.creator.user_name}:</b> {e.content}</span> 237 onClick={m => {
156 <span>Last Updated: {time_ago(new Date(e.updated_at.replace("T", " ").replace("Z", "")))}</span> 238 m.stopPropagation();
157 </button> 239 _delete_map_discussion(e);
158 </div> 240 }}
159 ))} 241 >
160 </>) 242 Delete Post
161 : 243 </button>
162 (<span style={{ textAlign: "center", display: "block" }}>No Discussions...</span>) 244 ) : (
163 ) 245 <span></span>
246 )}
247 <span>
248 <b>{e.creator.user_name}:</b> {e.content}
249 </span>
250 <span>
251 Last Updated:{" "}
252 {time_ago(
253 new Date(
254 e.updated_at.replace("T", " ").replace("Z", "")
255 )
256 )}
257 </span>
258 </button>
259 </div>
260 ))}
261 </>
262 ) : (
263 <span style={{ textAlign: "center", display: "block" }}>
264 No Discussions...
265 </span>
266 )
164 } 267 }
165 </section> 268 </section>
166 ); 269 );
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index d8879ef..b18c9d9 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -1,24 +1,24 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from '@customTypes/Game'; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css"
6 5
7interface GameCategoryProps { 6interface GameCategoryProps {
8 game: Game; 7 game: Game;
9 cat: GameCategoryPortals; 8 cat: GameCategoryPortals;
10} 9}
11 10
12const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { 11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
13 return ( 12 return (
14 <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> 13 <Link
15 <div> 14 className="bg-surface text-center w-full h-[100px] rounded-3xl text-foreground m-3 hover:bg-surface1 transition-colors flex flex-col justify-between p-4"
16 <span className='games-page-item-body-item-title'>{cat.category.name}</span> 15 to={"/games/" + game.id + "?cat=" + cat.category.id}
17 <br /> 16 >
18 <span className='games-page-item-body-item-num'>{cat.portal_count}</span> 17 <p className="text-3xl font-semibold">{cat.category.name}</p>
19 </div> 18 <br />
20 </Link> 19 <p className="font-bold text-4xl">{cat.portal_count}</p>
21 ) 20 </Link>
22} 21 );
22};
23 23
24export default GameCategory; 24export default GameCategory;
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index 3bd2842..f8fd179 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -1,10 +1,9 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from '@customTypes/Game'; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css"
6 5
7import GameCategory from '@components/GameCategory'; 6import GameCategory from "@components/GameCategory";
8 7
9interface GameEntryProps { 8interface GameEntryProps {
10 game: Game; 9 game: Game;
@@ -18,17 +17,28 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 }, [game.category_portals]); 17 }, [game.category_portals]);
19 18
20 return ( 19 return (
21 <Link to={"/games/" + game.id}><div className='games-page-item'> 20 <Link to={"/games/" + game.id} className="w-full">
22 <div className='games-page-item-header'> 21 <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6">
23 <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> 22 <div className="w-full h-1/2 bg-cover overflow-hidden relative">
24 <span><b>{game.name}</b></span> 23 <div
24 style={{ backgroundImage: `url(${game.image})` }}
25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover"
26 ></div>
27 <span className="absolute inset-0 flex justify-center items-center">
28 <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b>
29 </span>
30 </div>
31 <div className="flex justify-center items-center h-1/2">
32 <div className="flex flex-row justify-between w-full">
33 {catInfo.map((cat, index) => {
34 return (
35 <GameCategory key={index} cat={cat} game={game} />
36 );
37 })}
38 </div>
39 </div>
25 </div> 40 </div>
26 <div id={game.id as any as string} className='games-page-item-body'> 41 </Link>
27 {catInfo.map((cat, index) => {
28 return <GameCategory cat={cat} game={game} key={index}></GameCategory>
29 })}
30 </div>
31 </div></Link>
32 ); 42 );
33}; 43};
34 44
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index fb614fa..1de9b08 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -1,15 +1,15 @@
1import React from 'react'; 1import React, { useCallback } from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { DownloadIcon, ThreedotIcon } from '@images/Images'; 4import { DownloadIcon, ThreedotIcon } from "@images/Images";
5import { MapLeaderboard } from '@customTypes/Map'; 5import { MapLeaderboard } from "@customTypes/Map";
6import { ticks_to_time, time_ago } from '@utils/Time'; 6import { ticks_to_time, time_ago } from "@utils/Time";
7import { API } from "@api/Api"; 7import { API } from "@api/Api";
8import useMessage from "@hooks/UseMessage"; 8import useMessage from "@hooks/UseMessage";
9import "@css/Maps.css" 9import "@css/Maps.css";
10 10
11interface LeaderboardsProps { 11interface LeaderboardsProps {
12 mapID: string; 12 mapID: string;
13} 13}
14 14
15const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { 15const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
@@ -17,109 +17,228 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
17 const [data, setData] = React.useState<MapLeaderboard | undefined>(undefined); 17 const [data, setData] = React.useState<MapLeaderboard | undefined>(undefined);
18 const [pageNumber, setPageNumber] = React.useState<number>(1); 18 const [pageNumber, setPageNumber] = React.useState<number>(1);
19 19
20 const _fetch_map_leaderboards = async () => { 20 const _fetch_map_leaderboards = useCallback(async () => {
21 const mapLeaderboards = await API.get_map_leaderboard(mapID, pageNumber.toString()); 21 const mapLeaderboards = await API.get_map_leaderboard(
22 mapID,
23 pageNumber.toString()
24 );
22 setData(mapLeaderboards); 25 setData(mapLeaderboards);
23 }; 26 }, [mapID, pageNumber]);
24 27
25 const { message, MessageDialogComponent } = useMessage(); 28 const { message, MessageDialogComponent } = useMessage();
26 29
27 React.useEffect(() => { 30 React.useEffect(() => {
28 _fetch_map_leaderboards(); 31 _fetch_map_leaderboards();
29 console.log(data); 32 console.log(data);
30 }, [pageNumber, navigate]) 33 }, [pageNumber, navigate, _fetch_map_leaderboards, data]);
31 34
32 if (!data) { 35 if (!data) {
33 return ( 36 return (
34 <section id='section6' className='summary2'> 37 <section id="section6" className="summary2">
35 <h1 style={{ textAlign: "center" }}>Map is not available for competitive boards.</h1> 38 <h1 style={{ textAlign: "center" }}>
39 Loading...
40 </h1>
36 </section> 41 </section>
37 ); 42 );
38 }; 43 }
39 44
40 if (data.records.length === 0) { 45 if (data.records.length === 0) {
41 return ( 46 return (
42 <section id='section6' className='summary2'> 47 <section id="section6" className="summary2">
43 <h1 style={{ textAlign: "center" }}>No records found.</h1> 48 <h1 style={{ textAlign: "center" }}>No records found.</h1>
44 </section> 49 </section>
45 ); 50 );
46 }; 51 }
47 52
48 return ( 53 return (
49 <div> 54 <div className="text-foreground">
50 {MessageDialogComponent} 55 {MessageDialogComponent}
51 <section id='section6' className='summary2'> 56 <section id="section6" className="summary2">
52 57 <div
53 <div id='leaderboard-top' 58 id="leaderboard-top"
54 style={data.map.is_coop ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }} 59 style={
55 > 60 data.map.is_coop
56 <span>Place</span> 61 ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" }
57 62 : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" }
58 {data.map.is_coop ? ( 63 }
59 <div id='runner'> 64 >
60 <span>Blue</span> 65 <span>Place</span>
61 <span>Orange</span> 66
62 </div> 67 {data.map.is_coop ? (
63 ) : ( 68 <div id="runner">
64 <span>Runner</span> 69 <span>Blue</span>
65 )} 70 <span>Orange</span>
66 71 </div>
67 <span>Portals</span> 72 ) : (
68 <span>Time</span> 73 <span>Runner</span>
69 <span>Date</span> 74 )}
70 <div id='page-number'> 75
71 <div> 76 <span>Portals</span>
72 77 <span>Time</span>
73 <button onClick={() => pageNumber === 1 ? null : setPageNumber(prevPageNumber => prevPageNumber - 1)} 78 <span>Date</span>
74 ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> 79 <div id="page-number">
75 <span>{data.pagination.current_page}/{data.pagination.total_pages}</span> 80 <div>
76 <button onClick={() => pageNumber === data.pagination.total_pages ? null : setPageNumber(prevPageNumber => prevPageNumber + 1)} 81 <button
77 ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> 82 onClick={() =>
83 pageNumber === 1
84 ? null
85 : setPageNumber(prevPageNumber => prevPageNumber - 1)
86 }
87 >
88 <i
89 className="triangle"
90 style={{ position: "relative", left: "-5px" }}
91 ></i>{" "}
92 </button>
93 <span>
94 {data.pagination.current_page}/{data.pagination.total_pages}
95 </span>
96 <button
97 onClick={() =>
98 pageNumber === data.pagination.total_pages
99 ? null
100 : setPageNumber(prevPageNumber => prevPageNumber + 1)
101 }
102 >
103 <i
104 className="triangle"
105 style={{
106 position: "relative",
107 left: "5px",
108 transform: "rotate(180deg)",
109 }}
110 ></i>{" "}
111 </button>
112 </div>
78 </div> 113 </div>
79 </div> 114 </div>
80 </div> 115 <hr />
81 <hr /> 116 <div id="leaderboard-records">
82 <div id='leaderboard-records'> 117 {data.records.map((r, index) => (
83 {data.records.map((r, index) => ( 118 <span
84 <span className='leaderboard-record' key={index} 119 className="leaderboard-record"
85 style={data.map.is_coop ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }} 120 key={index}
86 > 121 style={
87 <span>{r.placement}</span> 122 data.map.is_coop
88 <span> </span> 123 ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" }
89 {r.kind === "multiplayer" ? ( 124 : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" }
90 <div> 125 }
91 <Link to={`/users/${r.host.steam_id}`}><span><img src={r.host.avatar_link} alt='' /> &nbsp; {r.host.user_name}</span></Link> 126 >
92 <Link to={`/users/${r.partner.steam_id}`}><span><img src={r.partner.avatar_link} alt='' /> &nbsp; {r.partner.user_name}</span></Link> 127 <span>{r.placement}</span>
93 </div> 128 <span> </span>
94 ) : r.kind === "singleplayer" && ( 129 {r.kind === "multiplayer" ? (
95 <div> 130 <div>
96 <Link to={`/users/${r.user.steam_id}`}><span><img src={r.user.avatar_link} alt='' /> &nbsp; {r.user.user_name}</span></Link> 131 <Link to={`/users/${r.host.steam_id}`}>
97 </div> 132 <span>
98 )} 133 <img src={r.host.avatar_link} alt="" /> &nbsp;{" "}
99 134 {r.host.user_name}
100 <span>{r.score_count}</span> 135 </span>
101 <span> </span> 136 </Link>
102 <span className='hover-popup' popup-text={(r.score_time) + " ticks"}>{ticks_to_time(r.score_time)}</span> 137 <Link to={`/users/${r.partner.steam_id}`}>
103 <span className='hover-popup' popup-text={r.record_date.replace("T", ' ').split(".")[0]}>{time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))}</span> 138 <span>
104 139 <img src={r.partner.avatar_link} alt="" /> &nbsp;{" "}
105 {r.kind === "multiplayer" ? ( 140 {r.partner.user_name}
106 <span> 141 </span>
107 <button onClick={() => { message("Demo Information", `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 142 </Link>
108 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(160deg) contrast(60%) saturate(1000%)" }} /></button> 143 </div>
109 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`}><img src={DownloadIcon} alt="download" style={{ filter: "hue-rotate(300deg) contrast(60%) saturate(1000%)" }} /></button> 144 ) : (
145 r.kind === "singleplayer" && (
146 <div>
147 <Link to={`/users/${r.user.steam_id}`}>
148 <span>
149 <img src={r.user.avatar_link} alt="" /> &nbsp;{" "}
150 {r.user.user_name}
151 </span>
152 </Link>
153 </div>
154 )
155 )}
156
157 <span>{r.score_count}</span>
158 <span> </span>
159 <span
160 className="hover-popup"
161 popup-text={r.score_time + " ticks"}
162 >
163 {ticks_to_time(r.score_time)}
110 </span> 164 </span>
111 ) : r.kind === "singleplayer" && ( 165 <span
112 166 className="hover-popup"
113 <span> 167 popup-text={r.record_date.replace("T", " ").split(".")[0]}
114 <button onClick={() => { message("Demo Information", `Demo ID: ${r.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> 168 >
115 <button onClick={() => window.location.href = `/api/v1/demos?uuid=${r.demo_id}`}><img src={DownloadIcon} alt="download" /></button> 169 {time_ago(
170 new Date(r.record_date.replace("T", " ").replace("Z", ""))
171 )}
116 </span> 172 </span>
117 )} 173
118 </span> 174 {r.kind === "multiplayer" ? (
119 ))} 175 <span>
120 </div> 176 <button
121 </section> 177 onClick={() => {
122 </div> 178 message(
179 "Demo Information",
180 `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}`
181 );
182 }}
183 >
184 <img src={ThreedotIcon} alt="demo_id" />
185 </button>
186 <button
187 onClick={() =>
188 (window.location.href = `/api/v1/demos?uuid=${r.partner_demo_id}`)
189 }
190 >
191 <img
192 src={DownloadIcon}
193 alt="download"
194 style={{
195 filter:
196 "hue-rotate(160deg) contrast(60%) saturate(1000%)",
197 }}
198 className="w-6 h-6 mx-4"
199 />
200 </button>
201 <button
202 onClick={() =>
203 (window.location.href = `/api/v1/demos?uuid=${r.host_demo_id}`)
204 }
205 >
206 <img
207 src={DownloadIcon}
208 alt="download"
209 style={{
210 filter:
211 "hue-rotate(300deg) contrast(60%) saturate(1000%)",
212 }}
213 className="w-6 h-6"
214 />
215 </button>
216 </span>
217 ) : (
218 r.kind === "singleplayer" && (
219 <span>
220 <button
221 onClick={() => {
222 message("Demo Information", `Demo ID: ${r.demo_id}`);
223 }}
224 >
225 <img src={ThreedotIcon} alt="demo_id" />
226 </button>
227 <button
228 onClick={() =>
229 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`)
230 }
231 >
232 <img src={DownloadIcon} alt="download" className="w-6 h-6 mr-4" />
233 </button>
234 </span>
235 )
236 )}
237 </span>
238 ))}
239 </div>
240 </section>
241 </div>
123 ); 242 );
124}; 243};
125 244
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index f1628b2..ba85aeb 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -1,19 +1,18 @@
1import React from 'react'; 1import React from "react";
2import { Link, useNavigate } from 'react-router-dom'; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { ExitIcon, UserIcon, LoginIcon } from '@images/Images'; 4import { ExitIcon, UserIcon, LoginIcon } from "../images/Images";
5import { UserProfile } from '@customTypes/Profile'; 5import { UserProfile } from "@customTypes/Profile";
6import { API } from '@api/Api'; 6import { API } from "@api/Api";
7import "@css/Login.css";
8 7
9interface LoginProps { 8interface LoginProps {
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 9 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 10 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 11 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
13}; 12 isOpen: boolean;
14 13}
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
16 14
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile, isOpen }) => {
17 const navigate = useNavigate(); 16 const navigate = useNavigate();
18 17
19 const _login = () => { 18 const _login = () => {
@@ -29,52 +28,71 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
29 28
30 return ( 29 return (
31 <> 30 <>
32 {profile 31 {profile ? (
33 ? 32 <>
34 ( 33 {profile.profile ? (
35 <> 34 <>
36 {profile.profile ? 35 <Link to="/profile" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
37 ( 36 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
38 <> 37 <img
39 <Link to="/profile" tabIndex={-1} className='login'> 38 className="rounded-[50px]"
40 <button className='sidebar-button'> 39 src={profile.avatar_link}
41 <img className="avatar-img" src={profile.avatar_link} alt="" /> 40 alt=""
42 <span>{profile.user_name}</span> 41 />
43 </button> 42 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">{profile.user_name}</span>
44 <button className='logout-button' onClick={_logout}> 43 </button>
45 <img src={ExitIcon} alt="" /><span /> 44 <button className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent" onClick={_logout}>
46 </button> 45 <img src={ExitIcon} alt="" />
47 </Link> 46 <span />
48 </> 47 </button>
49 ) 48 </Link>
50 : 49 </>
51 ( 50 ) : (
52 <> 51 <>
53 <Link to="/" tabIndex={-1} className='login'> 52 <Link to="/" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
54 <button className='sidebar-button'> 53 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
55 <img className="avatar-img" src={profile.avatar_link} alt="" /> 54 <img
56 <span>Loading Profile...</span> 55 className="rounded-[50px]"
57 </button> 56 src={profile.avatar_link}
58 <button disabled className='logout-button' onClick={_logout}> 57 alt=""
59 <img src={ExitIcon} alt="" /><span /> 58 />
60 </button> 59 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">Loading Profile...</span>
61 </Link> 60 </button>
62 </> 61 <button disabled className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent hidden" onClick={_logout}>
63 ) 62 <img src={ExitIcon} alt="" />
64 } 63 <span />
65 </> 64 </button>
66 ) 65 </Link>
67 : 66 </>
68 ( 67 )}
69 <Link to="/api/v1/login" tabIndex={-1} className='login' > 68 </>
70 <button className='sidebar-button' onClick={_login}> 69 ) : (
71 <img className="avatar-img" src={UserIcon} alt="" /> 70 <Link to="/api/v1/login" tabIndex={-1}>
72 <span> 71 <button
73 <img src={LoginIcon} alt="Sign in through Steam" /> 72 className={`${
74 </span> 73 isOpen
75 </button> 74 ? "grid grid-cols-[50px_auto] place-items-start pl-[11px]"
76 </Link> 75 : "flex items-center justify-center"
77 )} 76 } text-left bg-inherit cursor-pointer border-none w-[310px] h-16 rounded-[20px] py-[0.3em] px-0 transition-all duration-300 ${isOpen ? "text-white" : "text-gray-400"}`}
77 onClick={_login}
78 >
79 <span className={`font-[--font-barlow-semicondensed-regular] text-lg h-12 leading-7 transition-opacity duration-100 ${isOpen ? " overflow-hidden" : ""}`}>
80 {isOpen ? (
81 <div className="bg-neutral-800 p-2 rounded-lg w-64 flex flex-row items-center justifyt-start gap-2 font-semibold">
82 <LoginIcon />
83 <span>
84 Login with Steam
85 </span>
86 </div>
87 ) : (
88 <div className="bg-neutral-800 p-2 rounded-lg w-">
89 <LoginIcon />
90 </div>
91 )}
92 </span>
93 </button>
94 </Link>
95 )}
78 </> 96 </>
79 ); 97 );
80}; 98};
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx
index 0f494ad..985e806 100644
--- a/frontend/src/components/MapEntry.tsx
+++ b/frontend/src/components/MapEntry.tsx
@@ -1,12 +1,8 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4const MapEntry: React.FC = () => { 4const MapEntry: React.FC = () => {
5 return ( 5 return <div></div>;
6 <div> 6};
7
8 </div>
9 )
10}
11 7
12export default MapEntry; 8export default MapEntry;
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx
index 5c85189..fcf4d8d 100644
--- a/frontend/src/components/MessageDialog.tsx
+++ b/frontend/src/components/MessageDialog.tsx
@@ -1,29 +1,33 @@
1import React from 'react'; 1import React from "react";
2 2
3import "@css/Dialog.css" 3import "@css/Dialog.css";
4 4
5interface MessageDialogProps { 5interface MessageDialogProps {
6 title: string; 6 title: string;
7 subtitle: string; 7 subtitle: string;
8 onClose: () => void; 8 onClose: () => void;
9}; 9}
10 10
11const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { 11const MessageDialog: React.FC<MessageDialogProps> = ({
12 return ( 12 title,
13 <div className='dimmer'> 13 subtitle,
14 <div className='dialog'> 14 onClose,
15 <div className='dialog-element dialog-header'> 15}) => {
16 <span>{title}</span> 16 return (
17 </div> 17 <div className="dimmer">
18 <div className='dialog-element dialog-description'> 18 <div className="dialog">
19 <span>{subtitle}</span> 19 <div className="dialog-element dialog-header">
20 </div> 20 <span>{title}</span>
21 <div className='dialog-element dialog-btns-container'>
22 <button onClick={onClose}>Close</button>
23 </div>
24 </div>
25 </div> 21 </div>
26 ) 22 <div className="dialog-element dialog-description">
27} 23 <span>{subtitle}</span>
24 </div>
25 <div className="dialog-element dialog-btns-container">
26 <button onClick={onClose}>Close</button>
27 </div>
28 </div>
29 </div>
30 );
31};
28 32
29export default MessageDialog; 33export default MessageDialog;
diff --git a/frontend/src/components/MessageDialogLoad.tsx b/frontend/src/components/MessageDialogLoad.tsx
index 966e064..64cdd29 100644
--- a/frontend/src/components/MessageDialogLoad.tsx
+++ b/frontend/src/components/MessageDialogLoad.tsx
@@ -1,29 +1,31 @@
1import React from 'react'; 1import React from "react";
2 2
3import "@css/Dialog.css" 3import "@css/Dialog.css";
4 4
5interface MessageDialogLoadProps { 5interface MessageDialogLoadProps {
6 title: string; 6 title: string;
7 onClose: () => void; 7 onClose: () => void;
8}; 8}
9 9
10const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { 10const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({
11 return ( 11 title,
12 <div className='dimmer'> 12 onClose,
13 <div className='dialog'> 13}) => {
14 <div className='dialog-element dialog-header'> 14 return (
15 <span>{title}</span> 15 <div className="dimmer">
16 </div> 16 <div className="dialog">
17 <div className='dialog-element dialog-description'> 17 <div className="dialog-element dialog-header">
18 <div style={{display: "flex", justifyContent: "center"}}> 18 <span>{title}</span>
19 <span className="loader"></span>
20 </div>
21 </div>
22 <div className='dialog-element dialog-btns-container'>
23 </div>
24 </div>
25 </div> 19 </div>
26 ) 20 <div className="dialog-element dialog-description">
27} 21 <div style={{ display: "flex", justifyContent: "center" }}>
22 <span className="loader"></span>
23 </div>
24 </div>
25 <div className="dialog-element dialog-btns-container"></div>
26 </div>
27 </div>
28 );
29};
28 30
29export default MessageDialogLoad; 31export default MessageDialogLoad;
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx
index 925b8a8..a0d7eb7 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -1,12 +1,11 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3import { useNavigate } from 'react-router-dom'; 3import { useNavigate } from "react-router-dom";
4 4
5import { MapSummary } from '@customTypes/Map'; 5import { MapSummary } from "@customTypes/Map";
6import { ModMenuContent } from '@customTypes/Content'; 6import { ModMenuContent } from "@customTypes/Content";
7import { API } from '@api/Api'; 7import { API } from "@api/Api";
8import "@css/ModMenu.css" 8import useConfirm from "@hooks/UseConfirm";
9import useConfirm from '@hooks/UseConfirm';
10 9
11interface ModMenuProps { 10interface ModMenuProps {
12 token?: string; 11 token?: string;
@@ -15,8 +14,12 @@ interface ModMenuProps {
15 mapID: string; 14 mapID: string;
16} 15}
17 16
18const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) => { 17const ModMenu: React.FC<ModMenuProps> = ({
19 18 token,
19 data,
20 selectedRun,
21 mapID,
22}) => {
20 const { confirm, ConfirmDialogComponent } = useConfirm(); 23 const { confirm, ConfirmDialogComponent } = useConfirm();
21 24
22 const [menu, setMenu] = React.useState<number>(0); 25 const [menu, setMenu] = React.useState<number>(0);
@@ -55,74 +58,99 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
55 width *= 320 / height; 58 width *= 320 / height;
56 height = 320; 59 height = 320;
57 } 60 }
58 const canvas = document.createElement('canvas'); 61 const canvas = document.createElement("canvas");
59 canvas.width = width; 62 canvas.width = width;
60 canvas.height = height; 63 canvas.height = height;
61 canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); 64 canvas.getContext("2d")!.drawImage(img, 0, 0, width, height);
62 resolve(canvas.toDataURL(file.type, 0.6)); 65 resolve(canvas.toDataURL(file.type, 0.6));
63 }; 66 };
64 } 67 }
65 }; 68 };
66 }); 69 });
67 }; 70 }
68 71
69 const _edit_map_summary_image = async () => { 72 const _edit_map_summary_image = async () => {
70 if (await confirm("Edit Map Summary Image", "Are you sure you want to submit this to the database?")) { 73 if (
74 await confirm(
75 "Edit Map Summary Image",
76 "Are you sure you want to submit this to the database?"
77 )
78 ) {
71 if (token) { 79 if (token) {
72 const success = await API.put_map_image(token, mapID, image); 80 const success = await API.put_map_image(token, mapID, image);
73 if (success) { 81 if (success) {
74 navigate(0); 82 navigate(0);
75 } else { 83 } else {
76 alert("Error. Check logs.") 84 alert("Error. Check logs.");
77 } 85 }
78 } 86 }
79 } 87 }
80 }; 88 };
81 89
82 const _edit_map_summary_route = async () => { 90 const _edit_map_summary_route = async () => {
83 if (await confirm("Edit Map Summary Route", "Are you sure you want to submit this to the database?")) { 91 if (
92 await confirm(
93 "Edit Map Summary Route",
94 "Are you sure you want to submit this to the database?"
95 )
96 ) {
84 if (token) { 97 if (token) {
85 routeContent.date += "T00:00:00Z"; 98 routeContent.date += "T00:00:00Z";
86 const success = await API.put_map_summary(token, mapID, routeContent); 99 const success = await API.put_map_summary(token, mapID, routeContent);
87 if (success) { 100 if (success) {
88 navigate(0); 101 navigate(0);
89 } else { 102 } else {
90 alert("Error. Check logs.") 103 alert("Error. Check logs.");
91 } 104 }
92 } 105 }
93 } 106 }
94 }; 107 };
95 108
96 const _create_map_summary_route = async () => { 109 const _create_map_summary_route = async () => {
97 if (await confirm("Create Map Summary Route", "Are you sure you want to submit this to the database?")) { 110 if (
111 await confirm(
112 "Create Map Summary Route",
113 "Are you sure you want to submit this to the database?"
114 )
115 ) {
98 if (token) { 116 if (token) {
99 routeContent.date += "T00:00:00Z"; 117 routeContent.date += "T00:00:00Z";
100 const success = await API.post_map_summary(token, mapID, routeContent); 118 const success = await API.post_map_summary(token, mapID, routeContent);
101 if (success) { 119 if (success) {
102 navigate(0); 120 navigate(0);
103 } else { 121 } else {
104 alert("Error. Check logs.") 122 alert("Error. Check logs.");
105 } 123 }
106 } 124 }
107 } 125 }
108 }; 126 };
109 127
110 const _delete_map_summary_route = async () => { 128 const _delete_map_summary_route = async () => {
111 if (await confirm("Delete Map Summary Route", `Are you sure you want to submit this to the database?\n 129 if (
112 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}`)) { 130 await confirm(
131 "Delete Map Summary Route",
132 `Are you sure you want to submit this to the database?\n
133 ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}`
134 )
135 ) {
113 if (token) { 136 if (token) {
114 const success = await API.delete_map_summary(token, mapID, data.summary.routes[selectedRun].route_id); 137 const success = await API.delete_map_summary(
138 token,
139 mapID,
140 data.summary.routes[selectedRun].route_id
141 );
115 if (success) { 142 if (success) {
116 navigate(0); 143 navigate(0);
117 } else { 144 } else {
118 alert("Error. Check logs.") 145 alert("Error. Check logs.");
119 } 146 }
120 } 147 }
121 } 148 }
122 }; 149 };
123 150
124 React.useEffect(() => { 151 React.useEffect(() => {
125 if (menu === 3) { // add route 152 if (menu === 3) {
153 // add route
126 setRouteContent({ 154 setRouteContent({
127 id: 0, 155 id: 0,
128 name: "", 156 name: "",
@@ -134,7 +162,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
134 }); 162 });
135 setMd("No description available."); 163 setMd("No description available.");
136 } 164 }
137 if (menu === 2) { // edit route 165 if (menu === 2) {
166 // edit route
138 setRouteContent({ 167 setRouteContent({
139 id: data.summary.routes[selectedRun].route_id, 168 id: data.summary.routes[selectedRun].route_id,
140 name: data.summary.routes[selectedRun].history.runner_name, 169 name: data.summary.routes[selectedRun].history.runner_name,
@@ -146,207 +175,335 @@ const ModMenu: React.FC<ModMenuProps> = ({ token, data, selectedRun, mapID }) =>
146 }); 175 });
147 setMd(data.summary.routes[selectedRun].description); 176 setMd(data.summary.routes[selectedRun].description);
148 } 177 }
149 }, [menu]); 178 }, [menu, data.summary.routes, selectedRun]);
150 179
151 React.useEffect(() => { 180 React.useEffect(() => {
152 const modview = document.querySelector("div#modview") as HTMLElement 181 const modview = document.querySelector("div#modview") as HTMLElement;
153 if (modview) { 182 if (modview) {
154 showButton ? modview.style.transform = "translateY(-68%)" 183 showButton
155 : modview.style.transform = "translateY(0%)" 184 ? (modview.style.transform = "translateY(-68%)")
185 : (modview.style.transform = "translateY(0%)");
156 } 186 }
157 187
158 const modview_block = document.querySelector("#modview_block") as HTMLElement 188 const modview_block = document.querySelector(
189 "#modview_block"
190 ) as HTMLElement;
159 if (modview_block) { 191 if (modview_block) {
160 showButton ? modview_block.style.display = "none" : modview_block.style.display = "block" 192 showButton
193 ? (modview_block.style.display = "none")
194 : (modview_block.style.display = "block");
161 } 195 }
162 }, [showButton]) 196 }, [showButton]);
163 197
164 return ( 198 return (
165 <> 199 <>
166 {ConfirmDialogComponent} 200 {ConfirmDialogComponent}
167 <div id="modview_block" /> 201 <div id="modview_block" />
168 <div id='modview'> 202 <div id="modview">
169 <div> 203 <div>
170 <button onClick={() => setMenu(1)}>Edit Image</button> 204 <button onClick={() => setMenu(1)}>Edit Image</button>
171 <button onClick={() => setMenu(2)}>Edit Selected Route</button> 205 <button onClick={() => setMenu(2)}>Edit Selected Route</button>
172 <button onClick={() => setMenu(3)}>Add New Route</button> 206 <button onClick={() => setMenu(3)}>Add New Route</button>
173 <button onClick={() => _delete_map_summary_route()}>Delete Selected Route</button> 207 <button onClick={() => _delete_map_summary_route()}>
208 Delete Selected Route
209 </button>
174 </div> 210 </div>
175 <div> 211 <div>
176 {showButton ? ( 212 {showButton ? (
177 <button onClick={() => setShowButton(false)}>Show</button> 213 <button onClick={() => setShowButton(false)}>Show</button>
178 ) : ( 214 ) : (
179 <button onClick={() => { setShowButton(true); setMenu(0); }}>Hide</button> 215 <button
216 onClick={() => {
217 setShowButton(true);
218 setMenu(0);
219 }}
220 >
221 Hide
222 </button>
180 )} 223 )}
181 </div> 224 </div>
182 </div><div id='modview-menu'> 225 </div>
183 {// Edit Image 226 <div id="modview-menu">
227 {
228 // Edit Image
184 menu === 1 && ( 229 menu === 1 && (
185 <div id='modview-menu-image'> 230 <div id="modview-menu-image">
186 <div> 231 <div>
187 <span>Current Image:</span> 232 <span>Current Image:</span>
188 <img src={data.map.image} alt="missing" /> 233 <img src={data.map.image} alt="missing" />
189 </div> 234 </div>
190 235
191 <div> 236 <div>
192 <span>New Image: 237 <span>
193 <input type="file" accept='image/*' onChange={e => { 238 New Image:
194 if (e.target.files) { 239 <input
195 compressImage(e.target.files[0]) 240 type="file"
196 .then(d => setImage(d)); 241 accept="image/*"
197 } 242 onChange={e => {
198 }} /></span> 243 if (e.target.files) {
199 {image ? (<button onClick={() => _edit_map_summary_image()}>upload</button>) : <span></span>} 244 compressImage(e.target.files[0]).then(d => setImage(d));
200 <img src={image} alt="" id='modview-menu-image-file' /> 245 }
201 246 }}
247 />
248 </span>
249 {image ? (
250 <button onClick={() => _edit_map_summary_image()}>
251 upload
252 </button>
253 ) : (
254 <span></span>
255 )}
256 <img src={image} alt="" id="modview-menu-image-file" />
202 </div> 257 </div>
203 </div> 258 </div>
204 )} 259 )
260 }
205 261
206 {// Edit Route 262 {
263 // Edit Route
207 menu === 2 && ( 264 menu === 2 && (
208 <div id='modview-menu-edit'> 265 <div id="modview-menu-edit">
209 <div id='modview-route-id'> 266 <div id="modview-route-id">
210 <span>Route ID:</span> 267 <span>Route ID:</span>
211 <input type="number" value={routeContent.id} disabled /> 268 <input type="number" value={routeContent.id} disabled />
212 </div> 269 </div>
213 <div id='modview-route-name'> 270 <div id="modview-route-name">
214 <span>Runner Name:</span> 271 <span>Runner Name:</span>
215 <input type="text" value={routeContent.name} onChange={(e) => { 272 <input
216 setRouteContent({ 273 type="text"
217 ...routeContent, 274 value={routeContent.name}
218 name: e.target.value, 275 onChange={e => {
219 }); 276 setRouteContent({
220 }} /> 277 ...routeContent,
278 name: e.target.value,
279 });
280 }}
281 />
221 </div> 282 </div>
222 <div id='modview-route-score'> 283 <div id="modview-route-score">
223 <span>Score:</span> 284 <span>Score:</span>
224 <input type="number" value={routeContent.score} onChange={(e) => { 285 <input
225 setRouteContent({ 286 type="number"
226 ...routeContent, 287 value={routeContent.score}
227 score: parseInt(e.target.value), 288 onChange={e => {
228 }); 289 setRouteContent({
229 }} /> 290 ...routeContent,
291 score: parseInt(e.target.value),
292 });
293 }}
294 />
230 </div> 295 </div>
231 <div id='modview-route-date'> 296 <div id="modview-route-date">
232 <span>Date:</span> 297 <span>Date:</span>
233 <input type="date" value={routeContent.date} onChange={(e) => { 298 <input
234 setRouteContent({ 299 type="date"
235 ...routeContent, 300 value={routeContent.date}
236 date: e.target.value, 301 onChange={e => {
237 }); 302 setRouteContent({
238 }} /> 303 ...routeContent,
304 date: e.target.value,
305 });
306 }}
307 />
239 </div> 308 </div>
240 <div id='modview-route-showcase'> 309 <div id="modview-route-showcase">
241 <span>Showcase Video:</span> 310 <span>Showcase Video:</span>
242 <input type="text" value={routeContent.showcase} onChange={(e) => { 311 <input
243 setRouteContent({ 312 type="text"
244 ...routeContent, 313 value={routeContent.showcase}
245 showcase: e.target.value, 314 onChange={e => {
246 }); 315 setRouteContent({
247 }} /> 316 ...routeContent,
317 showcase: e.target.value,
318 });
319 }}
320 />
248 </div> 321 </div>
249 <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> 322 <div
323 id="modview-route-description"
324 style={{ height: "180px", gridColumn: "1 / span 5" }}
325 >
250 <span>Description:</span> 326 <span>Description:</span>
251 <textarea value={routeContent.description} onChange={(e) => { 327 <textarea
252 setRouteContent({ 328 value={routeContent.description}
253 ...routeContent, 329 onChange={e => {
254 description: e.target.value, 330 setRouteContent({
255 }); 331 ...routeContent,
256 setMd(routeContent.description); 332 description: e.target.value,
257 }} /> 333 });
334 setMd(routeContent.description);
335 }}
336 />
258 </div> 337 </div>
259 <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_edit_map_summary_route}>Apply</button> 338 <button
339 style={{ gridColumn: "2 / span 3", height: "40px" }}
340 onClick={_edit_map_summary_route}
341 >
342 Apply
343 </button>
260 344
261 <div id='modview-md'> 345 <div id="modview-md">
262 <span>Markdown Preview</span> 346 <span>Markdown Preview</span>
263 <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>Documentation</a></span> 347 <span>
264 <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>Demo</a></span> 348 <a
349 href="https://commonmark.org/help/"
350 rel="noreferrer"
351 target="_blank"
352 >
353 Documentation
354 </a>
355 </span>
356 <span>
357 <a
358 href="https://remarkjs.github.io/react-markdown/"
359 rel="noreferrer"
360 target="_blank"
361 >
362 Demo
363 </a>
364 </span>
265 <p> 365 <p>
266 <ReactMarkdown>{md} 366 <ReactMarkdown>{md}</ReactMarkdown>
267 </ReactMarkdown>
268 </p> 367 </p>
269 </div> 368 </div>
270 </div> 369 </div>
271 )} 370 )
371 }
272 372
273 {// Add Route 373 {
374 // Add Route
274 menu === 3 && ( 375 menu === 3 && (
275 <div id='modview-menu-add'> 376 <div id="modview-menu-add">
276 <div id='modview-route-category'> 377 <div id="modview-route-category">
277 <span>Category:</span> 378 <span>Category:</span>
278 <select onChange={(e) => { 379 <select
279 setRouteContent({ 380 onChange={e => {
280 ...routeContent, 381 setRouteContent({
281 category_id: parseInt(e.target.value), 382 ...routeContent,
282 }); 383 category_id: parseInt(e.target.value),
283 }}> 384 });
284 <option value="1" key="1">CM</option> 385 }}
285 <option value="2" key="2">No SLA</option> 386 >
286 {data.map.game_name === "Portal 2 - Cooperative" ? "" : ( 387 <option value="1" key="1">
287 <option value="3" key="3">Inbounds SLA</option>)} 388 CM
288 <option value="4" key="4">Any%</option> 389 </option>
390 <option value="2" key="2">
391 No SLA
392 </option>
393 {data.map.game_name === "Portal 2 - Cooperative" ? (
394 ""
395 ) : (
396 <option value="3" key="3">
397 Inbounds SLA
398 </option>
399 )}
400 <option value="4" key="4">
401 Any%
402 </option>
289 </select> 403 </select>
290 </div> 404 </div>
291 <div id='modview-route-name'> 405 <div id="modview-route-name">
292 <span>Runner Name:</span> 406 <span>Runner Name:</span>
293 <input type="text" value={routeContent.name} onChange={(e) => { 407 <input
294 setRouteContent({ 408 type="text"
295 ...routeContent, 409 value={routeContent.name}
296 name: e.target.value, 410 onChange={e => {
297 }); 411 setRouteContent({
298 }} /> 412 ...routeContent,
413 name: e.target.value,
414 });
415 }}
416 />
299 </div> 417 </div>
300 <div id='modview-route-score'> 418 <div id="modview-route-score">
301 <span>Score:</span> 419 <span>Score:</span>
302 <input type="number" value={routeContent.score} onChange={(e) => { 420 <input
303 setRouteContent({ 421 type="number"
304 ...routeContent, 422 value={routeContent.score}
305 score: parseInt(e.target.value), 423 onChange={e => {
306 }); 424 setRouteContent({
307 }} /> 425 ...routeContent,
426 score: parseInt(e.target.value),
427 });
428 }}
429 />
308 </div> 430 </div>
309 <div id='modview-route-date'> 431 <div id="modview-route-date">
310 <span>Date:</span> 432 <span>Date:</span>
311 <input type="date" value={routeContent.date} onChange={(e) => { 433 <input
312 setRouteContent({ 434 type="date"
313 ...routeContent, 435 value={routeContent.date}
314 date: e.target.value, 436 onChange={e => {
315 }); 437 setRouteContent({
316 }} /> 438 ...routeContent,
439 date: e.target.value,
440 });
441 }}
442 />
317 </div> 443 </div>
318 <div id='modview-route-showcase'> 444 <div id="modview-route-showcase">
319 <span>Showcase Video:</span> 445 <span>Showcase Video:</span>
320 <input type="text" value={routeContent.showcase} onChange={(e) => { 446 <input
321 setRouteContent({ 447 type="text"
322 ...routeContent, 448 value={routeContent.showcase}
323 showcase: e.target.value, 449 onChange={e => {
324 }); 450 setRouteContent({
325 }} /> 451 ...routeContent,
452 showcase: e.target.value,
453 });
454 }}
455 />
326 </div> 456 </div>
327 <div id='modview-route-description' style={{ height: "180px", gridColumn: "1 / span 5" }}> 457 <div
458 id="modview-route-description"
459 style={{ height: "180px", gridColumn: "1 / span 5" }}
460 >
328 <span>Description:</span> 461 <span>Description:</span>
329 <textarea value={routeContent.description} onChange={(e) => { 462 <textarea
330 setRouteContent({ 463 value={routeContent.description}
331 ...routeContent, 464 onChange={e => {
332 description: e.target.value, 465 setRouteContent({
333 }); 466 ...routeContent,
334 setMd(routeContent.description); 467 description: e.target.value,
335 }} /> 468 });
469 setMd(routeContent.description);
470 }}
471 />
336 </div> 472 </div>
337 <button style={{ gridColumn: "2 / span 3", height: "40px" }} onClick={_create_map_summary_route}>Apply</button> 473 <button
474 style={{ gridColumn: "2 / span 3", height: "40px" }}
475 onClick={_create_map_summary_route}
476 >
477 Apply
478 </button>
338 479
339 <div id='modview-md'> 480 <div id="modview-md">
340 <span>Markdown preview</span> 481 <span>Markdown preview</span>
341 <span><a href="https://commonmark.org/help/" rel="noreferrer" target='_blank'>documentation</a></span> 482 <span>
342 <span><a href="https://remarkjs.github.io/react-markdown/" rel="noreferrer" target='_blank'>demo</a></span> 483 <a
484 href="https://commonmark.org/help/"
485 rel="noreferrer"
486 target="_blank"
487 >
488 documentation
489 </a>
490 </span>
491 <span>
492 <a
493 href="https://remarkjs.github.io/react-markdown/"
494 rel="noreferrer"
495 target="_blank"
496 >
497 demo
498 </a>
499 </span>
343 <p> 500 <p>
344 <ReactMarkdown>{md} 501 <ReactMarkdown>{md}</ReactMarkdown>
345 </ReactMarkdown>
346 </p> 502 </p>
347 </div> 503 </div>
348 </div> 504 </div>
349 )} 505 )
506 }
350 </div> 507 </div>
351 </> 508 </>
352 ); 509 );
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx
index b899965..f28eabf 100644
--- a/frontend/src/components/RankingEntry.tsx
+++ b/frontend/src/components/RankingEntry.tsx
@@ -1,46 +1,65 @@
1import React from 'react'; 1import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3import { RankingType, SteamRanking, SteamRankingType } from '@customTypes/Ranking'; 3import { RankingType, SteamRankingType } from "@customTypes/Ranking";
4 4
5enum RankingCategories { 5enum RankingCategories {
6 rankings_overall, 6 rankings_overall,
7 rankings_multiplayer, 7 rankings_multiplayer,
8 rankings_singleplayer 8 rankings_singleplayer,
9} 9}
10 10
11interface RankingEntryProps { 11interface RankingEntryProps {
12 curRankingData: RankingType | SteamRankingType; 12 curRankingData: RankingType | SteamRankingType;
13 currentLeaderboardType: RankingCategories 13 currentLeaderboardType: RankingCategories;
14};
15
16const RankingEntry: React.FC<RankingEntryProps> = (prop) => {
17 if ("placement" in prop.curRankingData) {
18 return (
19 <div className='leaderboard-entry'>
20 <span>{prop.curRankingData.placement}</span>
21 <div>
22 <Link to={`/users/${prop.curRankingData.user.steam_id}`}>
23 <img src={prop.curRankingData.user.avatar_link}></img>
24 <span>{prop.curRankingData.user.user_name}</span>
25 </Link>
26 </div>
27 <span>{prop.curRankingData.total_score}</span>
28 </div>
29 )
30 } else {
31 return (
32 <div className='leaderboard-entry'>
33 <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_rank : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_rank : prop.curRankingData.overall_rank}</span>
34 <div>
35 <Link to={`/users/${prop.curRankingData.steam_id}`}>
36 <img src={prop.curRankingData.avatar_link}></img>
37 <span>{prop.curRankingData.user_name}</span>
38 </Link>
39 </div>
40 <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span>
41 </div>
42 )
43 }
44} 14}
45 15
16const RankingEntry: React.FC<RankingEntryProps> = prop => {
17 if ("placement" in prop.curRankingData) {
18 return (
19 <div className="leaderboard-entry">
20 <span>{prop.curRankingData.placement}</span>
21 <div>
22 <Link to={`/users/${prop.curRankingData.user.steam_id}`}>
23 <img
24 src={prop.curRankingData.user.avatar_link}
25 alt={`${prop.curRankingData.user.user_name}'s Avatar`}
26 ></img>
27 <span>{prop.curRankingData.user.user_name}</span>
28 </Link>
29 </div>
30 <span>{prop.curRankingData.total_score}</span>
31 </div>
32 );
33 } else {
34 return (
35 <div className="leaderboard-entry">
36 <span>
37 {prop.currentLeaderboardType ===
38 RankingCategories.rankings_singleplayer
39 ? prop.curRankingData.sp_rank
40 : prop.currentLeaderboardType ===
41 RankingCategories.rankings_multiplayer
42 ? prop.curRankingData.mp_rank
43 : prop.curRankingData.overall_rank}
44 </span>
45 <div>
46 <Link to={`/users/${prop.curRankingData.steam_id}`}>
47 <img src={prop.curRankingData.avatar_link}></img>
48 <span>{prop.curRankingData.user_name}</span>
49 </Link>
50 </div>
51 <span>
52 {prop.currentLeaderboardType ===
53 RankingCategories.rankings_singleplayer
54 ? prop.curRankingData.sp_score
55 : prop.currentLeaderboardType ===
56 RankingCategories.rankings_multiplayer
57 ? prop.curRankingData.mp_score
58 : prop.curRankingData.overall_score}
59 </span>
60 </div>
61 );
62 }
63};
64
46export default RankingEntry; 65export default RankingEntry;
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..88a5297 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,99 +1,80 @@
1import React from 'react'; 1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from 'react-router-dom'; 2import { Link, useLocation } from "react-router-dom";
3 3
4import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; 4import {
5import Login from '@components/Login'; 5 BookIcon,
6import { UserProfile } from '@customTypes/Profile'; 6 FlagIcon,
7import { Search } from '@customTypes/Search'; 7 HelpIcon,
8import { API } from '@api/Api'; 8 HomeIcon,
9import "@css/Sidebar.css"; 9 LogoIcon,
10 PortalIcon,
11 SearchIcon,
12 UploadIcon,
13} from "../images/Images";
14import Login from "@components/Login";
15import { UserProfile } from "@customTypes/Profile";
16import { Search } from "@customTypes/Search";
17import { API } from "@api/Api";
10 18
11interface SidebarProps { 19interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 20 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile; 21 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 22 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15 onUploadRun: () => void; 23 onUploadRun: () => void;
16}; 24}
25
26function OpenSidebarIcon(){
27 return (
28 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-close-icon lucide-panel-right-close"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m8 9 3 3-3 3"/></svg>
29 )
30}
17 31
18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { 32function ClosedSidebarIcon(){
33 return (
34<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg> )
35}
19 36
20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 37const Sidebar: React.FC<SidebarProps> = ({
38 setToken,
39 profile,
40 setProfile,
41 onUploadRun,
42}) => {
43 const [searchData, setSearchData] = React.useState<Search | undefined>(
44 undefined
45 );
21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 46 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
22 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
23 49
24 const location = useLocation(); 50 const location = useLocation();
25 const path = location.pathname; 51 const path = location.pathname;
26 52
27 const handle_sidebar_click = (clicked_sidebar_idx: number) => { 53 const sidebarRef = useRef<HTMLDivElement>(null);
28 const btn = document.querySelectorAll("button.sidebar-button"); 54 const searchbarRef = useRef<HTMLInputElement>(null);
29 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } 55 const uploadRunRef = useRef<HTMLButtonElement>(null);
30 // clusterfuck 56 const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
31 btn.forEach((e, i) => {
32 btn[i].classList.remove("sidebar-button-selected")
33 btn[i].classList.add("sidebar-button-deselected")
34 })
35 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected")
36 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected")
37 };
38 57
39 const _handle_sidebar_hide = () => { 58 const _handle_sidebar_toggle = useCallback(() => {
40 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> 59 if (!sidebarRef.current) return;
41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement>
42 const side = document.querySelector("#sidebar-list") as HTMLElement;
43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement;
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement;
46 60
47 if (isSidebarOpen) { 61 if (isSidebarOpen) {
48 if (profile) {
49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
50 login.style.opacity = "1"
51 uploadRunBtn.style.width = "310px"
52 uploadRunBtn.style.padding = "0.4em 0 0 11px"
53 uploadRunSpan.style.opacity = "0"
54 setTimeout(() => {
55 uploadRunSpan.style.opacity = "1"
56 }, 100)
57 }
58 setSidebarOpen(false); 62 setSidebarOpen(false);
59 side.style.width = "320px"
60 btn.forEach((e, i) => {
61 e.style.width = "310px"
62 e.style.padding = "0.4em 0 0 11px"
63 setTimeout(() => {
64 span[i].style.opacity = "1"
65 }, 100)
66 });
67 side.style.zIndex = "2"
68 } else { 63 } else {
69 if (profile) {
70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
71 login.style.opacity = "0"
72 uploadRunBtn.style.width = "40px"
73 uploadRunBtn.style.padding = "0.4em 0 0 5px"
74 uploadRunSpan.style.opacity = "0"
75 }
76 setSidebarOpen(true); 64 setSidebarOpen(true);
77 side.style.width = "40px"; 65 searchbarRef.current?.focus();
78 searchbar.focus();
79 btn.forEach((e, i) => {
80 e.style.width = "40px"
81 e.style.padding = "0.4em 0 0 5px"
82 span[i].style.opacity = "0"
83 })
84 setTimeout(() => {
85 side.style.zIndex = "0"
86 }, 300);
87 } 66 }
88 }; 67 }, [isSidebarOpen]);
89 68
90 const _handle_sidebar_lock = () => { 69 const handle_sidebar_click = useCallback(
91 if (!isSidebarLocked) { 70 (clicked_sidebar_idx: number) => {
92 _handle_sidebar_hide() 71 setSelectedButtonIndex(clicked_sidebar_idx);
93 setIsSidebarLocked(true); 72 if (isSidebarOpen) {
94 setTimeout(() => setIsSidebarLocked(false), 300); 73 setSidebarOpen(false);
95 } 74 }
96 }; 75 },
76 [isSidebarOpen]
77 );
97 78
98 const _handle_search_change = async (q: string) => { 79 const _handle_search_change = async (q: string) => {
99 const searchResponse = await API.get_search(q); 80 const searchResponse = await API.get_search(q);
@@ -101,100 +82,200 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
101 }; 82 };
102 83
103 React.useEffect(() => { 84 React.useEffect(() => {
104 if (path === "/") { handle_sidebar_click(1) } 85 if (path === "/") {
105 else if (path.includes("games")) { handle_sidebar_click(2) } 86 setSelectedButtonIndex(1);
106 else if (path.includes("rankings")) { handle_sidebar_click(3) } 87 } else if (path.includes("games")) {
107 // else if (path.includes("news")) { handle_sidebar_click(4) } 88 setSelectedButtonIndex(2);
108 // else if (path.includes("scorelog")) { handle_sidebar_click(5) } 89 } else if (path.includes("rankings")) {
109 else if (path.includes("profile")) { handle_sidebar_click(4) } 90 setSelectedButtonIndex(3);
110 else if (path.includes("rules")) { handle_sidebar_click(5) } 91 } else if (path.includes("profile")) {
111 else if (path.includes("about")) { handle_sidebar_click(6) } 92 setSelectedButtonIndex(4);
93 } else if (path.includes("rules")) {
94 setSelectedButtonIndex(5);
95 } else if (path.includes("about")) {
96 setSelectedButtonIndex(6);
97 }
112 }, [path]); 98 }, [path]);
113 99
114 return ( 100 const getButtonClasses = (buttonIndex: number) => {
115 <div id='sidebar'> 101 const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-lg py-3 px-3 transition-all duration-300 hover:bg-surface1";
116 <Link to="/" tabIndex={-1}> 102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
117 <div id='logo'> {/* logo */} 103
118 <img src={LogoIcon} alt="" height={"80px"} /> 104 return `${baseClasses} ${selectedClasses}`;
119 <div id='logo-text'> 105 };
120 <span><b>PORTAL 2</b></span><br />
121 <span>Least Portals Hub</span>
122 </div>
123 </div>
124 </Link>
125 <div id='sidebar-list'> {/* List */}
126 <div id='sidebar-toplist'> {/* Top */}
127
128 <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
129
130 <span></span>
131 106
132 <Link to="/" tabIndex={-1}> 107 const iconClasses = "w-6 h-6 flex-shrink-0";
133 <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
134 </Link>
135 108
136 <Link to="/games" tabIndex={-1}> 109 return (
137 <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> 110 <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${
138 </Link> 111 isSidebarOpen ? 'w-80' : 'w-20'
112 }`}>
113 <div className="flex items-center h-20 px-4 border-b border-border">
114 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
115 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
116 {isSidebarOpen && (
117 <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden">
118 <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate">
119 PORTAL 2
120 </div>
121 <div className="text-sm leading-4 truncate">
122 Least Portals Hub
123 </div>
124 </div>
125 )}
126 </Link>
127
128 <button
129 onClick={_handle_sidebar_toggle}
130 className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground"
131 title={isSidebarOpen ? "Close sidebar" : "Open sidebar"}
132 >
133 {isSidebarOpen ? <ClosedSidebarIcon /> : <OpenSidebarIcon />}
134 </button>
135 </div>
139 136
140 <Link to="/rankings" tabIndex={-1}> 137 {/* Sidebar Content */}
141 <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> 138 <div
142 </Link> 139 ref={sidebarRef}
140 className="flex flex-col h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden"
141 >
142 {isSidebarOpen && (
143 <div className="p-4 border-b border-border min-w-0">
144 <div className="flex items-center gap-3 mb-3">
145 <img src={SearchIcon} alt="Search" className={iconClasses} />
146 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span>
147 </div>
148
149 <div className="min-w-0">
150 <input
151 ref={searchbarRef}
152 type="text"
153 id="searchbar"
154 placeholder="Search for map or a player..."
155 onChange={e => _handle_search_change(e.target.value)}
156 className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0"
157 />
143 158
144 {/* <Link to="/news" tabIndex={-1}> 159 {searchData && (
145 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> 160 <div className="mt-2 max-h-40 overflow-y-auto min-w-0">
146 </Link> */} 161 {searchData?.maps.map((q, index) => (
162 <Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}>
163 <span className="block text-xs text-subtext1 truncate">{q.game}</span>
164 <span className="block text-xs text-subtext1 truncate">{q.chapter}</span>
165 <span className="block text-sm text-foreground truncate">{q.map}</span>
166 </Link>
167 ))}
168 {searchData?.players.map((q, index) => (
169 <Link
170 to={
171 profile && q.steam_id === profile.steam_id
172 ? `/profile`
173 : `/users/${q.steam_id}`
174 }
175 className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0"
176 key={index}
177 >
178 <img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" />
179 <span className="text-sm text-foreground truncate">
180 {q.user_name}
181 </span>
182 </Link>
183 ))}
184 </div>
185 )}
186 </div>
187 </div>
188 )}
147 189
148 {/* <Link to="/scorelog" tabIndex={-1}> 190 <div className="flex-1 p-4 min-w-0">
149 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button> 191 <nav className="space-y-2">
150 </Link> */} 192 {[
193 {
194 to: "/",
195 refIndex: 1,
196 icon: HomeIcon,
197 alt: "Home",
198 label: "Home Page",
199 },
200 {
201 to: "/games",
202 refIndex: 2,
203 icon: PortalIcon,
204 alt: "Games",
205 label: "Games",
206 },
207 {
208 to: "/rankings",
209 refIndex: 3,
210 icon: FlagIcon,
211 alt: "Rankings",
212 label: "Rankings",
213 },
214 ].map(({ to, refIndex, icon, alt, label }) => (
215 <Link to={to} tabIndex={-1} key={refIndex}>
216 <button
217 ref={el => sidebarButtonRefs.current[refIndex] = el}
218 className={getButtonClasses(refIndex)}
219 onClick={() => handle_sidebar_click(refIndex)}
220 >
221 <img src={icon} alt={alt} className={iconClasses} />
222 {isSidebarOpen && (
223 <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">
224 {label}
225 </span>
226 )}
227 </button>
228 </Link>
229 ))}
230 </nav>
151 </div> 231 </div>
152 <div id='sidebar-bottomlist'>
153 <span></span>
154 232
155 { 233 {/* Bottom Section */}
156 profile && profile.profile ? 234 <div className="p-4 border-t border-border space-y-2 min-w-0">
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button> 235 {profile && profile.profile && (
158 : 236 <button
159 <span></span> 237 ref={uploadRunRef}
160 } 238 id="upload-run"
239 className={getButtonClasses(-1)}
240 onClick={() => onUploadRun()}
241 >
242 <img src={UploadIcon} alt="Upload" className={iconClasses} />
243 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>}
244 </button>
245 )}
161 246
162 <Login setToken={setToken} profile={profile} setProfile={setProfile} /> 247 <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}>
248 <Login
249 setToken={setToken}
250 profile={profile}
251 setProfile={setProfile}
252 isOpen={isSidebarOpen}
253 />
254 </div>
163 255
164 <Link to="/rules" tabIndex={-1}> 256 <Link to="/rules" tabIndex={-1}>
165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button> 257 <button
258 ref={el => sidebarButtonRefs.current[5] = el}
259 className={getButtonClasses(5)}
260 onClick={() => handle_sidebar_click(5)}
261 >
262 <img src={BookIcon} alt="Rules" className={iconClasses} />
263 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>}
264 </button>
166 </Link> 265 </Link>
167 266
168 <Link to="/about" tabIndex={-1}> 267 <Link to="/about" tabIndex={-1}>
169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button> 268 <button
269 ref={el => sidebarButtonRefs.current[6] = el}
270 className={getButtonClasses(6)}
271 onClick={() => handle_sidebar_click(6)}
272 >
273 <img src={HelpIcon} alt="About" className={iconClasses} />
274 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>}
275 </button>
170 </Link> 276 </Link>
171 </div> 277 </div>
172 </div> 278 </div>
173 <div>
174 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
175
176 <div id='search-data'>
177
178 {searchData?.maps.map((q, index) => (
179 <Link to={`/maps/${q.id}`} className='search-map' key={index}>
180 <span>{q.game}</span>
181 <span>{q.chapter}</span>
182 <span>{q.map}</span>
183 </Link>
184 ))}
185 {searchData?.players.map((q, index) =>
186 (
187 <Link to={
188 profile && q.steam_id === profile.steam_id ? `/profile` :
189 `/users/${q.steam_id}`
190 } className='search-player' key={index}>
191 <img src={q.avatar_link} alt='pfp'></img>
192 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
193 </Link>
194 ))}
195
196 </div>
197 </div>
198 </div> 279 </div>
199 ); 280 );
200}; 281};
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 7da2f1e..ba91f57 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -1,193 +1,267 @@
1import React from 'react'; 1import React from "react";
2import ReactMarkdown from 'react-markdown'; 2import ReactMarkdown from "react-markdown";
3 3
4import { MapSummary } from '@customTypes/Map'; 4import { MapSummary } from "@customTypes/Map";
5import "@css/Maps.css"
6 5
7interface SummaryProps { 6interface SummaryProps {
8 selectedRun: number 7 selectedRun: number;
9 setSelectedRun: (x: number) => void; 8 setSelectedRun: (x: number) => void;
10 data: MapSummary; 9 data: MapSummary;
11} 10}
12 11
13const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { 12const Summary: React.FC<SummaryProps> = ({
14 13 selectedRun,
14 setSelectedRun,
15 data,
16}) => {
15 const [selectedCategory, setSelectedCategory] = React.useState<number>(1); 17 const [selectedCategory, setSelectedCategory] = React.useState<number>(1);
16 const [historySelected, setHistorySelected] = React.useState<boolean>(false); 18 const [historySelected, setHistorySelected] = React.useState<boolean>(false);
17 19
18 function _select_run(idx: number, category_id: number) { 20 const _select_run = React.useCallback(
19 let r = document.querySelectorAll("button.record"); 21 (idx: number, category_id: number) => {
20 r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); 22 let r = document.querySelectorAll("button.record");
21 (r[idx] as HTMLElement).style.backgroundColor = "#161723" 23 r.forEach(e => ((e as HTMLElement).style.backgroundColor = "#2b2e46"));
22 24 (r[idx] as HTMLElement).style.backgroundColor = "#161723";
23 25
24 if (data && data.summary.routes.length !== 0) { 26 if (data && data.summary.routes.length !== 0) {
25 idx += data.summary.routes.filter(e => e.category.id < category_id).length // lethimcook 27 idx += data.summary.routes.filter(
26 setSelectedRun(idx); 28 e => e.category.id < category_id
27 } 29 ).length; // lethimcook
28 }; 30 setSelectedRun(idx);
31 }
32 },
33 [data, setSelectedRun]
34 );
29 35
30 function _get_youtube_id(url: string): string { 36 function _get_youtube_id(url: string): string {
31 const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); 37 const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
32 return (urlArray[2] !== undefined) ? urlArray[2].split(/[^0-9a-z_-]/i)[0] : urlArray[0]; 38 return urlArray[2] !== undefined
33 }; 39 ? urlArray[2].split(/[^0-9a-z_-]/i)[0]
40 : urlArray[0];
41 }
34 42
35 function _category_change() { 43 const _category_change = React.useCallback(() => {
36 const btn = document.querySelectorAll("#section3 #category span button"); 44 const btn = document.querySelectorAll("#section3 #category span button");
37 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 45 btn.forEach(e => {
46 (e as HTMLElement).style.backgroundColor = "#2b2e46";
47 });
38 // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories 48 // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories
39 const idx = selectedCategory === 1 ? 0 : data.map.is_coop ? selectedCategory - 3 : selectedCategory - 1; 49 const idx =
50 selectedCategory === 1
51 ? 0
52 : data.map.is_coop
53 ? selectedCategory - 3
54 : selectedCategory - 1;
40 (btn[idx] as HTMLElement).style.backgroundColor = "#202232"; 55 (btn[idx] as HTMLElement).style.backgroundColor = "#202232";
41 }; 56 }, [selectedCategory, data.map.is_coop]);
42 57
43 function _history_change() { 58 const _history_change = React.useCallback(() => {
44 const btn = document.querySelectorAll("#section3 #history span button"); 59 const btn = document.querySelectorAll("#section3 #history span button");
45 btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); 60 btn.forEach(e => {
46 (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; 61 (e as HTMLElement).style.backgroundColor = "#2b2e46";
47 }; 62 });
63 (historySelected
64 ? (btn[1] as HTMLElement)
65 : (btn[0] as HTMLElement)
66 ).style.backgroundColor = "#202232";
67 }, [historySelected]);
48 68
49 React.useEffect(() => { 69 React.useEffect(() => {
50 _history_change(); 70 _history_change();
51 }, [historySelected]); 71 }, [historySelected, _history_change]);
52 72
53 React.useEffect(() => { 73 React.useEffect(() => {
54 _category_change(); 74 _category_change();
55 _select_run(0, selectedCategory); 75 _select_run(0, selectedCategory);
56 }, [selectedCategory]); 76 }, [selectedCategory, _category_change, _select_run]);
57 77
58 React.useEffect(() => { 78 React.useEffect(() => {
59 _select_run(0, selectedCategory); 79 _select_run(0, selectedCategory);
60 }, []); 80 }, [_select_run, selectedCategory]);
61 81
62 return ( 82 return (
63 <> 83 <>
64 <section id='section3' className='summary1'> 84 <section id="section3" className="summary1 text-foreground">
65 <div id='category' 85 <div
66 style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> 86 id="category"
67 <img src={data.map.image} alt="" id='category-image'></img> 87 style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}
68 <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> 88 >
69 {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> 89 <img src={data.map.image} alt="" id="category-image"></img>
70 {data.map.is_coop ? // TODO: make this part dynamic 90 <p>
71 ( 91 <span className="portal-count">
72 <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> 92 {data.summary.routes[selectedRun].history.score_count}
73 <button onClick={() => setSelectedCategory(1)}>CM</button> 93 </span>
74 <button onClick={() => setSelectedCategory(4)}>Any%</button> 94 {data.summary.routes[selectedRun].history.score_count === 1
75 <button onClick={() => setSelectedCategory(5)}>All Courses</button> 95 ? ` portal`
76 </span> 96 : ` portals`}
77 ) 97 </p>
78 : 98 {data.map.is_coop ? ( // TODO: make this part dynamic
79 ( 99 <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}>
80 <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}> 100 <button onClick={() => setSelectedCategory(1)}>CM</button>
81 101 <button onClick={() => setSelectedCategory(4)}>Any%</button>
82 <button onClick={() => setSelectedCategory(1)}>CM</button> 102 <button onClick={() => setSelectedCategory(5)}>
83 <button onClick={() => setSelectedCategory(2)}>NoSLA</button> 103 All Courses
84 <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button> 104 </button>
85 <button onClick={() => setSelectedCategory(4)}>Any%</button> 105 </span>
86 </span> 106 ) : (
87 ) 107 <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}>
88 } 108 <button onClick={() => setSelectedCategory(1)}>CM</button>
89 109 <button onClick={() => setSelectedCategory(2)}>NoSLA</button>
110 <button onClick={() => setSelectedCategory(3)}>
111 Inbounds SLA
112 </button>
113 <button onClick={() => setSelectedCategory(4)}>Any%</button>
114 </span>
115 )}
90 </div> 116 </div>
91 117
92 <div id='history'> 118 <div id="history">
93
94 <div style={{ display: historySelected ? "none" : "block" }}> 119 <div style={{ display: historySelected ? "none" : "block" }}>
95 {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : 120 {data.summary.routes.filter(e => e.category.id === selectedCategory)
121 .length === 0 ? (
122 <h5>There are no records for this map.</h5>
123 ) : (
96 <> 124 <>
97 <div className='record-top'> 125 <div className="record-top">
98 <span>Date</span> 126 <span>Date</span>
99 <span>Record</span> 127 <span>Record</span>
100 <span>First Completion</span> 128 <span>First Completion</span>
101 </div> 129 </div>
102 <hr /> 130 <hr />
103 <div id='records'> 131 <div id="records">
104
105 {data.summary.routes 132 {data.summary.routes
106 .filter(e => e.category.id === selectedCategory) 133 .filter(e => e.category.id === selectedCategory)
107 .map((r, index) => ( 134 .map((r, index) => (
108 <button className='record' key={index} onClick={() => { 135 <button
109 _select_run(index, r.category.id); 136 className="record"
110 }}> 137 key={index}
111 <span>{new Date(r.history.date).toLocaleDateString( 138 onClick={() => {
112 "en-US", { month: 'long', day: 'numeric', year: 'numeric' } 139 _select_run(index, r.category.id);
113 )}</span> 140 }}
141 >
142 <span>
143 {new Date(r.history.date).toLocaleDateString(
144 "en-US",
145 { month: "long", day: "numeric", year: "numeric" }
146 )}
147 </span>
114 <span>{r.history.score_count}</span> 148 <span>{r.history.score_count}</span>
115 <span>{r.history.runner_name}</span> 149 <span>{r.history.runner_name}</span>
116 </button> 150 </button>
117 ))} 151 ))}
118 </div> 152 </div>
119 </> 153 </>
120 } 154 )}
121 </div> 155 </div>
122 156
123 <div style={{ display: historySelected ? "block" : "none" }}> 157 <div style={{ display: historySelected ? "block" : "none" }}>
124 {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : 158 {data.summary.routes.filter(e => e.category.id === selectedCategory)
125 <div id='graph'> 159 .length === 0 ? (
160 <h5>There are no records for this map.</h5>
161 ) : (
162 <div id="graph">
126 {/* <div>{graph(1)}</div> 163 {/* <div>{graph(1)}</div>
127 <div>{graph(2)}</div> 164 <div>{graph(2)}</div>
128 <div>{graph(3)}</div> */} 165 <div>{graph(3)}</div> */}
129 </div> 166 </div>
130 } 167 )}
131 </div> 168 </div>
132 <span> 169 <span>
133 <button onClick={() => setHistorySelected(false)}>List</button> 170 <button onClick={() => setHistorySelected(false)}>List</button>
134 <button onClick={() => setHistorySelected(true)}>Graph</button> 171 <button onClick={() => setHistorySelected(true)}>Graph</button>
135 </span> 172 </span>
136 </div> 173 </div>
137 174 </section>
138 175 <section id="section4" className="summary1">
139 </section > 176 <div id="difficulty">
140 <section id='section4' className='summary1'> 177 <span className="">Difficulty</span>
141 <div id='difficulty'> 178 {data.map.difficulty <= 2 && (
142 <span>Difficulty</span> 179 <span style={{ color: "lime" }}>Very Easy</span>
143 {data.map.difficulty <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)} 180 )}
144 {data.map.difficulty > 2 && data.map.difficulty <= 4 && (<span style={{ color: "green" }}>Easy</span>)} 181 {data.map.difficulty > 2 && data.map.difficulty <= 4 && (
145 {data.map.difficulty > 4 && data.map.difficulty <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)} 182 <span style={{ color: "green" }}>Easy</span>
146 {data.map.difficulty > 6 && data.map.difficulty <= 8 && (<span style={{ color: "orange" }}>Hard</span>)} 183 )}
147 {data.map.difficulty > 8 && data.map.difficulty <= 10 && (<span style={{ color: "red" }}>Very hard</span>)} 184 {data.map.difficulty > 4 && data.map.difficulty <= 6 && (
185 <span style={{ color: "yellow" }}>Medium</span>
186 )}
187 {data.map.difficulty > 6 && data.map.difficulty <= 8 && (
188 <span style={{ color: "orange" }}>Hard</span>
189 )}
190 {data.map.difficulty > 8 && data.map.difficulty <= 10 && (
191 <span style={{ color: "red" }}>Very Hard</span>
192 )}
148 <div> 193 <div>
149 {data.map.difficulty <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} 194 {data.map.difficulty <= 2 && ? (
150 {data.map.difficulty > 2 && data.map.difficulty <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} 195 <div
151 {data.map.difficulty > 4 && data.map.difficulty <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} 196 className="difficulty-rating"
152 {data.map.difficulty > 6 && data.map.difficulty <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} 197 style={{ backgroundColor: "lime" }}
153 {data.map.difficulty > 8 && data.map.difficulty <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} 198 ></div>
199 ) : (
200 <div className="difficulty-rating"></div>
201 )}
202 {data.map.difficulty > 2 && data.map.difficulty <= 4 && ? (
203 <div
204 className="difficulty-rating"
205 style={{ backgroundColor: "green" }}
206 ></div>
207 ) : (
208 <div className="difficulty-rating"></div>
209 )}
210 {data.map.difficulty > 4 && data.map.difficulty <= 6 && ? (
211 <div
212 className="difficulty-rating"
213 style={{ backgroundColor: "yellow" }}
214 ></div>
215 ) : (
216 <div className="difficulty-rating"></div>
217 )}
218 {data.map.difficulty > 6 && data.map.difficulty <= 8 && ? (
219 <div
220 className="difficulty-rating"
221 style={{ backgroundColor: "orange" }}
222 ></div>
223 ) : (
224 <div className="difficulty-rating"></div>
225 )}
226 {data.map.difficulty > 8 && data.map.difficulty <= 10 && ? (
227 <div
228 className="difficulty-rating"
229 style={{ backgroundColor: "red" }}
230 ></div>
231 ) : (
232 <div className="difficulty-rating"></div>
233 )}
154 </div> 234 </div>
155 </div> 235 </div>
156 {/* <div id='difficulty'> 236 <div id="count">
157 <span>Difficulty</span>
158 {data.summary.routes[selectedRun].rating <= 2 && (<span style={{ color: "lime" }}>Very easy</span>)}
159 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 && (<span style={{ color: "green" }}>Easy</span>)}
160 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 && (<span style={{ color: "yellow" }}>Medium</span>)}
161 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 && (<span style={{ color: "orange" }}>Hard</span>)}
162 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 && (<span style={{ color: "red" }}>Very hard</span>)}
163 <div>
164 {data.summary.routes[selectedRun].rating <= 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)}
165 {data.summary.routes[selectedRun].rating > 2 && data.summary.routes[selectedRun].rating <= 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)}
166 {data.summary.routes[selectedRun].rating > 4 && data.summary.routes[selectedRun].rating <= 6 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)}
167 {data.summary.routes[selectedRun].rating > 6 && data.summary.routes[selectedRun].rating <= 8 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)}
168 {data.summary.routes[selectedRun].rating > 8 && data.summary.routes[selectedRun].rating <= 10 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)}
169 </div>
170 </div> */}
171 <div id='count'>
172 <span>Completion Count</span> 237 <span>Completion Count</span>
173 <div>{data.summary.routes[selectedRun].completion_count}</div> 238 <div>{data.summary.routes[selectedRun].completion_count}</div>
174 </div> 239 </div>
175 </section> 240 </section>
176 241
177 <section id='section5' className='summary1'> 242 <section id="section5" className="summary1">
178 <div id='description'> 243 <div id="description">
179 {data.summary.routes[selectedRun].showcase !== "" ? 244 {data.summary.routes[selectedRun].showcase !== "" ? (
180 <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> 245 <iframe
181 : ""} 246 title="Showcase video"
182 <h3>Route Description</h3> 247 src={
183 <span id='description-text'> 248 "https://www.youtube.com/embed/" +
184 <ReactMarkdown> 249 _get_youtube_id(data.summary.routes[selectedRun].showcase)
250 }
251 >
252 {" "}
253 </iframe>
254 ) : (
255 ""
256 )}
257 <h3 className="font-semibold">Route Description</h3>
258 <span id="description-text">
259 <ReactMarkdown className="text-foreground">
185 {data.summary.routes[selectedRun].description} 260 {data.summary.routes[selectedRun].description}
186 </ReactMarkdown> 261 </ReactMarkdown>
187 </span> 262 </span>
188 </div> 263 </div>
189 </section> 264 </section>
190
191 </> 265 </>
192 ); 266 );
193}; 267};
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index c02fdb8..0034019 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -1,15 +1,14 @@
1import React from 'react'; 1import React from "react";
2import { UploadRunContent } from '@customTypes/Content'; 2import { UploadRunContent } from "@customTypes/Content";
3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; 3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp";
4 4
5import '@css/UploadRunDialog.css'; 5import { Game } from "@customTypes/Game";
6import { Game } from '@customTypes/Game'; 6import { API } from "@api/Api";
7import { API } from '@api/Api'; 7import { useNavigate } from "react-router-dom";
8import { useNavigate } from 'react-router-dom'; 8import useMessage from "@hooks/UseMessage";
9import useMessage from '@hooks/UseMessage'; 9import useConfirm from "@hooks/UseConfirm";
10import useConfirm from '@hooks/UseConfirm';
11import useMessageLoad from "@hooks/UseMessageLoad"; 10import useMessageLoad from "@hooks/UseMessageLoad";
12import { MapNames } from '@customTypes/MapNames'; 11import { MapNames } from "@customTypes/MapNames";
13 12
14interface UploadRunDialogProps { 13interface UploadRunDialogProps {
15 token?: string; 14 token?: string;
@@ -18,18 +17,24 @@ interface UploadRunDialogProps {
18 games: Game[]; 17 games: Game[];
19} 18}
20 19
21const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { 20const UploadRunDialog: React.FC<UploadRunDialogProps> = ({
22 21 token,
22 open,
23 onClose,
24 games,
25}) => {
23 const { message, MessageDialogComponent } = useMessage(); 26 const { message, MessageDialogComponent } = useMessage();
24 const { confirm, ConfirmDialogComponent } = useConfirm(); 27 const { confirm, ConfirmDialogComponent } = useConfirm();
25 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); 28 const { messageLoad, messageLoadClose, MessageDialogLoadComponent } =
29 useMessageLoad();
26 30
27 const navigate = useNavigate(); 31 const navigate = useNavigate();
28 32
29 const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ 33 const [uploadRunContent, setUploadRunContent] =
30 host_demo: null, 34 React.useState<UploadRunContent>({
31 partner_demo: null, 35 host_demo: null,
32 }); 36 partner_demo: null,
37 });
33 38
34 const [selectedGameID, setSelectedGameID] = React.useState<number>(0); 39 const [selectedGameID, setSelectedGameID] = React.useState<number>(0);
35 const [selectedGameName, setSelectedGameName] = React.useState<string>(""); 40 const [selectedGameName, setSelectedGameName] = React.useState<string>("");
@@ -41,7 +46,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
41 const [loading, setLoading] = React.useState<boolean>(false); 46 const [loading, setLoading] = React.useState<boolean>(false);
42 47
43 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); 48 const [dragHightlight, setDragHighlight] = React.useState<boolean>(false);
44 const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); 49 const [dragHightlightPartner, setDragHighlightPartner] =
50 React.useState<boolean>(false);
45 51
46 const fileInputRef = React.useRef<HTMLInputElement>(null); 52 const fileInputRef = React.useRef<HTMLInputElement>(null);
47 const fileInputRefPartner = React.useRef<HTMLInputElement>(null); 53 const fileInputRefPartner = React.useRef<HTMLInputElement>(null);
@@ -52,9 +58,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
52 } else { 58 } else {
53 fileInputRefPartner.current?.click(); 59 fileInputRefPartner.current?.click();
54 } 60 }
55 } 61 };
56 62
57 const _handle_drag_over = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 63 const _handle_drag_over = (
64 e: React.DragEvent<HTMLDivElement>,
65 host: boolean
66 ) => {
58 e.preventDefault(); 67 e.preventDefault();
59 e.stopPropagation(); 68 e.stopPropagation();
60 if (host) { 69 if (host) {
@@ -62,9 +71,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
62 } else { 71 } else {
63 setDragHighlightPartner(true); 72 setDragHighlightPartner(true);
64 } 73 }
65 } 74 };
66 75
67 const _handle_drag_leave = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 76 const _handle_drag_leave = (
77 e: React.DragEvent<HTMLDivElement>,
78 host: boolean
79 ) => {
68 e.preventDefault(); 80 e.preventDefault();
69 e.stopPropagation(); 81 e.stopPropagation();
70 if (host) { 82 if (host) {
@@ -72,7 +84,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
72 } else { 84 } else {
73 setDragHighlightPartner(false); 85 setDragHighlightPartner(false);
74 } 86 }
75 } 87 };
76 88
77 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { 89 const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => {
78 e.preventDefault(); 90 e.preventDefault();
@@ -80,18 +92,18 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
80 setDragHighlight(true); 92 setDragHighlight(true);
81 93
82 _handle_file_change(e.dataTransfer.files, host); 94 _handle_file_change(e.dataTransfer.files, host);
83 } 95 };
84 96
85 const _handle_dropdowns = (dropdown: number) => { 97 const _handle_dropdowns = (dropdown: number) => {
86 setDropdown1Vis(false); 98 setDropdown1Vis(false);
87 setDropdown2Vis(false); 99 setDropdown2Vis(false);
88 if (dropdown == 1) { 100 if (dropdown === 1) {
89 setDropdown1Vis(!dropdown1Vis); 101 setDropdown1Vis(!dropdown1Vis);
90 } else if (dropdown == 2) { 102 } else if (dropdown === 2) {
91 setDropdown2Vis(!dropdown2Vis); 103 setDropdown2Vis(!dropdown2Vis);
92 document.querySelector("#dropdown2")?.scrollTo(0, 0); 104 document.querySelector("#dropdown2")?.scrollTo(0, 0);
93 } 105 }
94 } 106 };
95 107
96 const _handle_game_select = async (game_id: string, game_name: string) => { 108 const _handle_game_select = async (game_id: string, game_name: string) => {
97 setLoading(true); 109 setLoading(true);
@@ -120,53 +132,76 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
120 if (token) { 132 if (token) {
121 if (games[selectedGameID].is_coop) { 133 if (games[selectedGameID].is_coop) {
122 if (uploadRunContent.host_demo === null) { 134 if (uploadRunContent.host_demo === null) {
123 await message("Error", "You must select a host demo to upload.") 135 await message("Error", "You must select a host demo to upload.");
124 return 136 return;
125 } else if (uploadRunContent.partner_demo === null) { 137 } else if (uploadRunContent.partner_demo === null) {
126 await message("Error", "You must select a partner demo to upload.") 138 await message("Error", "You must select a partner demo to upload.");
127 return 139 return;
128 } 140 }
129 } else { 141 } else {
130 if (uploadRunContent.host_demo === null) { 142 if (uploadRunContent.host_demo === null) {
131 await message("Error", "You must select a demo to upload.") 143 await message("Error", "You must select a demo to upload.");
132 return 144 return;
133 } 145 }
134 } 146 }
135 const demo = SourceDemoParser.default() 147 const demo = SourceDemoParser.default()
136 .setOptions({ packets: true, header: true }) 148 .setOptions({ packets: true, header: true })
137 .parse(await uploadRunContent.host_demo.arrayBuffer()); 149 .parse(await uploadRunContent.host_demo.arrayBuffer());
138 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => { 150 const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>(msg => {
139 return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate; 151 return (
140 }) 152 msg instanceof NetMessages.SvcUserMessage &&
153 msg.userMessage instanceof ScoreboardTempUpdate
154 );
155 });
141 156
142 if (!scoreboard) { 157 if (!scoreboard) {
143 await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") 158 await message(
144 return 159 "Error",
160 "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode."
161 );
162 return;
145 } 163 }
146 164
147 if (!demo.mapName || !MapNames[demo.mapName]) { 165 if (!demo.mapName || !MapNames[demo.mapName]) {
148 await message("Error", "Error while processing demo: Invalid map name.") 166 await message(
149 return 167 "Error",
168 "Error while processing demo: Invalid map name."
169 );
170 return;
150 } 171 }
151 172
152 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { 173 if (selectedGameID === 0 && MapNames[demo.mapName] > 60) {
153 await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") 174 await message(
154 return 175 "Error",
176 "Error while processing demo: Invalid cooperative demo in singleplayer submission."
177 );
178 return;
155 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { 179 } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) {
156 await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") 180 await message(
157 return 181 "Error",
182 "Error while processing demo: Invalid singleplayer demo in cooperative submission."
183 );
184 return;
158 } 185 }
159 186
160 const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; 187 const { portalScore, timeScore } =
188 scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {};
161 189
162 const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); 190 const userConfirmed = await confirm(
191 "Upload Record",
192 `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`
193 );
163 194
164 if (!userConfirmed) { 195 if (!userConfirmed) {
165 return; 196 return;
166 } 197 }
167 198
168 messageLoad("Uploading..."); 199 messageLoad("Uploading...");
169 const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); 200 const [success, response] = await API.post_record(
201 token,
202 uploadRunContent,
203 MapNames[demo.mapName]
204 );
170 messageLoadClose(); 205 messageLoadClose();
171 await message("Upload Record", response); 206 await message("Upload Record", response);
172 if (success) { 207 if (success) {
@@ -196,84 +231,191 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
196 {MessageDialogLoadComponent} 231 {MessageDialogLoadComponent}
197 {ConfirmDialogComponent} 232 {ConfirmDialogComponent}
198 233
199 <div id='upload-run-menu'> 234 <div id="upload-run-menu">
200 <div id='upload-run-menu-add'> 235 <div id="upload-run-menu-add">
201 <div id='upload-run-route-category'> 236 <div id="upload-run-route-category">
202 <div style={{ padding: "15px 0px" }} className='upload-run-dropdown-container upload-run-item'> 237 <div
238 style={{ padding: "15px 0px" }}
239 className="upload-run-dropdown-container upload-run-item"
240 >
203 <h3 style={{ margin: "0px 0px" }}>Select Game</h3> 241 <h3 style={{ margin: "0px 0px" }}>Select Game</h3>
204 <div onClick={() => _handle_dropdowns(1)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> 242 <div
205 <div className='dropdown-cur'>{selectedGameName}</div> 243 onClick={() => _handle_dropdowns(1)}
206 <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> 244 style={{
245 display: "flex",
246 alignItems: "center",
247 cursor: "pointer",
248 justifyContent: "space-between",
249 margin: "10px 0px",
250 }}
251 >
252 <div className="dropdown-cur">{selectedGameName}</div>
253 <i
254 style={{
255 rotate: "-90deg",
256 transform: "translate(-5px, 10px)",
257 }}
258 className="triangle"
259 ></i>
207 </div> 260 </div>
208 <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> 261 <div
209 {games.map((game) => ( 262 style={{ top: "110px" }}
210 <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> 263 className={
264 dropdown1Vis
265 ? "upload-run-dropdown"
266 : "upload-run-dropdown hidden"
267 }
268 >
269 {games.map(game => (
270 <div
271 onClick={() => {
272 _handle_game_select(game.id.toString(), game.name);
273 _handle_dropdowns(1);
274 }}
275 key={game.id}
276 >
277 {game.name}
278 </div>
211 ))} 279 ))}
212 </div> 280 </div>
213 </div> 281 </div>
214 282
215 { 283 {!loading && (
216 !loading && 284 <>
217 ( 285 <div>
218 <> 286 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3>
219 287 <div
220 <div> 288 onClick={() => {
221 <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> 289 _handle_file_click(true);
222 <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> 290 }}
223 <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> 291 onDragOver={e => {
224 {!uploadRunContent.host_demo ? 292 _handle_drag_over(e, true);
293 }}
294 onDrop={e => {
295 _handle_drop(e, true);
296 }}
297 onDragLeave={e => {
298 _handle_drag_leave(e, true);
299 }}
300 className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}
301 >
302 <input
303 ref={fileInputRef}
304 type="file"
305 name="host_demo"
306 id="host_demo"
307 accept=".dem"
308 onChange={e =>
309 _handle_file_change(e.target.files, true)
310 }
311 />
312 {!uploadRunContent.host_demo ? (
313 <div>
314 <span>Drag and drop</span>
225 <div> 315 <div>
226 <span>Drag and drop</span> 316 <span
227 <div> 317 style={{
228 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> 318 fontFamily: "BarlowSemiCondensed-Regular",
229 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> 319 }}
230 </div> 320 >
321 Or click here
322 </span>
323 <br />
324 <button
325 style={{
326 borderRadius: "24px",
327 padding: "5px 8px",
328 margin: "5px 0px",
329 }}
330 >
331 Upload
332 </button>
231 </div> 333 </div>
232 : null} 334 </div>
233 335 ) : null}
234 <span className="upload-run-demo-name">{uploadRunContent.host_demo?.name}</span>
235 </div>
236 {
237 games[selectedGameID].is_coop &&
238 (
239 <>
240 <div>
241 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
242 <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}>
243 <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ?
244 <div>
245 <span>Drag and drop</span>
246 <div>
247 <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br />
248 <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button>
249 </div>
250 </div>
251 : null}
252
253 <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span>
254 </div>
255 </div>
256 </>
257 )
258 }
259 </div>
260 <div className='search-container'>
261 336
337 <span className="upload-run-demo-name">
338 {uploadRunContent.host_demo?.name}
339 </span>
262 </div> 340 </div>
263 341 {games[selectedGameID].is_coop && (
264 </> 342 <>
265 ) 343 <div>
266 } 344 <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3>
345 <div
346 onClick={() => {
347 _handle_file_click(false);
348 }}
349 onDragOver={e => {
350 _handle_drag_over(e, false);
351 }}
352 onDrop={e => {
353 _handle_drop(e, false);
354 }}
355 onDragLeave={e => {
356 _handle_drag_leave(e, false);
357 }}
358 className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}
359 >
360 <input
361 ref={fileInputRefPartner}
362 type="file"
363 name="partner_demo"
364 id="partner_demo"
365 accept=".dem"
366 onChange={e =>
367 _handle_file_change(e.target.files, false)
368 }
369 />{" "}
370 {!uploadRunContent.partner_demo ? (
371 <div>
372 <span>Drag and drop</span>
373 <div>
374 <span
375 style={{
376 fontFamily: "BarlowSemiCondensed-Regular",
377 }}
378 >
379 Or click here
380 </span>
381 <br />
382 <button
383 style={{
384 borderRadius: "24px",
385 padding: "5px 8px",
386 margin: "5px 0px",
387 }}
388 >
389 Upload
390 </button>
391 </div>
392 </div>
393 ) : null}
394 <span className="upload-run-demo-name">
395 {uploadRunContent.partner_demo?.name}
396 </span>
397 </div>
398 </div>
399 </>
400 )}
401 </div>
402 <div className="search-container"></div>
403 </>
404 )}
267 </div> 405 </div>
268 <div className='upload-run-buttons-container'> 406 <div className="upload-run-buttons-container">
269 <button onClick={_upload_run}>Submit</button> 407 <button onClick={_upload_run}>Submit</button>
270 <button onClick={() => { 408 <button
271 onClose(false); 409 onClick={() => {
272 setUploadRunContent({ 410 onClose(false);
273 host_demo: null, 411 setUploadRunContent({
274 partner_demo: null, 412 host_demo: null,
275 }); 413 partner_demo: null,
276 }}>Cancel</button> 414 });
415 }}
416 >
417 Cancel
418 </button>
277 </div> 419 </div>
278 </div> 420 </div>
279 </div> 421 </div>
@@ -281,10 +423,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose,
281 ); 423 );
282 } 424 }
283 425
284 return ( 426 return <></>;
285 <></>
286 );
287
288}; 427};
289 428
290export default UploadRunDialog; 429export default UploadRunDialog;