diff options
| author | FifthWit <fifthwitbusiness@gmail.com> | 2025-01-30 10:44:30 -0600 |
|---|---|---|
| committer | FifthWit <fifthwitbusiness@gmail.com> | 2025-01-30 10:44:30 -0600 |
| commit | e40f07211f5f15dcb138e2520a76d13afd3c0cfd (patch) | |
| tree | 46bad6a17e66d55a4a65088c0b6eb8c48641615a /frontend/src/components | |
| parent | added prettier for more consistency (diff) | |
| download | lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.gz lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.tar.bz2 lphub-e40f07211f5f15dcb138e2520a76d13afd3c0cfd.zip | |
formatted with prettier
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/ConfirmDialog.tsx | 49 | ||||
| -rw-r--r-- | frontend/src/components/Discussions.tsx | 70 | ||||
| -rw-r--r-- | frontend/src/components/GameCategory.tsx | 37 | ||||
| -rw-r--r-- | frontend/src/components/GameEntry.tsx | 33 | ||||
| -rw-r--r-- | frontend/src/components/Leaderboards.tsx | 72 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 103 | ||||
| -rw-r--r-- | frontend/src/components/MapEntry.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/components/MessageDialog.tsx | 46 | ||||
| -rw-r--r-- | frontend/src/components/MessageDialogLoad.tsx | 44 | ||||
| -rw-r--r-- | frontend/src/components/ModMenu.tsx | 126 | ||||
| -rw-r--r-- | frontend/src/components/RankingEntry.tsx | 94 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 262 | ||||
| -rw-r--r-- | frontend/src/components/Summary.tsx | 265 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 366 |
14 files changed, 963 insertions, 614 deletions
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index 44a653b..925118d 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx | |||
| @@ -1,31 +1,36 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface ConfirmDialogProps { | 5 | interface ConfirmDialogProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | subtitle: string; | 7 | subtitle: string; |
| 8 | onConfirm: () => void; | 8 | onConfirm: () => void; |
| 9 | onCancel: () => void; | 9 | onCancel: () => void; |
| 10 | }; | 10 | } |
| 11 | 11 | ||
| 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ title, subtitle, onConfirm, onCancel }) => { | 12 | const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ |
| 13 | return ( | 13 | title, |
| 14 | <div className='dimmer'> | 14 | subtitle, |
| 15 | <div className='dialog'> | 15 | onConfirm, |
| 16 | <div className='dialog-element dialog-header'> | 16 | onCancel, |
| 17 | <span>{title}</span> | 17 | }) => { |
| 18 | </div> | 18 | return ( |
| 19 | <div className='dialog-element dialog-description'> | 19 | <div className="dimmer"> |
| 20 | <span>{subtitle}</span> | 20 | <div className="dialog"> |
| 21 | </div> | 21 | <div className="dialog-element dialog-header"> |
| 22 | <div className='dialog-element dialog-btns-container'> | 22 | <span>{title}</span> |
| 23 | <button onClick={onCancel}>Cancel</button> | 23 | </div> |
| 24 | <button onClick={onConfirm}>Confirm</button> | 24 | <div className="dialog-element dialog-description"> |
| 25 | </div> | 25 | <span>{subtitle}</span> |
| 26 | </div> | 26 | </div> |
| 27 | <div className="dialog-element dialog-btns-container"> | ||
| 28 | <button onClick={onCancel}>Cancel</button> | ||
| 29 | <button onClick={onConfirm}>Confirm</button> | ||
| 27 | </div> | 30 | </div> |
| 28 | ) | 31 | </div> |
| 32 | </div> | ||
| 33 | ); | ||
| 29 | }; | 34 | }; |
| 30 | 35 | ||
| 31 | export default ConfirmDialog; | 36 | export default ConfirmDialog; |
diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx index 62a9fc7..994cd8e 100644 --- a/frontend/src/components/Discussions.tsx +++ b/frontend/src/components/Discussions.tsx | |||
| @@ -1,16 +1,16 @@ | |||
| 1 | import React from "react"; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | MapDiscussion, | 4 | MapDiscussion, |
| 5 | MapDiscussions, | 5 | MapDiscussions, |
| 6 | MapDiscussionsDetail, | 6 | MapDiscussionsDetail, |
| 7 | } from "@customTypes/Map"; | 7 | } from '@customTypes/Map'; |
| 8 | import { MapDiscussionContent } from "@customTypes/Content"; | 8 | import { MapDiscussionContent } from '@customTypes/Content'; |
| 9 | import { time_ago } from "@utils/Time"; | 9 | import { time_ago } from '@utils/Time'; |
| 10 | import { API } from "@api/Api"; | 10 | import { API } from '@api/Api'; |
| 11 | import "@css/Maps.css"; | 11 | import '@css/Maps.css'; |
| 12 | import { Link } from "react-router-dom"; | 12 | import { Link } from 'react-router-dom'; |
| 13 | import useConfirm from "@hooks/UseConfirm"; | 13 | import useConfirm from '@hooks/UseConfirm'; |
| 14 | 14 | ||
| 15 | interface DiscussionsProps { | 15 | interface DiscussionsProps { |
| 16 | token?: string; | 16 | token?: string; |
| @@ -32,17 +32,17 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 32 | const [discussionThread, setDiscussionThread] = React.useState< | 32 | const [discussionThread, setDiscussionThread] = React.useState< |
| 33 | MapDiscussion | undefined | 33 | MapDiscussion | undefined |
| 34 | >(undefined); | 34 | >(undefined); |
| 35 | const [discussionSearch, setDiscussionSearch] = React.useState<string>(""); | 35 | const [discussionSearch, setDiscussionSearch] = React.useState<string>(''); |
| 36 | 36 | ||
| 37 | const [createDiscussion, setCreateDiscussion] = | 37 | const [createDiscussion, setCreateDiscussion] = |
| 38 | React.useState<boolean>(false); | 38 | React.useState<boolean>(false); |
| 39 | const [createDiscussionContent, setCreateDiscussionContent] = | 39 | const [createDiscussionContent, setCreateDiscussionContent] = |
| 40 | React.useState<MapDiscussionContent>({ | 40 | React.useState<MapDiscussionContent>({ |
| 41 | title: "", | 41 | title: '', |
| 42 | content: "", | 42 | content: '', |
| 43 | }); | 43 | }); |
| 44 | const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = | 44 | const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = |
| 45 | React.useState<string>(""); | 45 | React.useState<string>(''); |
| 46 | 46 | ||
| 47 | const _open_map_discussion = async (discussion_id: number) => { | 47 | const _open_map_discussion = async (discussion_id: number) => { |
| 48 | const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); | 48 | const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); |
| @@ -72,7 +72,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 72 | const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { | 72 | const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { |
| 73 | if ( | 73 | if ( |
| 74 | await confirm( | 74 | await confirm( |
| 75 | "Delete Map Discussion", | 75 | 'Delete Map Discussion', |
| 76 | `Are you sure you want to remove post: ${discussion.title}?` | 76 | `Are you sure you want to remove post: ${discussion.title}?` |
| 77 | ) | 77 | ) |
| 78 | ) { | 78 | ) { |
| @@ -90,8 +90,8 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 90 | <input | 90 | <input |
| 91 | type="text" | 91 | type="text" |
| 92 | value={discussionSearch} | 92 | value={discussionSearch} |
| 93 | placeholder={"Search for posts..."} | 93 | placeholder={'Search for posts...'} |
| 94 | onChange={(e) => setDiscussionSearch(e.target.value)} | 94 | onChange={e => setDiscussionSearch(e.target.value)} |
| 95 | /> | 95 | /> |
| 96 | <div> | 96 | <div> |
| 97 | <button onClick={() => setCreateDiscussion(true)}>New Post</button> | 97 | <button onClick={() => setCreateDiscussion(true)}>New Post</button> |
| @@ -104,11 +104,11 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 104 | <div id="discussion-create"> | 104 | <div id="discussion-create"> |
| 105 | <span>Create Post</span> | 105 | <span>Create Post</span> |
| 106 | <button onClick={() => setCreateDiscussion(false)}>X</button> | 106 | <button onClick={() => setCreateDiscussion(false)}>X</button> |
| 107 | <div style={{ gridColumn: "1 / span 2" }}> | 107 | <div style={{ gridColumn: '1 / span 2' }}> |
| 108 | <input | 108 | <input |
| 109 | id="discussion-create-title" | 109 | id="discussion-create-title" |
| 110 | placeholder="Title..." | 110 | placeholder="Title..." |
| 111 | onChange={(e) => | 111 | onChange={e => |
| 112 | setCreateDiscussionContent({ | 112 | setCreateDiscussionContent({ |
| 113 | ...createDiscussionContent, | 113 | ...createDiscussionContent, |
| 114 | title: e.target.value, | 114 | title: e.target.value, |
| @@ -118,7 +118,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 118 | <input | 118 | <input |
| 119 | id="discussion-create-content" | 119 | id="discussion-create-content" |
| 120 | placeholder="Enter the content..." | 120 | placeholder="Enter the content..." |
| 121 | onChange={(e) => | 121 | onChange={e => |
| 122 | setCreateDiscussionContent({ | 122 | setCreateDiscussionContent({ |
| 123 | ...createDiscussionContent, | 123 | ...createDiscussionContent, |
| 124 | content: e.target.value, | 124 | content: e.target.value, |
| @@ -126,7 +126,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 126 | } | 126 | } |
| 127 | /> | 127 | /> |
| 128 | </div> | 128 | </div> |
| 129 | <div style={{ placeItems: "end", gridColumn: "1 / span 2" }}> | 129 | <div style={{ placeItems: 'end', gridColumn: '1 / span 2' }}> |
| 130 | <button | 130 | <button |
| 131 | id="discussion-create-button" | 131 | id="discussion-create-button" |
| 132 | onClick={() => _create_map_discussion()} | 132 | onClick={() => _create_map_discussion()} |
| @@ -157,8 +157,8 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 157 | {time_ago( | 157 | {time_ago( |
| 158 | new Date( | 158 | new Date( |
| 159 | discussionThread.discussion.created_at | 159 | discussionThread.discussion.created_at |
| 160 | .replace("T", " ") | 160 | .replace('T', ' ') |
| 161 | .replace("Z", "") | 161 | .replace('Z', '') |
| 162 | ) | 162 | ) |
| 163 | )} | 163 | )} |
| 164 | </span> | 164 | </span> |
| @@ -170,7 +170,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 170 | (a, b) => | 170 | (a, b) => |
| 171 | new Date(a.date).getTime() - new Date(b.date).getTime() | 171 | new Date(a.date).getTime() - new Date(b.date).getTime() |
| 172 | ) | 172 | ) |
| 173 | .map((e) => ( | 173 | .map(e => ( |
| 174 | <> | 174 | <> |
| 175 | <Link to={`/users/${e.user.steam_id}`}> | 175 | <Link to={`/users/${e.user.steam_id}`}> |
| 176 | <img src={e.user.avatar_link} alt="" /> | 176 | <img src={e.user.avatar_link} alt="" /> |
| @@ -180,7 +180,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 180 | <span> | 180 | <span> |
| 181 | {time_ago( | 181 | {time_ago( |
| 182 | new Date( | 182 | new Date( |
| 183 | e.date.replace("T", " ").replace("Z", "") | 183 | e.date.replace('T', ' ').replace('Z', '') |
| 184 | ) | 184 | ) |
| 185 | )} | 185 | )} |
| 186 | </span> | 186 | </span> |
| @@ -188,29 +188,29 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 188 | </div> | 188 | </div> |
| 189 | </> | 189 | </> |
| 190 | )) | 190 | )) |
| 191 | : ""} | 191 | : ''} |
| 192 | </div> | 192 | </div> |
| 193 | <div id="discussion-send"> | 193 | <div id="discussion-send"> |
| 194 | <input | 194 | <input |
| 195 | type="text" | 195 | type="text" |
| 196 | value={createDiscussionCommentContent} | 196 | value={createDiscussionCommentContent} |
| 197 | placeholder={"Message"} | 197 | placeholder={'Message'} |
| 198 | onKeyDown={(e) => | 198 | onKeyDown={e => |
| 199 | e.key === "Enter" && | 199 | e.key === 'Enter' && |
| 200 | _create_map_discussion_comment(discussionThread.discussion.id) | 200 | _create_map_discussion_comment(discussionThread.discussion.id) |
| 201 | } | 201 | } |
| 202 | onChange={(e) => | 202 | onChange={e => |
| 203 | setCreateDiscussionCommentContent(e.target.value) | 203 | setCreateDiscussionCommentContent(e.target.value) |
| 204 | } | 204 | } |
| 205 | /> | 205 | /> |
| 206 | <div> | 206 | <div> |
| 207 | <button | 207 | <button |
| 208 | onClick={() => { | 208 | onClick={() => { |
| 209 | if (createDiscussionCommentContent !== "") { | 209 | if (createDiscussionCommentContent !== '') { |
| 210 | _create_map_discussion_comment( | 210 | _create_map_discussion_comment( |
| 211 | discussionThread.discussion.id | 211 | discussionThread.discussion.id |
| 212 | ); | 212 | ); |
| 213 | setCreateDiscussionCommentContent(""); | 213 | setCreateDiscussionCommentContent(''); |
| 214 | } | 214 | } |
| 215 | }} | 215 | }} |
| 216 | > | 216 | > |
| @@ -222,7 +222,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 222 | ) : data ? ( | 222 | ) : data ? ( |
| 223 | <> | 223 | <> |
| 224 | {data.discussions | 224 | {data.discussions |
| 225 | .filter((f) => f.title.includes(discussionSearch)) | 225 | .filter(f => f.title.includes(discussionSearch)) |
| 226 | .sort( | 226 | .sort( |
| 227 | (a, b) => | 227 | (a, b) => |
| 228 | new Date(b.updated_at).getTime() - | 228 | new Date(b.updated_at).getTime() - |
| @@ -234,7 +234,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 234 | <span>{e.title}</span> | 234 | <span>{e.title}</span> |
| 235 | {isModerator ? ( | 235 | {isModerator ? ( |
| 236 | <button | 236 | <button |
| 237 | onClick={(m) => { | 237 | onClick={m => { |
| 238 | m.stopPropagation(); | 238 | m.stopPropagation(); |
| 239 | _delete_map_discussion(e); | 239 | _delete_map_discussion(e); |
| 240 | }} | 240 | }} |
| @@ -248,10 +248,10 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 248 | <b>{e.creator.user_name}:</b> {e.content} | 248 | <b>{e.creator.user_name}:</b> {e.content} |
| 249 | </span> | 249 | </span> |
| 250 | <span> | 250 | <span> |
| 251 | Last Updated:{" "} | 251 | Last Updated:{' '} |
| 252 | {time_ago( | 252 | {time_ago( |
| 253 | new Date( | 253 | new Date( |
| 254 | e.updated_at.replace("T", " ").replace("Z", "") | 254 | e.updated_at.replace('T', ' ').replace('Z', '') |
| 255 | ) | 255 | ) |
| 256 | )} | 256 | )} |
| 257 | </span> | 257 | </span> |
| @@ -260,7 +260,7 @@ const Discussions: React.FC<DiscussionsProps> = ({ | |||
| 260 | ))} | 260 | ))} |
| 261 | </> | 261 | </> |
| 262 | ) : ( | 262 | ) : ( |
| 263 | <span style={{ textAlign: "center", display: "block" }}> | 263 | <span style={{ textAlign: 'center', display: 'block' }}> |
| 264 | No Discussions... | 264 | No Discussions... |
| 265 | </span> | 265 | </span> |
| 266 | ) | 266 | ) |
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index d8879ef..b13be48 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx | |||
| @@ -1,24 +1,31 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; |
| 5 | import "@css/Games.css" | 5 | import '@css/Games.css'; |
| 6 | 6 | ||
| 7 | interface GameCategoryProps { | 7 | interface GameCategoryProps { |
| 8 | game: Game; | 8 | game: Game; |
| 9 | cat: GameCategoryPortals; | 9 | cat: GameCategoryPortals; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | const GameCategory: React.FC<GameCategoryProps> = ({cat, game}) => { | 12 | const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { |
| 13 | return ( | 13 | return ( |
| 14 | <Link className="games-page-item-body-item" to={"/games/" + game.id + "?cat=" + cat.category.id}> | 14 | <Link |
| 15 | <div> | 15 | className="games-page-item-body-item" |
| 16 | <span className='games-page-item-body-item-title'>{cat.category.name}</span> | 16 | to={'/games/' + game.id + '?cat=' + cat.category.id} |
| 17 | <br /> | 17 | > |
| 18 | <span className='games-page-item-body-item-num'>{cat.portal_count}</span> | 18 | <div> |
| 19 | </div> | 19 | <span className="games-page-item-body-item-title"> |
| 20 | </Link> | 20 | {cat.category.name} |
| 21 | ) | 21 | </span> |
| 22 | } | 22 | <br /> |
| 23 | <span className="games-page-item-body-item-num"> | ||
| 24 | {cat.portal_count} | ||
| 25 | </span> | ||
| 26 | </div> | ||
| 27 | </Link> | ||
| 28 | ); | ||
| 29 | }; | ||
| 23 | 30 | ||
| 24 | export default GameCategory; | 31 | export default GameCategory; |
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 3bd2842..b58bbdd 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; | 4 | import { Game, GameCategoryPortals } from '@customTypes/Game'; |
| 5 | import "@css/Games.css" | 5 | import '@css/Games.css'; |
| 6 | 6 | ||
| 7 | import GameCategory from '@components/GameCategory'; | 7 | import GameCategory from '@components/GameCategory'; |
| 8 | 8 | ||
| @@ -18,17 +18,26 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => { | |||
| 18 | }, [game.category_portals]); | 18 | }, [game.category_portals]); |
| 19 | 19 | ||
| 20 | return ( | 20 | return ( |
| 21 | <Link to={"/games/" + game.id}><div className='games-page-item'> | 21 | <Link to={'/games/' + game.id}> |
| 22 | <div className='games-page-item-header'> | 22 | <div className="games-page-item"> |
| 23 | <div style={{ backgroundImage: `url(${game.image})` }} className='games-page-item-header-img'></div> | 23 | <div className="games-page-item-header"> |
| 24 | <span><b>{game.name}</b></span> | 24 | <div |
| 25 | style={{ backgroundImage: `url(${game.image})` }} | ||
| 26 | className="games-page-item-header-img" | ||
| 27 | ></div> | ||
| 28 | <span> | ||
| 29 | <b>{game.name}</b> | ||
| 30 | </span> | ||
| 31 | </div> | ||
| 32 | <div id={game.id as any as string} className="games-page-item-body"> | ||
| 33 | {catInfo.map((cat, index) => { | ||
| 34 | return ( | ||
| 35 | <GameCategory cat={cat} game={game} key={index}></GameCategory> | ||
| 36 | ); | ||
| 37 | })} | ||
| 38 | </div> | ||
| 25 | </div> | 39 | </div> |
| 26 | <div id={game.id as any as string} className='games-page-item-body'> | 40 | </Link> |
| 27 | {catInfo.map((cat, index) => { | ||
| 28 | return <GameCategory cat={cat} game={game} key={index}></GameCategory> | ||
| 29 | })} | ||
| 30 | </div> | ||
| 31 | </div></Link> | ||
| 32 | ); | 41 | ); |
| 33 | }; | 42 | }; |
| 34 | 43 | ||
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx index 05f01d0..fb72f2b 100644 --- a/frontend/src/components/Leaderboards.tsx +++ b/frontend/src/components/Leaderboards.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | import React from "react"; | 1 | import React from 'react'; |
| 2 | import { Link, useNavigate } from "react-router-dom"; | 2 | import { Link, useNavigate } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | import { DownloadIcon, ThreedotIcon } from "@images/Images"; | 4 | import { DownloadIcon, ThreedotIcon } from '@images/Images'; |
| 5 | import { MapLeaderboard } from "@customTypes/Map"; | 5 | import { MapLeaderboard } from '@customTypes/Map'; |
| 6 | import { ticks_to_time, time_ago } from "@utils/Time"; | 6 | import { ticks_to_time, time_ago } from '@utils/Time'; |
| 7 | import { API } from "@api/Api"; | 7 | import { API } from '@api/Api'; |
| 8 | import useMessage from "@hooks/UseMessage"; | 8 | import useMessage from '@hooks/UseMessage'; |
| 9 | import "@css/Maps.css"; | 9 | import '@css/Maps.css'; |
| 10 | 10 | ||
| 11 | interface LeaderboardsProps { | 11 | interface LeaderboardsProps { |
| 12 | mapID: string; | 12 | mapID: string; |
| @@ -35,7 +35,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 35 | if (!data) { | 35 | if (!data) { |
| 36 | return ( | 36 | return ( |
| 37 | <section id="section6" className="summary2"> | 37 | <section id="section6" className="summary2"> |
| 38 | <h1 style={{ textAlign: "center" }}> | 38 | <h1 style={{ textAlign: 'center' }}> |
| 39 | Map is not available for competitive boards. | 39 | Map is not available for competitive boards. |
| 40 | </h1> | 40 | </h1> |
| 41 | </section> | 41 | </section> |
| @@ -45,7 +45,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 45 | if (data.records.length === 0) { | 45 | if (data.records.length === 0) { |
| 46 | return ( | 46 | return ( |
| 47 | <section id="section6" className="summary2"> | 47 | <section id="section6" className="summary2"> |
| 48 | <h1 style={{ textAlign: "center" }}>No records found.</h1> | 48 | <h1 style={{ textAlign: 'center' }}>No records found.</h1> |
| 49 | </section> | 49 | </section> |
| 50 | ); | 50 | ); |
| 51 | } | 51 | } |
| @@ -58,8 +58,8 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 58 | id="leaderboard-top" | 58 | id="leaderboard-top" |
| 59 | style={ | 59 | style={ |
| 60 | data.map.is_coop | 60 | data.map.is_coop |
| 61 | ? { gridTemplateColumns: "7.5% 40% 7.5% 15% 15% 15%" } | 61 | ? { gridTemplateColumns: '7.5% 40% 7.5% 15% 15% 15%' } |
| 62 | : { gridTemplateColumns: "7.5% 30% 10% 20% 17.5% 15%" } | 62 | : { gridTemplateColumns: '7.5% 30% 10% 20% 17.5% 15%' } |
| 63 | } | 63 | } |
| 64 | > | 64 | > |
| 65 | <span>Place</span> | 65 | <span>Place</span> |
| @@ -82,13 +82,13 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 82 | onClick={() => | 82 | onClick={() => |
| 83 | pageNumber === 1 | 83 | pageNumber === 1 |
| 84 | ? null | 84 | ? null |
| 85 | : setPageNumber((prevPageNumber) => prevPageNumber - 1) | 85 | : setPageNumber(prevPageNumber => prevPageNumber - 1) |
| 86 | } | 86 | } |
| 87 | > | 87 | > |
| 88 | <i | 88 | <i |
| 89 | className="triangle" | 89 | className="triangle" |
| 90 | style={{ position: "relative", left: "-5px" }} | 90 | style={{ position: 'relative', left: '-5px' }} |
| 91 | ></i>{" "} | 91 | ></i>{' '} |
| 92 | </button> | 92 | </button> |
| 93 | <span> | 93 | <span> |
| 94 | {data.pagination.current_page}/{data.pagination.total_pages} | 94 | {data.pagination.current_page}/{data.pagination.total_pages} |
| @@ -97,17 +97,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 97 | onClick={() => | 97 | onClick={() => |
| 98 | pageNumber === data.pagination.total_pages | 98 | pageNumber === data.pagination.total_pages |
| 99 | ? null | 99 | ? null |
| 100 | : setPageNumber((prevPageNumber) => prevPageNumber + 1) | 100 | : setPageNumber(prevPageNumber => prevPageNumber + 1) |
| 101 | } | 101 | } |
| 102 | > | 102 | > |
| 103 | <i | 103 | <i |
| 104 | className="triangle" | 104 | className="triangle" |
| 105 | style={{ | 105 | style={{ |
| 106 | position: "relative", | 106 | position: 'relative', |
| 107 | left: "5px", | 107 | left: '5px', |
| 108 | transform: "rotate(180deg)", | 108 | transform: 'rotate(180deg)', |
| 109 | }} | 109 | }} |
| 110 | ></i>{" "} | 110 | ></i>{' '} |
| 111 | </button> | 111 | </button> |
| 112 | </div> | 112 | </div> |
| 113 | </div> | 113 | </div> |
| @@ -120,33 +120,33 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 120 | key={index} | 120 | key={index} |
| 121 | style={ | 121 | style={ |
| 122 | data.map.is_coop | 122 | data.map.is_coop |
| 123 | ? { gridTemplateColumns: "3% 4.5% 40% 4% 3.5% 15% 15% 14.5%" } | 123 | ? { gridTemplateColumns: '3% 4.5% 40% 4% 3.5% 15% 15% 14.5%' } |
| 124 | : { gridTemplateColumns: "3% 4.5% 30% 4% 6% 20% 17% 15%" } | 124 | : { gridTemplateColumns: '3% 4.5% 30% 4% 6% 20% 17% 15%' } |
| 125 | } | 125 | } |
| 126 | > | 126 | > |
| 127 | <span>{r.placement}</span> | 127 | <span>{r.placement}</span> |
| 128 | <span> </span> | 128 | <span> </span> |
| 129 | {r.kind === "multiplayer" ? ( | 129 | {r.kind === 'multiplayer' ? ( |
| 130 | <div> | 130 | <div> |
| 131 | <Link to={`/users/${r.host.steam_id}`}> | 131 | <Link to={`/users/${r.host.steam_id}`}> |
| 132 | <span> | 132 | <span> |
| 133 | <img src={r.host.avatar_link} alt="" /> {" "} | 133 | <img src={r.host.avatar_link} alt="" /> {' '} |
| 134 | {r.host.user_name} | 134 | {r.host.user_name} |
| 135 | </span> | 135 | </span> |
| 136 | </Link> | 136 | </Link> |
| 137 | <Link to={`/users/${r.partner.steam_id}`}> | 137 | <Link to={`/users/${r.partner.steam_id}`}> |
| 138 | <span> | 138 | <span> |
| 139 | <img src={r.partner.avatar_link} alt="" /> {" "} | 139 | <img src={r.partner.avatar_link} alt="" /> {' '} |
| 140 | {r.partner.user_name} | 140 | {r.partner.user_name} |
| 141 | </span> | 141 | </span> |
| 142 | </Link> | 142 | </Link> |
| 143 | </div> | 143 | </div> |
| 144 | ) : ( | 144 | ) : ( |
| 145 | r.kind === "singleplayer" && ( | 145 | r.kind === 'singleplayer' && ( |
| 146 | <div> | 146 | <div> |
| 147 | <Link to={`/users/${r.user.steam_id}`}> | 147 | <Link to={`/users/${r.user.steam_id}`}> |
| 148 | <span> | 148 | <span> |
| 149 | <img src={r.user.avatar_link} alt="" /> {" "} | 149 | <img src={r.user.avatar_link} alt="" /> {' '} |
| 150 | {r.user.user_name} | 150 | {r.user.user_name} |
| 151 | </span> | 151 | </span> |
| 152 | </Link> | 152 | </Link> |
| @@ -158,25 +158,25 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 158 | <span> </span> | 158 | <span> </span> |
| 159 | <span | 159 | <span |
| 160 | className="hover-popup" | 160 | className="hover-popup" |
| 161 | popup-text={r.score_time + " ticks"} | 161 | popup-text={r.score_time + ' ticks'} |
| 162 | > | 162 | > |
| 163 | {ticks_to_time(r.score_time)} | 163 | {ticks_to_time(r.score_time)} |
| 164 | </span> | 164 | </span> |
| 165 | <span | 165 | <span |
| 166 | className="hover-popup" | 166 | className="hover-popup" |
| 167 | popup-text={r.record_date.replace("T", " ").split(".")[0]} | 167 | popup-text={r.record_date.replace('T', ' ').split('.')[0]} |
| 168 | > | 168 | > |
| 169 | {time_ago( | 169 | {time_ago( |
| 170 | new Date(r.record_date.replace("T", " ").replace("Z", "")) | 170 | new Date(r.record_date.replace('T', ' ').replace('Z', '')) |
| 171 | )} | 171 | )} |
| 172 | </span> | 172 | </span> |
| 173 | 173 | ||
| 174 | {r.kind === "multiplayer" ? ( | 174 | {r.kind === 'multiplayer' ? ( |
| 175 | <span> | 175 | <span> |
| 176 | <button | 176 | <button |
| 177 | onClick={() => { | 177 | onClick={() => { |
| 178 | message( | 178 | message( |
| 179 | "Demo Information", | 179 | 'Demo Information', |
| 180 | `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}` | 180 | `Host Demo ID: ${r.host_demo_id} \nParnter Demo ID: ${r.partner_demo_id}` |
| 181 | ); | 181 | ); |
| 182 | }} | 182 | }} |
| @@ -193,7 +193,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 193 | alt="download" | 193 | alt="download" |
| 194 | style={{ | 194 | style={{ |
| 195 | filter: | 195 | filter: |
| 196 | "hue-rotate(160deg) contrast(60%) saturate(1000%)", | 196 | 'hue-rotate(160deg) contrast(60%) saturate(1000%)', |
| 197 | }} | 197 | }} |
| 198 | /> | 198 | /> |
| 199 | </button> | 199 | </button> |
| @@ -207,17 +207,17 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 207 | alt="download" | 207 | alt="download" |
| 208 | style={{ | 208 | style={{ |
| 209 | filter: | 209 | filter: |
| 210 | "hue-rotate(300deg) contrast(60%) saturate(1000%)", | 210 | 'hue-rotate(300deg) contrast(60%) saturate(1000%)', |
| 211 | }} | 211 | }} |
| 212 | /> | 212 | /> |
| 213 | </button> | 213 | </button> |
| 214 | </span> | 214 | </span> |
| 215 | ) : ( | 215 | ) : ( |
| 216 | r.kind === "singleplayer" && ( | 216 | r.kind === 'singleplayer' && ( |
| 217 | <span> | 217 | <span> |
| 218 | <button | 218 | <button |
| 219 | onClick={() => { | 219 | onClick={() => { |
| 220 | message("Demo Information", `Demo ID: ${r.demo_id}`); | 220 | message('Demo Information', `Demo ID: ${r.demo_id}`); |
| 221 | }} | 221 | }} |
| 222 | > | 222 | > |
| 223 | <img src={ThreedotIcon} alt="demo_id" /> | 223 | <img src={ThreedotIcon} alt="demo_id" /> |
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index f1628b2..093e406 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx | |||
| @@ -4,77 +4,78 @@ import { Link, useNavigate } from 'react-router-dom'; | |||
| 4 | import { ExitIcon, UserIcon, LoginIcon } from '@images/Images'; | 4 | import { ExitIcon, UserIcon, LoginIcon } from '@images/Images'; |
| 5 | import { UserProfile } from '@customTypes/Profile'; | 5 | import { UserProfile } from '@customTypes/Profile'; |
| 6 | import { API } from '@api/Api'; | 6 | import { API } from '@api/Api'; |
| 7 | import "@css/Login.css"; | 7 | import '@css/Login.css'; |
| 8 | 8 | ||
| 9 | interface LoginProps { | 9 | interface LoginProps { |
| 10 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | 10 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; |
| 11 | profile?: UserProfile; | 11 | profile?: UserProfile; |
| 12 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | 12 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; |
| 13 | }; | 13 | } |
| 14 | 14 | ||
| 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { |
| 16 | |||
| 17 | const navigate = useNavigate(); | 16 | const navigate = useNavigate(); |
| 18 | 17 | ||
| 19 | const _login = () => { | 18 | const _login = () => { |
| 20 | window.location.href = "/api/v1/login"; | 19 | window.location.href = '/api/v1/login'; |
| 21 | }; | 20 | }; |
| 22 | 21 | ||
| 23 | const _logout = () => { | 22 | const _logout = () => { |
| 24 | setProfile(undefined); | 23 | setProfile(undefined); |
| 25 | setToken(undefined); | 24 | setToken(undefined); |
| 26 | API.delete_token(); | 25 | API.delete_token(); |
| 27 | navigate("/"); | 26 | navigate('/'); |
| 28 | }; | 27 | }; |
| 29 | 28 | ||
| 30 | return ( | 29 | return ( |
| 31 | <> | 30 | <> |
| 32 | {profile | 31 | {profile ? ( |
| 33 | ? | 32 | <> |
| 34 | ( | 33 | {profile.profile ? ( |
| 35 | <> | 34 | <> |
| 36 | {profile.profile ? | 35 | <Link to="/profile" tabIndex={-1} className="login"> |
| 37 | ( | 36 | <button className="sidebar-button"> |
| 38 | <> | 37 | <img |
| 39 | <Link to="/profile" tabIndex={-1} className='login'> | 38 | className="avatar-img" |
| 40 | <button className='sidebar-button'> | 39 | src={profile.avatar_link} |
| 41 | <img className="avatar-img" src={profile.avatar_link} alt="" /> | 40 | alt="" |
| 42 | <span>{profile.user_name}</span> | 41 | /> |
| 43 | </button> | 42 | <span>{profile.user_name}</span> |
| 44 | <button className='logout-button' onClick={_logout}> | 43 | </button> |
| 45 | <img src={ExitIcon} alt="" /><span /> | 44 | <button className="logout-button" onClick={_logout}> |
| 46 | </button> | 45 | <img src={ExitIcon} alt="" /> |
| 47 | </Link> | 46 | <span /> |
| 48 | </> | 47 | </button> |
| 49 | ) | 48 | </Link> |
| 50 | : | 49 | </> |
| 51 | ( | 50 | ) : ( |
| 52 | <> | 51 | <> |
| 53 | <Link to="/" tabIndex={-1} className='login'> | 52 | <Link to="/" tabIndex={-1} className="login"> |
| 54 | <button className='sidebar-button'> | 53 | <button className="sidebar-button"> |
| 55 | <img className="avatar-img" src={profile.avatar_link} alt="" /> | 54 | <img |
| 56 | <span>Loading Profile...</span> | 55 | className="avatar-img" |
| 57 | </button> | 56 | src={profile.avatar_link} |
| 58 | <button disabled className='logout-button' onClick={_logout}> | 57 | alt="" |
| 59 | <img src={ExitIcon} alt="" /><span /> | 58 | /> |
| 60 | </button> | 59 | <span>Loading Profile...</span> |
| 61 | </Link> | 60 | </button> |
| 62 | </> | 61 | <button disabled className="logout-button" onClick={_logout}> |
| 63 | ) | 62 | <img src={ExitIcon} alt="" /> |
| 64 | } | 63 | <span /> |
| 65 | </> | 64 | </button> |
| 66 | ) | 65 | </Link> |
| 67 | : | 66 | </> |
| 68 | ( | 67 | )} |
| 69 | <Link to="/api/v1/login" tabIndex={-1} className='login' > | 68 | </> |
| 70 | <button className='sidebar-button' onClick={_login}> | 69 | ) : ( |
| 71 | <img className="avatar-img" src={UserIcon} alt="" /> | 70 | <Link to="/api/v1/login" tabIndex={-1} className="login"> |
| 72 | <span> | 71 | <button className="sidebar-button" onClick={_login}> |
| 73 | <img src={LoginIcon} alt="Sign in through Steam" /> | 72 | <img className="avatar-img" src={UserIcon} alt="" /> |
| 74 | </span> | 73 | <span> |
| 75 | </button> | 74 | <img src={LoginIcon} alt="Sign in through Steam" /> |
| 76 | </Link> | 75 | </span> |
| 77 | )} | 76 | </button> |
| 77 | </Link> | ||
| 78 | )} | ||
| 78 | </> | 79 | </> |
| 79 | ); | 80 | ); |
| 80 | }; | 81 | }; |
diff --git a/frontend/src/components/MapEntry.tsx b/frontend/src/components/MapEntry.tsx index 0f494ad..f1dee5b 100644 --- a/frontend/src/components/MapEntry.tsx +++ b/frontend/src/components/MapEntry.tsx | |||
| @@ -1,12 +1,8 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | const MapEntry: React.FC = () => { | 4 | const MapEntry: React.FC = () => { |
| 5 | return ( | 5 | return <div></div>; |
| 6 | <div> | 6 | }; |
| 7 | |||
| 8 | </div> | ||
| 9 | ) | ||
| 10 | } | ||
| 11 | 7 | ||
| 12 | export default MapEntry; | 8 | export default MapEntry; |
diff --git a/frontend/src/components/MessageDialog.tsx b/frontend/src/components/MessageDialog.tsx index 5c85189..b739ebc 100644 --- a/frontend/src/components/MessageDialog.tsx +++ b/frontend/src/components/MessageDialog.tsx | |||
| @@ -1,29 +1,33 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface MessageDialogProps { | 5 | interface MessageDialogProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | subtitle: string; | 7 | subtitle: string; |
| 8 | onClose: () => void; | 8 | onClose: () => void; |
| 9 | }; | 9 | } |
| 10 | 10 | ||
| 11 | const MessageDialog: React.FC<MessageDialogProps> = ({ title, subtitle, onClose }) => { | 11 | const 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 | ||
| 29 | export default MessageDialog; | 33 | export default MessageDialog; |
diff --git a/frontend/src/components/MessageDialogLoad.tsx b/frontend/src/components/MessageDialogLoad.tsx index 966e064..acea27d 100644 --- a/frontend/src/components/MessageDialogLoad.tsx +++ b/frontend/src/components/MessageDialogLoad.tsx | |||
| @@ -1,29 +1,31 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css" | 3 | import '@css/Dialog.css'; |
| 4 | 4 | ||
| 5 | interface MessageDialogLoadProps { | 5 | interface MessageDialogLoadProps { |
| 6 | title: string; | 6 | title: string; |
| 7 | onClose: () => void; | 7 | onClose: () => void; |
| 8 | }; | 8 | } |
| 9 | 9 | ||
| 10 | const MessageDialogLoad: React.FC<MessageDialogLoadProps> = ({ title, onClose }) => { | 10 | const 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 | ||
| 29 | export default MessageDialogLoad; | 31 | export default MessageDialogLoad; |
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx index f765cd8..140d6a3 100644 --- a/frontend/src/components/ModMenu.tsx +++ b/frontend/src/components/ModMenu.tsx | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | import React from "react"; | 1 | import React from 'react'; |
| 2 | import ReactMarkdown from "react-markdown"; | 2 | import ReactMarkdown from 'react-markdown'; |
| 3 | import { useNavigate } from "react-router-dom"; | 3 | import { useNavigate } from 'react-router-dom'; |
| 4 | 4 | ||
| 5 | import { MapSummary } from "@customTypes/Map"; | 5 | import { MapSummary } from '@customTypes/Map'; |
| 6 | import { ModMenuContent } from "@customTypes/Content"; | 6 | import { ModMenuContent } from '@customTypes/Content'; |
| 7 | import { API } from "@api/Api"; | 7 | import { API } from '@api/Api'; |
| 8 | import "@css/ModMenu.css"; | 8 | import '@css/ModMenu.css'; |
| 9 | import useConfirm from "@hooks/UseConfirm"; | 9 | import useConfirm from '@hooks/UseConfirm'; |
| 10 | 10 | ||
| 11 | interface ModMenuProps { | 11 | interface ModMenuProps { |
| 12 | token?: string; | 12 | token?: string; |
| @@ -28,26 +28,26 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 28 | 28 | ||
| 29 | const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ | 29 | const [routeContent, setRouteContent] = React.useState<ModMenuContent>({ |
| 30 | id: 0, | 30 | id: 0, |
| 31 | name: "", | 31 | name: '', |
| 32 | score: 0, | 32 | score: 0, |
| 33 | date: "", | 33 | date: '', |
| 34 | showcase: "", | 34 | showcase: '', |
| 35 | description: "No description available.", | 35 | description: 'No description available.', |
| 36 | category_id: 1, | 36 | category_id: 1, |
| 37 | }); | 37 | }); |
| 38 | 38 | ||
| 39 | const [image, setImage] = React.useState<string>(""); | 39 | const [image, setImage] = React.useState<string>(''); |
| 40 | const [md, setMd] = React.useState<string>(""); | 40 | const [md, setMd] = React.useState<string>(''); |
| 41 | 41 | ||
| 42 | const navigate = useNavigate(); | 42 | const navigate = useNavigate(); |
| 43 | 43 | ||
| 44 | function compressImage(file: File): Promise<string> { | 44 | function compressImage(file: File): Promise<string> { |
| 45 | const reader = new FileReader(); | 45 | const reader = new FileReader(); |
| 46 | reader.readAsDataURL(file); | 46 | reader.readAsDataURL(file); |
| 47 | return new Promise((resolve) => { | 47 | return new Promise(resolve => { |
| 48 | reader.onload = () => { | 48 | reader.onload = () => { |
| 49 | const img = new Image(); | 49 | const img = new Image(); |
| 50 | if (typeof reader.result === "string") { | 50 | if (typeof reader.result === 'string') { |
| 51 | img.src = reader.result; | 51 | img.src = reader.result; |
| 52 | img.onload = () => { | 52 | img.onload = () => { |
| 53 | let { width, height } = img; | 53 | let { width, height } = img; |
| @@ -59,10 +59,10 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 59 | width *= 320 / height; | 59 | width *= 320 / height; |
| 60 | height = 320; | 60 | height = 320; |
| 61 | } | 61 | } |
| 62 | const canvas = document.createElement("canvas"); | 62 | const canvas = document.createElement('canvas'); |
| 63 | canvas.width = width; | 63 | canvas.width = width; |
| 64 | canvas.height = height; | 64 | canvas.height = height; |
| 65 | canvas.getContext("2d")!.drawImage(img, 0, 0, width, height); | 65 | canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); |
| 66 | resolve(canvas.toDataURL(file.type, 0.6)); | 66 | resolve(canvas.toDataURL(file.type, 0.6)); |
| 67 | }; | 67 | }; |
| 68 | } | 68 | } |
| @@ -73,8 +73,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 73 | const _edit_map_summary_image = async () => { | 73 | const _edit_map_summary_image = async () => { |
| 74 | if ( | 74 | if ( |
| 75 | await confirm( | 75 | await confirm( |
| 76 | "Edit Map Summary Image", | 76 | 'Edit Map Summary Image', |
| 77 | "Are you sure you want to submit this to the database?" | 77 | 'Are you sure you want to submit this to the database?' |
| 78 | ) | 78 | ) |
| 79 | ) { | 79 | ) { |
| 80 | if (token) { | 80 | if (token) { |
| @@ -82,7 +82,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 82 | if (success) { | 82 | if (success) { |
| 83 | navigate(0); | 83 | navigate(0); |
| 84 | } else { | 84 | } else { |
| 85 | alert("Error. Check logs."); | 85 | alert('Error. Check logs.'); |
| 86 | } | 86 | } |
| 87 | } | 87 | } |
| 88 | } | 88 | } |
| @@ -91,17 +91,17 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 91 | const _edit_map_summary_route = async () => { | 91 | const _edit_map_summary_route = async () => { |
| 92 | if ( | 92 | if ( |
| 93 | await confirm( | 93 | await confirm( |
| 94 | "Edit Map Summary Route", | 94 | 'Edit Map Summary Route', |
| 95 | "Are you sure you want to submit this to the database?" | 95 | 'Are you sure you want to submit this to the database?' |
| 96 | ) | 96 | ) |
| 97 | ) { | 97 | ) { |
| 98 | if (token) { | 98 | if (token) { |
| 99 | routeContent.date += "T00:00:00Z"; | 99 | routeContent.date += 'T00:00:00Z'; |
| 100 | const success = await API.put_map_summary(token, mapID, routeContent); | 100 | const success = await API.put_map_summary(token, mapID, routeContent); |
| 101 | if (success) { | 101 | if (success) { |
| 102 | navigate(0); | 102 | navigate(0); |
| 103 | } else { | 103 | } else { |
| 104 | alert("Error. Check logs."); | 104 | alert('Error. Check logs.'); |
| 105 | } | 105 | } |
| 106 | } | 106 | } |
| 107 | } | 107 | } |
| @@ -110,17 +110,17 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 110 | const _create_map_summary_route = async () => { | 110 | const _create_map_summary_route = async () => { |
| 111 | if ( | 111 | if ( |
| 112 | await confirm( | 112 | await confirm( |
| 113 | "Create Map Summary Route", | 113 | 'Create Map Summary Route', |
| 114 | "Are you sure you want to submit this to the database?" | 114 | 'Are you sure you want to submit this to the database?' |
| 115 | ) | 115 | ) |
| 116 | ) { | 116 | ) { |
| 117 | if (token) { | 117 | if (token) { |
| 118 | routeContent.date += "T00:00:00Z"; | 118 | routeContent.date += 'T00:00:00Z'; |
| 119 | const success = await API.post_map_summary(token, mapID, routeContent); | 119 | const success = await API.post_map_summary(token, mapID, routeContent); |
| 120 | if (success) { | 120 | if (success) { |
| 121 | navigate(0); | 121 | navigate(0); |
| 122 | } else { | 122 | } else { |
| 123 | alert("Error. Check logs."); | 123 | alert('Error. Check logs.'); |
| 124 | } | 124 | } |
| 125 | } | 125 | } |
| 126 | } | 126 | } |
| @@ -129,7 +129,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 129 | const _delete_map_summary_route = async () => { | 129 | const _delete_map_summary_route = async () => { |
| 130 | if ( | 130 | if ( |
| 131 | await confirm( | 131 | await confirm( |
| 132 | "Delete Map Summary Route", | 132 | 'Delete Map Summary Route', |
| 133 | `Are you sure you want to submit this to the database?\n | 133 | `Are you sure you want to submit this to the database?\n |
| 134 | ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}` | 134 | ${data.summary.routes[selectedRun].category.name}\n${data.summary.routes[selectedRun].history.score_count} portals\n${data.summary.routes[selectedRun].history.runner_name}` |
| 135 | ) | 135 | ) |
| @@ -143,7 +143,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 143 | if (success) { | 143 | if (success) { |
| 144 | navigate(0); | 144 | navigate(0); |
| 145 | } else { | 145 | } else { |
| 146 | alert("Error. Check logs."); | 146 | alert('Error. Check logs.'); |
| 147 | } | 147 | } |
| 148 | } | 148 | } |
| 149 | } | 149 | } |
| @@ -154,14 +154,14 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 154 | // add route | 154 | // add route |
| 155 | setRouteContent({ | 155 | setRouteContent({ |
| 156 | id: 0, | 156 | id: 0, |
| 157 | name: "", | 157 | name: '', |
| 158 | score: 0, | 158 | score: 0, |
| 159 | date: "", | 159 | date: '', |
| 160 | showcase: "", | 160 | showcase: '', |
| 161 | description: "No description available.", | 161 | description: 'No description available.', |
| 162 | category_id: 1, | 162 | category_id: 1, |
| 163 | }); | 163 | }); |
| 164 | setMd("No description available."); | 164 | setMd('No description available.'); |
| 165 | } | 165 | } |
| 166 | if (menu === 2) { | 166 | if (menu === 2) { |
| 167 | // edit route | 167 | // edit route |
| @@ -169,7 +169,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 169 | id: data.summary.routes[selectedRun].route_id, | 169 | id: data.summary.routes[selectedRun].route_id, |
| 170 | name: data.summary.routes[selectedRun].history.runner_name, | 170 | name: data.summary.routes[selectedRun].history.runner_name, |
| 171 | score: data.summary.routes[selectedRun].history.score_count, | 171 | score: data.summary.routes[selectedRun].history.score_count, |
| 172 | date: data.summary.routes[selectedRun].history.date.split("T")[0], | 172 | date: data.summary.routes[selectedRun].history.date.split('T')[0], |
| 173 | showcase: data.summary.routes[selectedRun].showcase, | 173 | showcase: data.summary.routes[selectedRun].showcase, |
| 174 | description: data.summary.routes[selectedRun].description, | 174 | description: data.summary.routes[selectedRun].description, |
| 175 | category_id: data.summary.routes[selectedRun].category.id, | 175 | category_id: data.summary.routes[selectedRun].category.id, |
| @@ -179,20 +179,20 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 179 | }, [menu, data.summary.routes, selectedRun]); | 179 | }, [menu, data.summary.routes, selectedRun]); |
| 180 | 180 | ||
| 181 | React.useEffect(() => { | 181 | React.useEffect(() => { |
| 182 | const modview = document.querySelector("div#modview") as HTMLElement; | 182 | const modview = document.querySelector('div#modview') as HTMLElement; |
| 183 | if (modview) { | 183 | if (modview) { |
| 184 | showButton | 184 | showButton |
| 185 | ? (modview.style.transform = "translateY(-68%)") | 185 | ? (modview.style.transform = 'translateY(-68%)') |
| 186 | : (modview.style.transform = "translateY(0%)"); | 186 | : (modview.style.transform = 'translateY(0%)'); |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | const modview_block = document.querySelector( | 189 | const modview_block = document.querySelector( |
| 190 | "#modview_block" | 190 | '#modview_block' |
| 191 | ) as HTMLElement; | 191 | ) as HTMLElement; |
| 192 | if (modview_block) { | 192 | if (modview_block) { |
| 193 | showButton | 193 | showButton |
| 194 | ? (modview_block.style.display = "none") | 194 | ? (modview_block.style.display = 'none') |
| 195 | : (modview_block.style.display = "block"); | 195 | : (modview_block.style.display = 'block'); |
| 196 | } | 196 | } |
| 197 | }, [showButton]); | 197 | }, [showButton]); |
| 198 | 198 | ||
| @@ -240,11 +240,9 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 240 | <input | 240 | <input |
| 241 | type="file" | 241 | type="file" |
| 242 | accept="image/*" | 242 | accept="image/*" |
| 243 | onChange={(e) => { | 243 | onChange={e => { |
| 244 | if (e.target.files) { | 244 | if (e.target.files) { |
| 245 | compressImage(e.target.files[0]).then((d) => | 245 | compressImage(e.target.files[0]).then(d => setImage(d)); |
| 246 | setImage(d) | ||
| 247 | ); | ||
| 248 | } | 246 | } |
| 249 | }} | 247 | }} |
| 250 | /> | 248 | /> |
| @@ -275,7 +273,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 275 | <input | 273 | <input |
| 276 | type="text" | 274 | type="text" |
| 277 | value={routeContent.name} | 275 | value={routeContent.name} |
| 278 | onChange={(e) => { | 276 | onChange={e => { |
| 279 | setRouteContent({ | 277 | setRouteContent({ |
| 280 | ...routeContent, | 278 | ...routeContent, |
| 281 | name: e.target.value, | 279 | name: e.target.value, |
| @@ -288,7 +286,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 288 | <input | 286 | <input |
| 289 | type="number" | 287 | type="number" |
| 290 | value={routeContent.score} | 288 | value={routeContent.score} |
| 291 | onChange={(e) => { | 289 | onChange={e => { |
| 292 | setRouteContent({ | 290 | setRouteContent({ |
| 293 | ...routeContent, | 291 | ...routeContent, |
| 294 | score: parseInt(e.target.value), | 292 | score: parseInt(e.target.value), |
| @@ -301,7 +299,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 301 | <input | 299 | <input |
| 302 | type="date" | 300 | type="date" |
| 303 | value={routeContent.date} | 301 | value={routeContent.date} |
| 304 | onChange={(e) => { | 302 | onChange={e => { |
| 305 | setRouteContent({ | 303 | setRouteContent({ |
| 306 | ...routeContent, | 304 | ...routeContent, |
| 307 | date: e.target.value, | 305 | date: e.target.value, |
| @@ -314,7 +312,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 314 | <input | 312 | <input |
| 315 | type="text" | 313 | type="text" |
| 316 | value={routeContent.showcase} | 314 | value={routeContent.showcase} |
| 317 | onChange={(e) => { | 315 | onChange={e => { |
| 318 | setRouteContent({ | 316 | setRouteContent({ |
| 319 | ...routeContent, | 317 | ...routeContent, |
| 320 | showcase: e.target.value, | 318 | showcase: e.target.value, |
| @@ -324,12 +322,12 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 324 | </div> | 322 | </div> |
| 325 | <div | 323 | <div |
| 326 | id="modview-route-description" | 324 | id="modview-route-description" |
| 327 | style={{ height: "180px", gridColumn: "1 / span 5" }} | 325 | style={{ height: '180px', gridColumn: '1 / span 5' }} |
| 328 | > | 326 | > |
| 329 | <span>Description:</span> | 327 | <span>Description:</span> |
| 330 | <textarea | 328 | <textarea |
| 331 | value={routeContent.description} | 329 | value={routeContent.description} |
| 332 | onChange={(e) => { | 330 | onChange={e => { |
| 333 | setRouteContent({ | 331 | setRouteContent({ |
| 334 | ...routeContent, | 332 | ...routeContent, |
| 335 | description: e.target.value, | 333 | description: e.target.value, |
| @@ -339,7 +337,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 339 | /> | 337 | /> |
| 340 | </div> | 338 | </div> |
| 341 | <button | 339 | <button |
| 342 | style={{ gridColumn: "2 / span 3", height: "40px" }} | 340 | style={{ gridColumn: '2 / span 3', height: '40px' }} |
| 343 | onClick={_edit_map_summary_route} | 341 | onClick={_edit_map_summary_route} |
| 344 | > | 342 | > |
| 345 | Apply | 343 | Apply |
| @@ -380,7 +378,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 380 | <div id="modview-route-category"> | 378 | <div id="modview-route-category"> |
| 381 | <span>Category:</span> | 379 | <span>Category:</span> |
| 382 | <select | 380 | <select |
| 383 | onChange={(e) => { | 381 | onChange={e => { |
| 384 | setRouteContent({ | 382 | setRouteContent({ |
| 385 | ...routeContent, | 383 | ...routeContent, |
| 386 | category_id: parseInt(e.target.value), | 384 | category_id: parseInt(e.target.value), |
| @@ -393,8 +391,8 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 393 | <option value="2" key="2"> | 391 | <option value="2" key="2"> |
| 394 | No SLA | 392 | No SLA |
| 395 | </option> | 393 | </option> |
| 396 | {data.map.game_name === "Portal 2 - Cooperative" ? ( | 394 | {data.map.game_name === 'Portal 2 - Cooperative' ? ( |
| 397 | "" | 395 | '' |
| 398 | ) : ( | 396 | ) : ( |
| 399 | <option value="3" key="3"> | 397 | <option value="3" key="3"> |
| 400 | Inbounds SLA | 398 | Inbounds SLA |
| @@ -410,7 +408,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 410 | <input | 408 | <input |
| 411 | type="text" | 409 | type="text" |
| 412 | value={routeContent.name} | 410 | value={routeContent.name} |
| 413 | onChange={(e) => { | 411 | onChange={e => { |
| 414 | setRouteContent({ | 412 | setRouteContent({ |
| 415 | ...routeContent, | 413 | ...routeContent, |
| 416 | name: e.target.value, | 414 | name: e.target.value, |
| @@ -423,7 +421,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 423 | <input | 421 | <input |
| 424 | type="number" | 422 | type="number" |
| 425 | value={routeContent.score} | 423 | value={routeContent.score} |
| 426 | onChange={(e) => { | 424 | onChange={e => { |
| 427 | setRouteContent({ | 425 | setRouteContent({ |
| 428 | ...routeContent, | 426 | ...routeContent, |
| 429 | score: parseInt(e.target.value), | 427 | score: parseInt(e.target.value), |
| @@ -436,7 +434,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 436 | <input | 434 | <input |
| 437 | type="date" | 435 | type="date" |
| 438 | value={routeContent.date} | 436 | value={routeContent.date} |
| 439 | onChange={(e) => { | 437 | onChange={e => { |
| 440 | setRouteContent({ | 438 | setRouteContent({ |
| 441 | ...routeContent, | 439 | ...routeContent, |
| 442 | date: e.target.value, | 440 | date: e.target.value, |
| @@ -449,7 +447,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 449 | <input | 447 | <input |
| 450 | type="text" | 448 | type="text" |
| 451 | value={routeContent.showcase} | 449 | value={routeContent.showcase} |
| 452 | onChange={(e) => { | 450 | onChange={e => { |
| 453 | setRouteContent({ | 451 | setRouteContent({ |
| 454 | ...routeContent, | 452 | ...routeContent, |
| 455 | showcase: e.target.value, | 453 | showcase: e.target.value, |
| @@ -459,12 +457,12 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 459 | </div> | 457 | </div> |
| 460 | <div | 458 | <div |
| 461 | id="modview-route-description" | 459 | id="modview-route-description" |
| 462 | style={{ height: "180px", gridColumn: "1 / span 5" }} | 460 | style={{ height: '180px', gridColumn: '1 / span 5' }} |
| 463 | > | 461 | > |
| 464 | <span>Description:</span> | 462 | <span>Description:</span> |
| 465 | <textarea | 463 | <textarea |
| 466 | value={routeContent.description} | 464 | value={routeContent.description} |
| 467 | onChange={(e) => { | 465 | onChange={e => { |
| 468 | setRouteContent({ | 466 | setRouteContent({ |
| 469 | ...routeContent, | 467 | ...routeContent, |
| 470 | description: e.target.value, | 468 | description: e.target.value, |
| @@ -474,7 +472,7 @@ const ModMenu: React.FC<ModMenuProps> = ({ | |||
| 474 | /> | 472 | /> |
| 475 | </div> | 473 | </div> |
| 476 | <button | 474 | <button |
| 477 | style={{ gridColumn: "2 / span 3", height: "40px" }} | 475 | style={{ gridColumn: '2 / span 3', height: '40px' }} |
| 478 | onClick={_create_map_summary_route} | 476 | onClick={_create_map_summary_route} |
| 479 | > | 477 | > |
| 480 | Apply | 478 | Apply |
diff --git a/frontend/src/components/RankingEntry.tsx b/frontend/src/components/RankingEntry.tsx index b899965..add36ca 100644 --- a/frontend/src/components/RankingEntry.tsx +++ b/frontend/src/components/RankingEntry.tsx | |||
| @@ -1,46 +1,66 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link } from "react-router-dom"; | 2 | import { Link } from 'react-router-dom'; |
| 3 | import { RankingType, SteamRanking, SteamRankingType } from '@customTypes/Ranking'; | 3 | import { |
| 4 | RankingType, | ||
| 5 | SteamRanking, | ||
| 6 | SteamRankingType, | ||
| 7 | } from '@customTypes/Ranking'; | ||
| 4 | 8 | ||
| 5 | enum RankingCategories { | 9 | enum RankingCategories { |
| 6 | rankings_overall, | 10 | rankings_overall, |
| 7 | rankings_multiplayer, | 11 | rankings_multiplayer, |
| 8 | rankings_singleplayer | 12 | rankings_singleplayer, |
| 9 | } | 13 | } |
| 10 | 14 | ||
| 11 | interface RankingEntryProps { | 15 | interface RankingEntryProps { |
| 12 | curRankingData: RankingType | SteamRankingType; | 16 | curRankingData: RankingType | SteamRankingType; |
| 13 | currentLeaderboardType: RankingCategories | 17 | currentLeaderboardType: RankingCategories; |
| 14 | }; | ||
| 15 | |||
| 16 | const RankingEntry: React.FC<RankingEntryProps> = (prop) => { | ||
| 17 | if ("placement" in prop.curRankingData) { | ||
| 18 | return ( | ||
| 19 | <div className='leaderboard-entry'> | ||
| 20 | <span>{prop.curRankingData.placement}</span> | ||
| 21 | <div> | ||
| 22 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> | ||
| 23 | <img src={prop.curRankingData.user.avatar_link}></img> | ||
| 24 | <span>{prop.curRankingData.user.user_name}</span> | ||
| 25 | </Link> | ||
| 26 | </div> | ||
| 27 | <span>{prop.curRankingData.total_score}</span> | ||
| 28 | </div> | ||
| 29 | ) | ||
| 30 | } else { | ||
| 31 | return ( | ||
| 32 | <div className='leaderboard-entry'> | ||
| 33 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_rank : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_rank : prop.curRankingData.overall_rank}</span> | ||
| 34 | <div> | ||
| 35 | <Link to={`/users/${prop.curRankingData.steam_id}`}> | ||
| 36 | <img src={prop.curRankingData.avatar_link}></img> | ||
| 37 | <span>{prop.curRankingData.user_name}</span> | ||
| 38 | </Link> | ||
| 39 | </div> | ||
| 40 | <span>{prop.currentLeaderboardType == RankingCategories.rankings_singleplayer ? prop.curRankingData.sp_score : prop.currentLeaderboardType == RankingCategories.rankings_multiplayer ? prop.curRankingData.mp_score : prop.curRankingData.overall_score}</span> | ||
| 41 | </div> | ||
| 42 | ) | ||
| 43 | } | ||
| 44 | } | 18 | } |
| 45 | 19 | ||
| 20 | const RankingEntry: React.FC<RankingEntryProps> = prop => { | ||
| 21 | if ('placement' in prop.curRankingData) { | ||
| 22 | return ( | ||
| 23 | <div className="leaderboard-entry"> | ||
| 24 | <span>{prop.curRankingData.placement}</span> | ||
| 25 | <div> | ||
| 26 | <Link to={`/users/${prop.curRankingData.user.steam_id}`}> | ||
| 27 | <img src={prop.curRankingData.user.avatar_link}></img> | ||
| 28 | <span>{prop.curRankingData.user.user_name}</span> | ||
| 29 | </Link> | ||
| 30 | </div> | ||
| 31 | <span>{prop.curRankingData.total_score}</span> | ||
| 32 | </div> | ||
| 33 | ); | ||
| 34 | } else { | ||
| 35 | return ( | ||
| 36 | <div className="leaderboard-entry"> | ||
| 37 | <span> | ||
| 38 | {prop.currentLeaderboardType == | ||
| 39 | RankingCategories.rankings_singleplayer | ||
| 40 | ? prop.curRankingData.sp_rank | ||
| 41 | : prop.currentLeaderboardType == | ||
| 42 | RankingCategories.rankings_multiplayer | ||
| 43 | ? prop.curRankingData.mp_rank | ||
| 44 | : prop.curRankingData.overall_rank} | ||
| 45 | </span> | ||
| 46 | <div> | ||
| 47 | <Link to={`/users/${prop.curRankingData.steam_id}`}> | ||
| 48 | <img src={prop.curRankingData.avatar_link}></img> | ||
| 49 | <span>{prop.curRankingData.user_name}</span> | ||
| 50 | </Link> | ||
| 51 | </div> | ||
| 52 | <span> | ||
| 53 | {prop.currentLeaderboardType == | ||
| 54 | RankingCategories.rankings_singleplayer | ||
| 55 | ? prop.curRankingData.sp_score | ||
| 56 | : prop.currentLeaderboardType == | ||
| 57 | RankingCategories.rankings_multiplayer | ||
| 58 | ? prop.curRankingData.mp_score | ||
| 59 | : prop.curRankingData.overall_score} | ||
| 60 | </span> | ||
| 61 | </div> | ||
| 62 | ); | ||
| 63 | } | ||
| 64 | }; | ||
| 65 | |||
| 46 | export default RankingEntry; | 66 | export default RankingEntry; |
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 67f7f3d..71b79be 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx | |||
| @@ -1,23 +1,38 @@ | |||
| 1 | import React from 'react'; | 1 | import React from 'react'; |
| 2 | import { Link, useLocation } from 'react-router-dom'; | 2 | import { Link, useLocation } from 'react-router-dom'; |
| 3 | 3 | ||
| 4 | import { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; | 4 | import { |
| 5 | BookIcon, | ||
| 6 | FlagIcon, | ||
| 7 | HelpIcon, | ||
| 8 | HomeIcon, | ||
| 9 | LogoIcon, | ||
| 10 | PortalIcon, | ||
| 11 | SearchIcon, | ||
| 12 | UploadIcon, | ||
| 13 | } from '@images/Images'; | ||
| 5 | import Login from '@components/Login'; | 14 | import Login from '@components/Login'; |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 15 | import { UserProfile } from '@customTypes/Profile'; |
| 7 | import { Search } from '@customTypes/Search'; | 16 | import { Search } from '@customTypes/Search'; |
| 8 | import { API } from '@api/Api'; | 17 | import { API } from '@api/Api'; |
| 9 | import "@css/Sidebar.css"; | 18 | import '@css/Sidebar.css'; |
| 10 | 19 | ||
| 11 | interface SidebarProps { | 20 | interface SidebarProps { |
| 12 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | 21 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; |
| 13 | profile?: UserProfile; | 22 | profile?: UserProfile; |
| 14 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | 23 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; |
| 15 | onUploadRun: () => void; | 24 | onUploadRun: () => void; |
| 16 | }; | 25 | } |
| 17 | |||
| 18 | const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { | ||
| 19 | 26 | ||
| 20 | const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); | 27 | const Sidebar: React.FC<SidebarProps> = ({ |
| 28 | setToken, | ||
| 29 | profile, | ||
| 30 | setProfile, | ||
| 31 | onUploadRun, | ||
| 32 | }) => { | ||
| 33 | const [searchData, setSearchData] = React.useState<Search | undefined>( | ||
| 34 | undefined | ||
| 35 | ); | ||
| 21 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | 36 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); |
| 22 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); | 37 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); |
| 23 | 38 | ||
| @@ -25,71 +40,86 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 25 | const path = location.pathname; | 40 | const path = location.pathname; |
| 26 | 41 | ||
| 27 | const handle_sidebar_click = (clicked_sidebar_idx: number) => { | 42 | const handle_sidebar_click = (clicked_sidebar_idx: number) => { |
| 28 | const btn = document.querySelectorAll("button.sidebar-button"); | 43 | const btn = document.querySelectorAll('button.sidebar-button'); |
| 29 | if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } | 44 | if (isSidebarOpen) { |
| 45 | setSidebarOpen(false); | ||
| 46 | _handle_sidebar_hide(); | ||
| 47 | } | ||
| 30 | // clusterfuck | 48 | // clusterfuck |
| 31 | btn.forEach((e, i) => { | 49 | btn.forEach((e, i) => { |
| 32 | btn[i].classList.remove("sidebar-button-selected") | 50 | btn[i].classList.remove('sidebar-button-selected'); |
| 33 | btn[i].classList.add("sidebar-button-deselected") | 51 | btn[i].classList.add('sidebar-button-deselected'); |
| 34 | }) | 52 | }); |
| 35 | btn[clicked_sidebar_idx].classList.add("sidebar-button-selected") | 53 | btn[clicked_sidebar_idx].classList.add('sidebar-button-selected'); |
| 36 | btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected") | 54 | btn[clicked_sidebar_idx].classList.remove('sidebar-button-deselected'); |
| 37 | }; | 55 | }; |
| 38 | 56 | ||
| 39 | const _handle_sidebar_hide = () => { | 57 | const _handle_sidebar_hide = () => { |
| 40 | var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> | 58 | var btn = document.querySelectorAll( |
| 41 | const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement> | 59 | 'button.sidebar-button' |
| 42 | const side = document.querySelector("#sidebar-list") as HTMLElement; | 60 | ) as NodeListOf<HTMLElement>; |
| 43 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; | 61 | const span = document.querySelectorAll( |
| 44 | const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement; | 62 | 'button.sidebar-button>span' |
| 45 | const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement; | 63 | ) as NodeListOf<HTMLElement>; |
| 64 | const side = document.querySelector('#sidebar-list') as HTMLElement; | ||
| 65 | const searchbar = document.querySelector('#searchbar') as HTMLInputElement; | ||
| 66 | const uploadRunBtn = document.querySelector( | ||
| 67 | '#upload-run' | ||
| 68 | ) as HTMLInputElement; | ||
| 69 | const uploadRunSpan = document.querySelector( | ||
| 70 | '#upload-run>span' | ||
| 71 | ) as HTMLInputElement; | ||
| 46 | 72 | ||
| 47 | if (isSidebarOpen) { | 73 | if (isSidebarOpen) { |
| 48 | if (profile) { | 74 | if (profile) { |
| 49 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | 75 | const login = document.querySelectorAll( |
| 50 | login.style.opacity = "1" | 76 | '.login>button' |
| 51 | uploadRunBtn.style.width = "310px" | 77 | )[1] as HTMLElement; |
| 52 | uploadRunBtn.style.padding = "0.4em 0 0 11px" | 78 | login.style.opacity = '1'; |
| 53 | uploadRunSpan.style.opacity = "0" | 79 | uploadRunBtn.style.width = '310px'; |
| 80 | uploadRunBtn.style.padding = '0.4em 0 0 11px'; | ||
| 81 | uploadRunSpan.style.opacity = '0'; | ||
| 54 | setTimeout(() => { | 82 | setTimeout(() => { |
| 55 | uploadRunSpan.style.opacity = "1" | 83 | uploadRunSpan.style.opacity = '1'; |
| 56 | }, 100) | 84 | }, 100); |
| 57 | } | 85 | } |
| 58 | setSidebarOpen(false); | 86 | setSidebarOpen(false); |
| 59 | side.style.width = "320px" | 87 | side.style.width = '320px'; |
| 60 | btn.forEach((e, i) => { | 88 | btn.forEach((e, i) => { |
| 61 | e.style.width = "310px" | 89 | e.style.width = '310px'; |
| 62 | e.style.padding = "0.4em 0 0 11px" | 90 | e.style.padding = '0.4em 0 0 11px'; |
| 63 | setTimeout(() => { | 91 | setTimeout(() => { |
| 64 | span[i].style.opacity = "1" | 92 | span[i].style.opacity = '1'; |
| 65 | }, 100) | 93 | }, 100); |
| 66 | }); | 94 | }); |
| 67 | side.style.zIndex = "2" | 95 | side.style.zIndex = '2'; |
| 68 | } else { | 96 | } else { |
| 69 | if (profile) { | 97 | if (profile) { |
| 70 | const login = document.querySelectorAll(".login>button")[1] as HTMLElement; | 98 | const login = document.querySelectorAll( |
| 71 | login.style.opacity = "0" | 99 | '.login>button' |
| 72 | uploadRunBtn.style.width = "40px" | 100 | )[1] as HTMLElement; |
| 73 | uploadRunBtn.style.padding = "0.4em 0 0 5px" | 101 | login.style.opacity = '0'; |
| 74 | uploadRunSpan.style.opacity = "0" | 102 | uploadRunBtn.style.width = '40px'; |
| 103 | uploadRunBtn.style.padding = '0.4em 0 0 5px'; | ||
| 104 | uploadRunSpan.style.opacity = '0'; | ||
| 75 | } | 105 | } |
| 76 | setSidebarOpen(true); | 106 | setSidebarOpen(true); |
| 77 | side.style.width = "40px"; | 107 | side.style.width = '40px'; |
| 78 | searchbar.focus(); | 108 | searchbar.focus(); |
| 79 | btn.forEach((e, i) => { | 109 | btn.forEach((e, i) => { |
| 80 | e.style.width = "40px" | 110 | e.style.width = '40px'; |
| 81 | e.style.padding = "0.4em 0 0 5px" | 111 | e.style.padding = '0.4em 0 0 5px'; |
| 82 | span[i].style.opacity = "0" | 112 | span[i].style.opacity = '0'; |
| 83 | }) | 113 | }); |
| 84 | setTimeout(() => { | 114 | setTimeout(() => { |
| 85 | side.style.zIndex = "0" | 115 | side.style.zIndex = '0'; |
| 86 | }, 300); | 116 | }, 300); |
| 87 | } | 117 | } |
| 88 | }; | 118 | }; |
| 89 | 119 | ||
| 90 | const _handle_sidebar_lock = () => { | 120 | const _handle_sidebar_lock = () => { |
| 91 | if (!isSidebarLocked) { | 121 | if (!isSidebarLocked) { |
| 92 | _handle_sidebar_hide() | 122 | _handle_sidebar_hide(); |
| 93 | setIsSidebarLocked(true); | 123 | setIsSidebarLocked(true); |
| 94 | setTimeout(() => setIsSidebarLocked(false), 300); | 124 | setTimeout(() => setIsSidebarLocked(false), 300); |
| 95 | } | 125 | } |
| @@ -101,98 +131,148 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo | |||
| 101 | }; | 131 | }; |
| 102 | 132 | ||
| 103 | React.useEffect(() => { | 133 | React.useEffect(() => { |
| 104 | if (path === "/") { handle_sidebar_click(1) } | 134 | if (path === '/') { |
| 105 | else if (path.includes("games")) { handle_sidebar_click(2) } | 135 | handle_sidebar_click(1); |
| 106 | else if (path.includes("rankings")) { handle_sidebar_click(3) } | 136 | } else if (path.includes('games')) { |
| 137 | handle_sidebar_click(2); | ||
| 138 | } else if (path.includes('rankings')) { | ||
| 139 | handle_sidebar_click(3); | ||
| 140 | } | ||
| 107 | // else if (path.includes("news")) { handle_sidebar_click(4) } | 141 | // else if (path.includes("news")) { handle_sidebar_click(4) } |
| 108 | // else if (path.includes("scorelog")) { handle_sidebar_click(5) } | 142 | // else if (path.includes("scorelog")) { handle_sidebar_click(5) } |
| 109 | else if (path.includes("profile")) { handle_sidebar_click(4) } | 143 | else if (path.includes('profile')) { |
| 110 | else if (path.includes("rules")) { handle_sidebar_click(5) } | 144 | handle_sidebar_click(4); |
| 111 | else if (path.includes("about")) { handle_sidebar_click(6) } | 145 | } else if (path.includes('rules')) { |
| 146 | handle_sidebar_click(5); | ||
| 147 | } else if (path.includes('about')) { | ||
| 148 | handle_sidebar_click(6); | ||
| 149 | } | ||
| 112 | }, [path]); | 150 | }, [path]); |
| 113 | 151 | ||
| 114 | return ( | 152 | return ( |
| 115 | <div id='sidebar'> | 153 | <div id="sidebar"> |
| 116 | <Link to="/" tabIndex={-1}> | 154 | <Link to="/" tabIndex={-1}> |
| 117 | <div id='logo'> {/* logo */} | 155 | <div id="logo"> |
| 118 | <img src={LogoIcon} alt="" height={"80px"} /> | 156 | {' '} |
| 119 | <div id='logo-text'> | 157 | {/* logo */} |
| 120 | <span><b>PORTAL 2</b></span><br /> | 158 | <img src={LogoIcon} alt="" height={'80px'} /> |
| 159 | <div id="logo-text"> | ||
| 160 | <span> | ||
| 161 | <b>PORTAL 2</b> | ||
| 162 | </span> | ||
| 163 | <br /> | ||
| 121 | <span>Least Portals Hub</span> | 164 | <span>Least Portals Hub</span> |
| 122 | </div> | 165 | </div> |
| 123 | </div> | 166 | </div> |
| 124 | </Link> | 167 | </Link> |
| 125 | <div id='sidebar-list'> {/* List */} | 168 | <div id="sidebar-list"> |
| 126 | <div id='sidebar-toplist'> {/* Top */} | 169 | {' '} |
| 127 | 170 | {/* List */} | |
| 128 | <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button> | 171 | <div id="sidebar-toplist"> |
| 129 | 172 | {' '} | |
| 173 | {/* Top */} | ||
| 174 | <button | ||
| 175 | className="sidebar-button" | ||
| 176 | onClick={() => _handle_sidebar_lock()} | ||
| 177 | > | ||
| 178 | <img src={SearchIcon} alt="" /> | ||
| 179 | <span>Search</span> | ||
| 180 | </button> | ||
| 130 | <span></span> | 181 | <span></span> |
| 131 | |||
| 132 | <Link to="/" tabIndex={-1}> | 182 | <Link to="/" tabIndex={-1}> |
| 133 | <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home Page</span></button> | 183 | <button className="sidebar-button"> |
| 184 | <img src={HomeIcon} alt="homepage" /> | ||
| 185 | <span>Home Page</span> | ||
| 186 | </button> | ||
| 134 | </Link> | 187 | </Link> |
| 135 | |||
| 136 | <Link to="/games" tabIndex={-1}> | 188 | <Link to="/games" tabIndex={-1}> |
| 137 | <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> | 189 | <button className="sidebar-button"> |
| 190 | <img src={PortalIcon} alt="games" /> | ||
| 191 | <span>Games</span> | ||
| 192 | </button> | ||
| 138 | </Link> | 193 | </Link> |
| 139 | |||
| 140 | <Link to="/rankings" tabIndex={-1}> | 194 | <Link to="/rankings" tabIndex={-1}> |
| 141 | <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> | 195 | <button className="sidebar-button"> |
| 196 | <img src={FlagIcon} alt="rankings" /> | ||
| 197 | <span>Rankings</span> | ||
| 198 | </button> | ||
| 142 | </Link> | 199 | </Link> |
| 143 | |||
| 144 | {/* <Link to="/news" tabIndex={-1}> | 200 | {/* <Link to="/news" tabIndex={-1}> |
| 145 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> | 201 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> |
| 146 | </Link> */} | 202 | </Link> */} |
| 147 | |||
| 148 | {/* <Link to="/scorelog" tabIndex={-1}> | 203 | {/* <Link to="/scorelog" tabIndex={-1}> |
| 149 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> | 204 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> |
| 150 | </Link> */} | 205 | </Link> */} |
| 151 | </div> | 206 | </div> |
| 152 | <div id='sidebar-bottomlist'> | 207 | <div id="sidebar-bottomlist"> |
| 153 | <span></span> | 208 | <span></span> |
| 154 | 209 | ||
| 155 | { | 210 | {profile && profile.profile ? ( |
| 156 | profile && profile.profile ? | 211 | <button |
| 157 | <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload Record</span></button> | 212 | id="upload-run" |
| 158 | : | 213 | className="submit-run-button" |
| 159 | <span></span> | 214 | onClick={() => onUploadRun()} |
| 160 | } | 215 | > |
| 216 | <img src={UploadIcon} alt="upload" /> | ||
| 217 | <span>Upload Record</span> | ||
| 218 | </button> | ||
| 219 | ) : ( | ||
| 220 | <span></span> | ||
| 221 | )} | ||
| 161 | 222 | ||
| 162 | <Login setToken={setToken} profile={profile} setProfile={setProfile} /> | 223 | <Login |
| 224 | setToken={setToken} | ||
| 225 | profile={profile} | ||
| 226 | setProfile={setProfile} | ||
| 227 | /> | ||
| 163 | 228 | ||
| 164 | <Link to="/rules" tabIndex={-1}> | 229 | <Link to="/rules" tabIndex={-1}> |
| 165 | <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard Rules</span></button> | 230 | <button className="sidebar-button"> |
| 231 | <img src={BookIcon} alt="rules" /> | ||
| 232 | <span>Leaderboard Rules</span> | ||
| 233 | </button> | ||
| 166 | </Link> | 234 | </Link> |
| 167 | 235 | ||
| 168 | <Link to="/about" tabIndex={-1}> | 236 | <Link to="/about" tabIndex={-1}> |
| 169 | <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About LPHUB</span></button> | 237 | <button className="sidebar-button"> |
| 238 | <img src={HelpIcon} alt="about" /> | ||
| 239 | <span>About LPHUB</span> | ||
| 240 | </button> | ||
| 170 | </Link> | 241 | </Link> |
| 171 | </div> | 242 | </div> |
| 172 | </div> | 243 | </div> |
| 173 | <div> | 244 | <div> |
| 174 | <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} /> | 245 | <input |
| 175 | 246 | type="text" | |
| 176 | <div id='search-data'> | 247 | id="searchbar" |
| 248 | placeholder="Search for map or a player..." | ||
| 249 | onChange={e => _handle_search_change(e.target.value)} | ||
| 250 | /> | ||
| 177 | 251 | ||
| 252 | <div id="search-data"> | ||
| 178 | {searchData?.maps.map((q, index) => ( | 253 | {searchData?.maps.map((q, index) => ( |
| 179 | <Link to={`/maps/${q.id}`} className='search-map' key={index}> | 254 | <Link to={`/maps/${q.id}`} className="search-map" key={index}> |
| 180 | <span>{q.game}</span> | 255 | <span>{q.game}</span> |
| 181 | <span>{q.chapter}</span> | 256 | <span>{q.chapter}</span> |
| 182 | <span>{q.map}</span> | 257 | <span>{q.map}</span> |
| 183 | </Link> | 258 | </Link> |
| 184 | ))} | 259 | ))} |
| 185 | {searchData?.players.map((q, index) => | 260 | {searchData?.players.map((q, index) => ( |
| 186 | ( | 261 | <Link |
| 187 | <Link to={ | 262 | to={ |
| 188 | profile && q.steam_id === profile.steam_id ? `/profile` : | 263 | profile && q.steam_id === profile.steam_id |
| 189 | `/users/${q.steam_id}` | 264 | ? `/profile` |
| 190 | } className='search-player' key={index}> | 265 | : `/users/${q.steam_id}` |
| 191 | <img src={q.avatar_link} alt='pfp'></img> | 266 | } |
| 192 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span> | 267 | className="search-player" |
| 268 | key={index} | ||
| 269 | > | ||
| 270 | <img src={q.avatar_link} alt="pfp"></img> | ||
| 271 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}> | ||
| 272 | {q.user_name} | ||
| 273 | </span> | ||
| 193 | </Link> | 274 | </Link> |
| 194 | ))} | 275 | ))} |
| 195 | |||
| 196 | </div> | 276 | </div> |
| 197 | </div> | 277 | </div> |
| 198 | </div> | 278 | </div> |
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx index 4bcaa6a..1ba166a 100644 --- a/frontend/src/components/Summary.tsx +++ b/frontend/src/components/Summary.tsx | |||
| @@ -2,49 +2,67 @@ import React from 'react'; | |||
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from 'react-markdown'; |
| 3 | 3 | ||
| 4 | import { MapSummary } from '@customTypes/Map'; | 4 | import { MapSummary } from '@customTypes/Map'; |
| 5 | import "@css/Maps.css" | 5 | import '@css/Maps.css'; |
| 6 | 6 | ||
| 7 | interface SummaryProps { | 7 | interface SummaryProps { |
| 8 | selectedRun: number | 8 | selectedRun: number; |
| 9 | setSelectedRun: (x: number) => void; | 9 | setSelectedRun: (x: number) => void; |
| 10 | data: MapSummary; | 10 | data: MapSummary; |
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) => { | 13 | const Summary: React.FC<SummaryProps> = ({ |
| 14 | 14 | selectedRun, | |
| 15 | setSelectedRun, | ||
| 16 | data, | ||
| 17 | }) => { | ||
| 15 | const [selectedCategory, setSelectedCategory] = React.useState<number>(1); | 18 | const [selectedCategory, setSelectedCategory] = React.useState<number>(1); |
| 16 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); | 19 | const [historySelected, setHistorySelected] = React.useState<boolean>(false); |
| 17 | 20 | ||
| 18 | function _select_run(idx: number, category_id: number) { | 21 | function _select_run(idx: number, category_id: number) { |
| 19 | let r = document.querySelectorAll("button.record"); | 22 | let r = document.querySelectorAll('button.record'); |
| 20 | r.forEach(e => (e as HTMLElement).style.backgroundColor = "#2b2e46"); | 23 | r.forEach(e => ((e as HTMLElement).style.backgroundColor = '#2b2e46')); |
| 21 | (r[idx] as HTMLElement).style.backgroundColor = "#161723" | 24 | (r[idx] as HTMLElement).style.backgroundColor = '#161723'; |
| 22 | |||
| 23 | 25 | ||
| 24 | if (data && data.summary.routes.length !== 0) { | 26 | if (data && data.summary.routes.length !== 0) { |
| 25 | idx += data.summary.routes.filter(e => e.category.id < category_id).length // lethimcook | 27 | idx += data.summary.routes.filter( |
| 28 | e => e.category.id < category_id | ||
| 29 | ).length; // lethimcook | ||
| 26 | setSelectedRun(idx); | 30 | setSelectedRun(idx); |
| 27 | } | 31 | } |
| 28 | }; | 32 | } |
| 29 | 33 | ||
| 30 | function _get_youtube_id(url: string): string { | 34 | function _get_youtube_id(url: string): string { |
| 31 | const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); | 35 | const urlArray = url.split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/); |
| 32 | return (urlArray[2] !== undefined) ? urlArray[2].split(/[^0-9a-z_-]/i)[0] : urlArray[0]; | 36 | return urlArray[2] !== undefined |
| 33 | }; | 37 | ? urlArray[2].split(/[^0-9a-z_-]/i)[0] |
| 38 | : urlArray[0]; | ||
| 39 | } | ||
| 34 | 40 | ||
| 35 | function _category_change() { | 41 | function _category_change() { |
| 36 | const btn = document.querySelectorAll("#section3 #category span button"); | 42 | const btn = document.querySelectorAll('#section3 #category span button'); |
| 37 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | 43 | btn.forEach(e => { |
| 44 | (e as HTMLElement).style.backgroundColor = '#2b2e46'; | ||
| 45 | }); | ||
| 38 | // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories | 46 | // heavenly father forgive me for i have sinned. TODO: fix this bullshit with dynamic categories |
| 39 | const idx = selectedCategory === 1 ? 0 : data.map.is_coop ? selectedCategory - 3 : selectedCategory - 1; | 47 | const idx = |
| 40 | (btn[idx] as HTMLElement).style.backgroundColor = "#202232"; | 48 | selectedCategory === 1 |
| 41 | }; | 49 | ? 0 |
| 50 | : data.map.is_coop | ||
| 51 | ? selectedCategory - 3 | ||
| 52 | : selectedCategory - 1; | ||
| 53 | (btn[idx] as HTMLElement).style.backgroundColor = '#202232'; | ||
| 54 | } | ||
| 42 | 55 | ||
| 43 | function _history_change() { | 56 | function _history_change() { |
| 44 | const btn = document.querySelectorAll("#section3 #history span button"); | 57 | const btn = document.querySelectorAll('#section3 #history span button'); |
| 45 | btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); | 58 | btn.forEach(e => { |
| 46 | (historySelected ? btn[1] as HTMLElement : btn[0] as HTMLElement).style.backgroundColor = "#202232"; | 59 | (e as HTMLElement).style.backgroundColor = '#2b2e46'; |
| 47 | }; | 60 | }); |
| 61 | (historySelected | ||
| 62 | ? (btn[1] as HTMLElement) | ||
| 63 | : (btn[0] as HTMLElement) | ||
| 64 | ).style.backgroundColor = '#202232'; | ||
| 65 | } | ||
| 48 | 66 | ||
| 49 | React.useEffect(() => { | 67 | React.useEffect(() => { |
| 50 | _history_change(); | 68 | _history_change(); |
| @@ -61,119 +79,188 @@ const Summary: React.FC<SummaryProps> = ({ selectedRun, setSelectedRun, data }) | |||
| 61 | 79 | ||
| 62 | return ( | 80 | return ( |
| 63 | <> | 81 | <> |
| 64 | <section id='section3' className='summary1'> | 82 | <section id="section3" className="summary1"> |
| 65 | <div id='category' | 83 | <div |
| 66 | style={data.map.image === "" ? { backgroundColor: "#202232" } : {}}> | 84 | id="category" |
| 67 | <img src={data.map.image} alt="" id='category-image'></img> | 85 | style={data.map.image === '' ? { backgroundColor: '#202232' } : {}} |
| 68 | <p><span className='portal-count'>{data.summary.routes[selectedRun].history.score_count}</span> | 86 | > |
| 69 | {data.summary.routes[selectedRun].history.score_count === 1 ? ` portal` : ` portals`}</p> | 87 | <img src={data.map.image} alt="" id="category-image"></img> |
| 70 | {data.map.is_coop ? // TODO: make this part dynamic | 88 | <p> |
| 71 | ( | 89 | <span className="portal-count"> |
| 72 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr" }}> | 90 | {data.summary.routes[selectedRun].history.score_count} |
| 73 | <button onClick={() => setSelectedCategory(1)}>CM</button> | 91 | </span> |
| 74 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | 92 | {data.summary.routes[selectedRun].history.score_count === 1 |
| 75 | <button onClick={() => setSelectedCategory(5)}>All Courses</button> | 93 | ? ` portal` |
| 76 | </span> | 94 | : ` portals`} |
| 77 | ) | 95 | </p> |
| 78 | : | 96 | {data.map.is_coop ? ( // TODO: make this part dynamic |
| 79 | ( | 97 | <span style={{ gridTemplateColumns: '1fr 1fr 1fr' }}> |
| 80 | <span style={{ gridTemplateColumns: "1fr 1fr 1fr 1fr" }}> | 98 | <button onClick={() => setSelectedCategory(1)}>CM</button> |
| 81 | 99 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | |
| 82 | <button onClick={() => setSelectedCategory(1)}>CM</button> | 100 | <button onClick={() => setSelectedCategory(5)}> |
| 83 | <button onClick={() => setSelectedCategory(2)}>NoSLA</button> | 101 | All Courses |
| 84 | <button onClick={() => setSelectedCategory(3)}>Inbounds SLA</button> | 102 | </button> |
| 85 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | 103 | </span> |
| 86 | </span> | 104 | ) : ( |
| 87 | ) | 105 | <span style={{ gridTemplateColumns: '1fr 1fr 1fr 1fr' }}> |
| 88 | } | 106 | <button onClick={() => setSelectedCategory(1)}>CM</button> |
| 89 | 107 | <button onClick={() => setSelectedCategory(2)}>NoSLA</button> | |
| 108 | <button onClick={() => setSelectedCategory(3)}> | ||
| 109 | Inbounds SLA | ||
| 110 | </button> | ||
| 111 | <button onClick={() => setSelectedCategory(4)}>Any%</button> | ||
| 112 | </span> | ||
| 113 | )} | ||
| 90 | </div> | 114 | </div> |
| 91 | 115 | ||
| 92 | <div id='history'> | 116 | <div id="history"> |
| 93 | 117 | <div style={{ display: historySelected ? 'none' : 'block' }}> | |
| 94 | <div style={{ display: historySelected ? "none" : "block" }}> | 118 | {data.summary.routes.filter(e => e.category.id === selectedCategory) |
| 95 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | 119 | .length === 0 ? ( |
| 120 | <h5>There are no records for this map.</h5> | ||
| 121 | ) : ( | ||
| 96 | <> | 122 | <> |
| 97 | <div className='record-top'> | 123 | <div className="record-top"> |
| 98 | <span>Date</span> | 124 | <span>Date</span> |
| 99 | <span>Record</span> | 125 | <span>Record</span> |
| 100 | <span>First Completion</span> | 126 | <span>First Completion</span> |
| 101 | </div> | 127 | </div> |
| 102 | <hr /> | 128 | <hr /> |
| 103 | <div id='records'> | 129 | <div id="records"> |
| 104 | |||
| 105 | {data.summary.routes | 130 | {data.summary.routes |
| 106 | .filter(e => e.category.id === selectedCategory) | 131 | .filter(e => e.category.id === selectedCategory) |
| 107 | .map((r, index) => ( | 132 | .map((r, index) => ( |
| 108 | <button className='record' key={index} onClick={() => { | 133 | <button |
| 109 | _select_run(index, r.category.id); | 134 | className="record" |
| 110 | }}> | 135 | key={index} |
| 111 | <span>{new Date(r.history.date).toLocaleDateString( | 136 | onClick={() => { |
| 112 | "en-US", { month: 'long', day: 'numeric', year: 'numeric' } | 137 | _select_run(index, r.category.id); |
| 113 | )}</span> | 138 | }} |
| 139 | > | ||
| 140 | <span> | ||
| 141 | {new Date(r.history.date).toLocaleDateString( | ||
| 142 | 'en-US', | ||
| 143 | { month: 'long', day: 'numeric', year: 'numeric' } | ||
| 144 | )} | ||
| 145 | </span> | ||
| 114 | <span>{r.history.score_count}</span> | 146 | <span>{r.history.score_count}</span> |
| 115 | <span>{r.history.runner_name}</span> | 147 | <span>{r.history.runner_name}</span> |
| 116 | </button> | 148 | </button> |
| 117 | ))} | 149 | ))} |
| 118 | </div> | 150 | </div> |
| 119 | </> | 151 | </> |
| 120 | } | 152 | )} |
| 121 | </div> | 153 | </div> |
| 122 | 154 | ||
| 123 | <div style={{ display: historySelected ? "block" : "none" }}> | 155 | <div style={{ display: historySelected ? 'block' : 'none' }}> |
| 124 | {data.summary.routes.filter(e => e.category.id === selectedCategory).length === 0 ? <h5>There are no records for this map.</h5> : | 156 | {data.summary.routes.filter(e => e.category.id === selectedCategory) |
| 125 | <div id='graph'> | 157 | .length === 0 ? ( |
| 158 | <h5>There are no records for this map.</h5> | ||
| 159 | ) : ( | ||
| 160 | <div id="graph"> | ||
| 126 | {/* <div>{graph(1)}</div> | 161 | {/* <div>{graph(1)}</div> |
| 127 | <div>{graph(2)}</div> | 162 | <div>{graph(2)}</div> |
| 128 | <div>{graph(3)}</div> */} | 163 | <div>{graph(3)}</div> */} |
| 129 | </div> | 164 | </div> |
| 130 | } | 165 | )} |
| 131 | </div> | 166 | </div> |
| 132 | <span> | 167 | <span> |
| 133 | <button onClick={() => setHistorySelected(false)}>List</button> | 168 | <button onClick={() => setHistorySelected(false)}>List</button> |
| 134 | <button onClick={() => setHistorySelected(true)}>Graph</button> | 169 | <button onClick={() => setHistorySelected(true)}>Graph</button> |
| 135 | </span> | 170 | </span> |
| 136 | </div> | 171 | </div> |
| 137 | 172 | </section> | |
| 138 | 173 | <section id="section4" className="summary1"> | |
| 139 | </section > | 174 | <div id="difficulty"> |
| 140 | <section id='section4' className='summary1'> | ||
| 141 | <div id='difficulty'> | ||
| 142 | <span>Difficulty</span> | 175 | <span>Difficulty</span> |
| 143 | {data.summary.routes[selectedRun].rating === 0 && (<span>N/A</span>)} | 176 | {data.summary.routes[selectedRun].rating === 0 && <span>N/A</span>} |
| 144 | {data.summary.routes[selectedRun].rating === 1 && (<span style={{ color: "lime" }}>Very easy</span>)} | 177 | {data.summary.routes[selectedRun].rating === 1 && ( |
| 145 | {data.summary.routes[selectedRun].rating === 2 && (<span style={{ color: "green" }}>Easy</span>)} | 178 | <span style={{ color: 'lime' }}>Very easy</span> |
| 146 | {data.summary.routes[selectedRun].rating === 3 && (<span style={{ color: "yellow" }}>Medium</span>)} | 179 | )} |
| 147 | {data.summary.routes[selectedRun].rating === 4 && (<span style={{ color: "orange" }}>Hard</span>)} | 180 | {data.summary.routes[selectedRun].rating === 2 && ( |
| 148 | {data.summary.routes[selectedRun].rating === 5 && (<span style={{ color: "red" }}>Very hard</span>)} | 181 | <span style={{ color: 'green' }}>Easy</span> |
| 182 | )} | ||
| 183 | {data.summary.routes[selectedRun].rating === 3 && ( | ||
| 184 | <span style={{ color: 'yellow' }}>Medium</span> | ||
| 185 | )} | ||
| 186 | {data.summary.routes[selectedRun].rating === 4 && ( | ||
| 187 | <span style={{ color: 'orange' }}>Hard</span> | ||
| 188 | )} | ||
| 189 | {data.summary.routes[selectedRun].rating === 5 && ( | ||
| 190 | <span style={{ color: 'red' }}>Very hard</span> | ||
| 191 | )} | ||
| 149 | <div> | 192 | <div> |
| 150 | {data.summary.routes[selectedRun].rating === 1 ? (<div className='difficulty-rating' style={{ backgroundColor: "lime" }}></div>) : (<div className='difficulty-rating'></div>)} | 193 | {data.summary.routes[selectedRun].rating === 1 ? ( |
| 151 | {data.summary.routes[selectedRun].rating === 2 ? (<div className='difficulty-rating' style={{ backgroundColor: "green" }}></div>) : (<div className='difficulty-rating'></div>)} | 194 | <div |
| 152 | {data.summary.routes[selectedRun].rating === 3 ? (<div className='difficulty-rating' style={{ backgroundColor: "yellow" }}></div>) : (<div className='difficulty-rating'></div>)} | 195 | className="difficulty-rating" |
| 153 | {data.summary.routes[selectedRun].rating === 4 ? (<div className='difficulty-rating' style={{ backgroundColor: "orange" }}></div>) : (<div className='difficulty-rating'></div>)} | 196 | style={{ backgroundColor: 'lime' }} |
| 154 | {data.summary.routes[selectedRun].rating === 5 ? (<div className='difficulty-rating' style={{ backgroundColor: "red" }}></div>) : (<div className='difficulty-rating'></div>)} | 197 | ></div> |
| 198 | ) : ( | ||
| 199 | <div className="difficulty-rating"></div> | ||
| 200 | )} | ||
| 201 | {data.summary.routes[selectedRun].rating === 2 ? ( | ||
| 202 | <div | ||
| 203 | className="difficulty-rating" | ||
| 204 | style={{ backgroundColor: 'green' }} | ||
| 205 | ></div> | ||
| 206 | ) : ( | ||
| 207 | <div className="difficulty-rating"></div> | ||
| 208 | )} | ||
| 209 | {data.summary.routes[selectedRun].rating === 3 ? ( | ||
| 210 | <div | ||
| 211 | className="difficulty-rating" | ||
| 212 | style={{ backgroundColor: 'yellow' }} | ||
| 213 | ></div> | ||
| 214 | ) : ( | ||
| 215 | <div className="difficulty-rating"></div> | ||
| 216 | )} | ||
| 217 | {data.summary.routes[selectedRun].rating === 4 ? ( | ||
| 218 | <div | ||
| 219 | className="difficulty-rating" | ||
| 220 | style={{ backgroundColor: 'orange' }} | ||
| 221 | ></div> | ||
| 222 | ) : ( | ||
| 223 | <div className="difficulty-rating"></div> | ||
| 224 | )} | ||
| 225 | {data.summary.routes[selectedRun].rating === 5 ? ( | ||
| 226 | <div | ||
| 227 | className="difficulty-rating" | ||
| 228 | style={{ backgroundColor: 'red' }} | ||
| 229 | ></div> | ||
| 230 | ) : ( | ||
| 231 | <div className="difficulty-rating"></div> | ||
| 232 | )} | ||
| 155 | </div> | 233 | </div> |
| 156 | </div> | 234 | </div> |
| 157 | <div id='count'> | 235 | <div id="count"> |
| 158 | <span>Completion Count</span> | 236 | <span>Completion Count</span> |
| 159 | <div>{data.summary.routes[selectedRun].completion_count}</div> | 237 | <div>{data.summary.routes[selectedRun].completion_count}</div> |
| 160 | </div> | 238 | </div> |
| 161 | </section> | 239 | </section> |
| 162 | 240 | ||
| 163 | <section id='section5' className='summary1'> | 241 | <section id="section5" className="summary1"> |
| 164 | <div id='description'> | 242 | <div id="description"> |
| 165 | {data.summary.routes[selectedRun].showcase !== "" ? | 243 | {data.summary.routes[selectedRun].showcase !== '' ? ( |
| 166 | <iframe title='Showcase video' src={"https://www.youtube.com/embed/" + _get_youtube_id(data.summary.routes[selectedRun].showcase)}> </iframe> | 244 | <iframe |
| 167 | : ""} | 245 | title="Showcase video" |
| 246 | src={ | ||
| 247 | 'https://www.youtube.com/embed/' + | ||
| 248 | _get_youtube_id(data.summary.routes[selectedRun].showcase) | ||
| 249 | } | ||
| 250 | > | ||
| 251 | {' '} | ||
| 252 | </iframe> | ||
| 253 | ) : ( | ||
| 254 | '' | ||
| 255 | )} | ||
| 168 | <h3>Route Description</h3> | 256 | <h3>Route Description</h3> |
| 169 | <span id='description-text'> | 257 | <span id="description-text"> |
| 170 | <ReactMarkdown> | 258 | <ReactMarkdown> |
| 171 | {data.summary.routes[selectedRun].description} | 259 | {data.summary.routes[selectedRun].description} |
| 172 | </ReactMarkdown> | 260 | </ReactMarkdown> |
| 173 | </span> | 261 | </span> |
| 174 | </div> | 262 | </div> |
| 175 | </section> | 263 | </section> |
| 176 | |||
| 177 | </> | 264 | </> |
| 178 | ); | 265 | ); |
| 179 | }; | 266 | }; |
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index c02fdb8..227a564 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -8,7 +8,7 @@ import { API } from '@api/Api'; | |||
| 8 | import { useNavigate } from 'react-router-dom'; | 8 | import { useNavigate } from 'react-router-dom'; |
| 9 | import useMessage from '@hooks/UseMessage'; | 9 | import useMessage from '@hooks/UseMessage'; |
| 10 | import useConfirm from '@hooks/UseConfirm'; | 10 | import useConfirm from '@hooks/UseConfirm'; |
| 11 | import useMessageLoad from "@hooks/UseMessageLoad"; | 11 | import useMessageLoad from '@hooks/UseMessageLoad'; |
| 12 | import { MapNames } from '@customTypes/MapNames'; | 12 | import { MapNames } from '@customTypes/MapNames'; |
| 13 | 13 | ||
| 14 | interface UploadRunDialogProps { | 14 | interface UploadRunDialogProps { |
| @@ -18,21 +18,27 @@ interface UploadRunDialogProps { | |||
| 18 | games: Game[]; | 18 | games: Game[]; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, games }) => { | 21 | const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ |
| 22 | 22 | token, | |
| 23 | open, | ||
| 24 | onClose, | ||
| 25 | games, | ||
| 26 | }) => { | ||
| 23 | const { message, MessageDialogComponent } = useMessage(); | 27 | const { message, MessageDialogComponent } = useMessage(); |
| 24 | const { confirm, ConfirmDialogComponent } = useConfirm(); | 28 | const { confirm, ConfirmDialogComponent } = useConfirm(); |
| 25 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); | 29 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = |
| 30 | useMessageLoad(); | ||
| 26 | 31 | ||
| 27 | const navigate = useNavigate(); | 32 | const navigate = useNavigate(); |
| 28 | 33 | ||
| 29 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ | 34 | const [uploadRunContent, setUploadRunContent] = |
| 30 | host_demo: null, | 35 | React.useState<UploadRunContent>({ |
| 31 | partner_demo: null, | 36 | host_demo: null, |
| 32 | }); | 37 | partner_demo: null, |
| 38 | }); | ||
| 33 | 39 | ||
| 34 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); | 40 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); |
| 35 | const [selectedGameName, setSelectedGameName] = React.useState<string>(""); | 41 | const [selectedGameName, setSelectedGameName] = React.useState<string>(''); |
| 36 | 42 | ||
| 37 | // dropdowns | 43 | // dropdowns |
| 38 | const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false); | 44 | const [dropdown1Vis, setDropdown1Vis] = React.useState<boolean>(false); |
| @@ -41,7 +47,8 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 41 | const [loading, setLoading] = React.useState<boolean>(false); | 47 | const [loading, setLoading] = React.useState<boolean>(false); |
| 42 | 48 | ||
| 43 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); | 49 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); |
| 44 | const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); | 50 | const [dragHightlightPartner, setDragHighlightPartner] = |
| 51 | React.useState<boolean>(false); | ||
| 45 | 52 | ||
| 46 | const fileInputRef = React.useRef<HTMLInputElement>(null); | 53 | const fileInputRef = React.useRef<HTMLInputElement>(null); |
| 47 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); | 54 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); |
| @@ -52,9 +59,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 52 | } else { | 59 | } else { |
| 53 | fileInputRefPartner.current?.click(); | 60 | fileInputRefPartner.current?.click(); |
| 54 | } | 61 | } |
| 55 | } | 62 | }; |
| 56 | 63 | ||
| 57 | const _handle_drag_over = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 64 | const _handle_drag_over = ( |
| 65 | e: React.DragEvent<HTMLDivElement>, | ||
| 66 | host: boolean | ||
| 67 | ) => { | ||
| 58 | e.preventDefault(); | 68 | e.preventDefault(); |
| 59 | e.stopPropagation(); | 69 | e.stopPropagation(); |
| 60 | if (host) { | 70 | if (host) { |
| @@ -62,9 +72,12 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 62 | } else { | 72 | } else { |
| 63 | setDragHighlightPartner(true); | 73 | setDragHighlightPartner(true); |
| 64 | } | 74 | } |
| 65 | } | 75 | }; |
| 66 | 76 | ||
| 67 | const _handle_drag_leave = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 77 | const _handle_drag_leave = ( |
| 78 | e: React.DragEvent<HTMLDivElement>, | ||
| 79 | host: boolean | ||
| 80 | ) => { | ||
| 68 | e.preventDefault(); | 81 | e.preventDefault(); |
| 69 | e.stopPropagation(); | 82 | e.stopPropagation(); |
| 70 | if (host) { | 83 | if (host) { |
| @@ -72,7 +85,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 72 | } else { | 85 | } else { |
| 73 | setDragHighlightPartner(false); | 86 | setDragHighlightPartner(false); |
| 74 | } | 87 | } |
| 75 | } | 88 | }; |
| 76 | 89 | ||
| 77 | const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { | 90 | const _handle_drop = (e: React.DragEvent<HTMLDivElement>, host: boolean) => { |
| 78 | e.preventDefault(); | 91 | e.preventDefault(); |
| @@ -80,7 +93,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 80 | setDragHighlight(true); | 93 | setDragHighlight(true); |
| 81 | 94 | ||
| 82 | _handle_file_change(e.dataTransfer.files, host); | 95 | _handle_file_change(e.dataTransfer.files, host); |
| 83 | } | 96 | }; |
| 84 | 97 | ||
| 85 | const _handle_dropdowns = (dropdown: number) => { | 98 | const _handle_dropdowns = (dropdown: number) => { |
| 86 | setDropdown1Vis(false); | 99 | setDropdown1Vis(false); |
| @@ -89,9 +102,9 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 89 | setDropdown1Vis(!dropdown1Vis); | 102 | setDropdown1Vis(!dropdown1Vis); |
| 90 | } else if (dropdown == 2) { | 103 | } else if (dropdown == 2) { |
| 91 | setDropdown2Vis(!dropdown2Vis); | 104 | setDropdown2Vis(!dropdown2Vis); |
| 92 | document.querySelector("#dropdown2")?.scrollTo(0, 0); | 105 | document.querySelector('#dropdown2')?.scrollTo(0, 0); |
| 93 | } | 106 | } |
| 94 | } | 107 | }; |
| 95 | 108 | ||
| 96 | const _handle_game_select = async (game_id: string, game_name: string) => { | 109 | const _handle_game_select = async (game_id: string, game_name: string) => { |
| 97 | setLoading(true); | 110 | setLoading(true); |
| @@ -120,62 +133,85 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 120 | if (token) { | 133 | if (token) { |
| 121 | if (games[selectedGameID].is_coop) { | 134 | if (games[selectedGameID].is_coop) { |
| 122 | if (uploadRunContent.host_demo === null) { | 135 | if (uploadRunContent.host_demo === null) { |
| 123 | await message("Error", "You must select a host demo to upload.") | 136 | await message('Error', 'You must select a host demo to upload.'); |
| 124 | return | 137 | return; |
| 125 | } else if (uploadRunContent.partner_demo === null) { | 138 | } else if (uploadRunContent.partner_demo === null) { |
| 126 | await message("Error", "You must select a partner demo to upload.") | 139 | await message('Error', 'You must select a partner demo to upload.'); |
| 127 | return | 140 | return; |
| 128 | } | 141 | } |
| 129 | } else { | 142 | } else { |
| 130 | if (uploadRunContent.host_demo === null) { | 143 | if (uploadRunContent.host_demo === null) { |
| 131 | await message("Error", "You must select a demo to upload.") | 144 | await message('Error', 'You must select a demo to upload.'); |
| 132 | return | 145 | return; |
| 133 | } | 146 | } |
| 134 | } | 147 | } |
| 135 | const demo = SourceDemoParser.default() | 148 | const demo = SourceDemoParser.default() |
| 136 | .setOptions({ packets: true, header: true }) | 149 | .setOptions({ packets: true, header: true }) |
| 137 | .parse(await uploadRunContent.host_demo.arrayBuffer()); | 150 | .parse(await uploadRunContent.host_demo.arrayBuffer()); |
| 138 | const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>((msg) => { | 151 | const scoreboard = demo.findPacket<NetMessages.SvcUserMessage>(msg => { |
| 139 | return msg instanceof NetMessages.SvcUserMessage && msg.userMessage instanceof ScoreboardTempUpdate; | 152 | return ( |
| 140 | }) | 153 | msg instanceof NetMessages.SvcUserMessage && |
| 154 | msg.userMessage instanceof ScoreboardTempUpdate | ||
| 155 | ); | ||
| 156 | }); | ||
| 141 | 157 | ||
| 142 | if (!scoreboard) { | 158 | if (!scoreboard) { |
| 143 | await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") | 159 | await message( |
| 144 | return | 160 | 'Error', |
| 161 | "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode." | ||
| 162 | ); | ||
| 163 | return; | ||
| 145 | } | 164 | } |
| 146 | 165 | ||
| 147 | if (!demo.mapName || !MapNames[demo.mapName]) { | 166 | if (!demo.mapName || !MapNames[demo.mapName]) { |
| 148 | await message("Error", "Error while processing demo: Invalid map name.") | 167 | await message( |
| 149 | return | 168 | 'Error', |
| 169 | 'Error while processing demo: Invalid map name.' | ||
| 170 | ); | ||
| 171 | return; | ||
| 150 | } | 172 | } |
| 151 | 173 | ||
| 152 | if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { | 174 | if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { |
| 153 | await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") | 175 | await message( |
| 154 | return | 176 | 'Error', |
| 177 | 'Error while processing demo: Invalid cooperative demo in singleplayer submission.' | ||
| 178 | ); | ||
| 179 | return; | ||
| 155 | } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { | 180 | } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { |
| 156 | await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") | 181 | await message( |
| 157 | return | 182 | 'Error', |
| 183 | 'Error while processing demo: Invalid singleplayer demo in cooperative submission.' | ||
| 184 | ); | ||
| 185 | return; | ||
| 158 | } | 186 | } |
| 159 | 187 | ||
| 160 | const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; | 188 | const { portalScore, timeScore } = |
| 189 | scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; | ||
| 161 | 190 | ||
| 162 | const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); | 191 | const userConfirmed = await confirm( |
| 192 | 'Upload Record', | ||
| 193 | `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?` | ||
| 194 | ); | ||
| 163 | 195 | ||
| 164 | if (!userConfirmed) { | 196 | if (!userConfirmed) { |
| 165 | return; | 197 | return; |
| 166 | } | 198 | } |
| 167 | 199 | ||
| 168 | messageLoad("Uploading..."); | 200 | messageLoad('Uploading...'); |
| 169 | const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); | 201 | const [success, response] = await API.post_record( |
| 202 | token, | ||
| 203 | uploadRunContent, | ||
| 204 | MapNames[demo.mapName] | ||
| 205 | ); | ||
| 170 | messageLoadClose(); | 206 | messageLoadClose(); |
| 171 | await message("Upload Record", response); | 207 | await message('Upload Record', response); |
| 172 | if (success) { | 208 | if (success) { |
| 173 | setUploadRunContent({ | 209 | setUploadRunContent({ |
| 174 | host_demo: null, | 210 | host_demo: null, |
| 175 | partner_demo: null, | 211 | partner_demo: null, |
| 176 | }); | 212 | }); |
| 177 | onClose(success); | 213 | onClose(success); |
| 178 | navigate("/profile"); | 214 | navigate('/profile'); |
| 179 | } | 215 | } |
| 180 | } | 216 | } |
| 181 | }; | 217 | }; |
| @@ -184,7 +220,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 184 | if (open) { | 220 | if (open) { |
| 185 | setDragHighlightPartner(false); | 221 | setDragHighlightPartner(false); |
| 186 | setDragHighlight(false); | 222 | setDragHighlight(false); |
| 187 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. | 223 | _handle_game_select('1', 'Portal 2 - Singleplayer'); // a different approach?. |
| 188 | } | 224 | } |
| 189 | }, [open]); | 225 | }, [open]); |
| 190 | 226 | ||
| @@ -196,84 +232,191 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 196 | {MessageDialogLoadComponent} | 232 | {MessageDialogLoadComponent} |
| 197 | {ConfirmDialogComponent} | 233 | {ConfirmDialogComponent} |
| 198 | 234 | ||
| 199 | <div id='upload-run-menu'> | 235 | <div id="upload-run-menu"> |
| 200 | <div id='upload-run-menu-add'> | 236 | <div id="upload-run-menu-add"> |
| 201 | <div id='upload-run-route-category'> | 237 | <div id="upload-run-route-category"> |
| 202 | <div style={{ padding: "15px 0px" }} className='upload-run-dropdown-container upload-run-item'> | 238 | <div |
| 203 | <h3 style={{ margin: "0px 0px" }}>Select Game</h3> | 239 | style={{ padding: '15px 0px' }} |
| 204 | <div onClick={() => _handle_dropdowns(1)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> | 240 | className="upload-run-dropdown-container upload-run-item" |
| 205 | <div className='dropdown-cur'>{selectedGameName}</div> | 241 | > |
| 206 | <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> | 242 | <h3 style={{ margin: '0px 0px' }}>Select Game</h3> |
| 243 | <div | ||
| 244 | onClick={() => _handle_dropdowns(1)} | ||
| 245 | style={{ | ||
| 246 | display: 'flex', | ||
| 247 | alignItems: 'center', | ||
| 248 | cursor: 'pointer', | ||
| 249 | justifyContent: 'space-between', | ||
| 250 | margin: '10px 0px', | ||
| 251 | }} | ||
| 252 | > | ||
| 253 | <div className="dropdown-cur">{selectedGameName}</div> | ||
| 254 | <i | ||
| 255 | style={{ | ||
| 256 | rotate: '-90deg', | ||
| 257 | transform: 'translate(-5px, 10px)', | ||
| 258 | }} | ||
| 259 | className="triangle" | ||
| 260 | ></i> | ||
| 207 | </div> | 261 | </div> |
| 208 | <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> | 262 | <div |
| 209 | {games.map((game) => ( | 263 | style={{ top: '110px' }} |
| 210 | <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> | 264 | className={ |
| 265 | dropdown1Vis | ||
| 266 | ? 'upload-run-dropdown' | ||
| 267 | : 'upload-run-dropdown hidden' | ||
| 268 | } | ||
| 269 | > | ||
| 270 | {games.map(game => ( | ||
| 271 | <div | ||
| 272 | onClick={() => { | ||
| 273 | _handle_game_select(game.id.toString(), game.name); | ||
| 274 | _handle_dropdowns(1); | ||
| 275 | }} | ||
| 276 | key={game.id} | ||
| 277 | > | ||
| 278 | {game.name} | ||
| 279 | </div> | ||
| 211 | ))} | 280 | ))} |
| 212 | </div> | 281 | </div> |
| 213 | </div> | 282 | </div> |
| 214 | 283 | ||
| 215 | { | 284 | {!loading && ( |
| 216 | !loading && | 285 | <> |
| 217 | ( | 286 | <div> |
| 218 | <> | 287 | <h3 style={{ margin: '10px 0px' }}>Host Demo</h3> |
| 219 | 288 | <div | |
| 220 | <div> | 289 | onClick={() => { |
| 221 | <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> | 290 | _handle_file_click(true); |
| 222 | <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> | 291 | }} |
| 223 | <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> | 292 | onDragOver={e => { |
| 224 | {!uploadRunContent.host_demo ? | 293 | _handle_drag_over(e, true); |
| 294 | }} | ||
| 295 | onDrop={e => { | ||
| 296 | _handle_drop(e, true); | ||
| 297 | }} | ||
| 298 | onDragLeave={e => { | ||
| 299 | _handle_drag_leave(e, true); | ||
| 300 | }} | ||
| 301 | className={`upload-run-drag-area ${dragHightlight ? 'upload-run-drag-area-highlight' : ''} ${uploadRunContent.host_demo ? 'upload-run-drag-area-hidden' : ''}`} | ||
| 302 | > | ||
| 303 | <input | ||
| 304 | ref={fileInputRef} | ||
| 305 | type="file" | ||
| 306 | name="host_demo" | ||
| 307 | id="host_demo" | ||
| 308 | accept=".dem" | ||
| 309 | onChange={e => | ||
| 310 | _handle_file_change(e.target.files, true) | ||
| 311 | } | ||
| 312 | /> | ||
| 313 | {!uploadRunContent.host_demo ? ( | ||
| 314 | <div> | ||
| 315 | <span>Drag and drop</span> | ||
| 225 | <div> | 316 | <div> |
| 226 | <span>Drag and drop</span> | 317 | <span |
| 227 | <div> | 318 | style={{ |
| 228 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | 319 | fontFamily: 'BarlowSemiCondensed-Regular', |
| 229 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | 320 | }} |
| 230 | </div> | 321 | > |
| 322 | Or click here | ||
| 323 | </span> | ||
| 324 | <br /> | ||
| 325 | <button | ||
| 326 | style={{ | ||
| 327 | borderRadius: '24px', | ||
| 328 | padding: '5px 8px', | ||
| 329 | margin: '5px 0px', | ||
| 330 | }} | ||
| 331 | > | ||
| 332 | Upload | ||
| 333 | </button> | ||
| 231 | </div> | 334 | </div> |
| 232 | : null} | 335 | </div> |
| 233 | 336 | ) : null} | |
| 234 | <span className="upload-run-demo-name">{uploadRunContent.host_demo?.name}</span> | ||
| 235 | </div> | ||
| 236 | { | ||
| 237 | games[selectedGameID].is_coop && | ||
| 238 | ( | ||
| 239 | <> | ||
| 240 | <div> | ||
| 241 | <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3> | ||
| 242 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> | ||
| 243 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? | ||
| 244 | <div> | ||
| 245 | <span>Drag and drop</span> | ||
| 246 | <div> | ||
| 247 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | ||
| 248 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | ||
| 249 | </div> | ||
| 250 | </div> | ||
| 251 | : null} | ||
| 252 | |||
| 253 | <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> | ||
| 254 | </div> | ||
| 255 | </div> | ||
| 256 | </> | ||
| 257 | ) | ||
| 258 | } | ||
| 259 | </div> | ||
| 260 | <div className='search-container'> | ||
| 261 | 337 | ||
| 338 | <span className="upload-run-demo-name"> | ||
| 339 | {uploadRunContent.host_demo?.name} | ||
| 340 | </span> | ||
| 262 | </div> | 341 | </div> |
| 263 | 342 | {games[selectedGameID].is_coop && ( | |
| 264 | </> | 343 | <> |
| 265 | ) | 344 | <div> |
| 266 | } | 345 | <h3 style={{ margin: '10px 0px' }}>Partner Demo</h3> |
| 346 | <div | ||
| 347 | onClick={() => { | ||
| 348 | _handle_file_click(false); | ||
| 349 | }} | ||
| 350 | onDragOver={e => { | ||
| 351 | _handle_drag_over(e, false); | ||
| 352 | }} | ||
| 353 | onDrop={e => { | ||
| 354 | _handle_drop(e, false); | ||
| 355 | }} | ||
| 356 | onDragLeave={e => { | ||
| 357 | _handle_drag_leave(e, false); | ||
| 358 | }} | ||
| 359 | className={`upload-run-drag-area ${dragHightlightPartner ? 'upload-run-drag-area-highlight-partner' : ''} ${uploadRunContent.partner_demo ? 'upload-run-drag-area-hidden' : ''}`} | ||
| 360 | > | ||
| 361 | <input | ||
| 362 | ref={fileInputRefPartner} | ||
| 363 | type="file" | ||
| 364 | name="partner_demo" | ||
| 365 | id="partner_demo" | ||
| 366 | accept=".dem" | ||
| 367 | onChange={e => | ||
| 368 | _handle_file_change(e.target.files, false) | ||
| 369 | } | ||
| 370 | />{' '} | ||
| 371 | {!uploadRunContent.partner_demo ? ( | ||
| 372 | <div> | ||
| 373 | <span>Drag and drop</span> | ||
| 374 | <div> | ||
| 375 | <span | ||
| 376 | style={{ | ||
| 377 | fontFamily: 'BarlowSemiCondensed-Regular', | ||
| 378 | }} | ||
| 379 | > | ||
| 380 | Or click here | ||
| 381 | </span> | ||
| 382 | <br /> | ||
| 383 | <button | ||
| 384 | style={{ | ||
| 385 | borderRadius: '24px', | ||
| 386 | padding: '5px 8px', | ||
| 387 | margin: '5px 0px', | ||
| 388 | }} | ||
| 389 | > | ||
| 390 | Upload | ||
| 391 | </button> | ||
| 392 | </div> | ||
| 393 | </div> | ||
| 394 | ) : null} | ||
| 395 | <span className="upload-run-demo-name"> | ||
| 396 | {uploadRunContent.partner_demo?.name} | ||
| 397 | </span> | ||
| 398 | </div> | ||
| 399 | </div> | ||
| 400 | </> | ||
| 401 | )} | ||
| 402 | </div> | ||
| 403 | <div className="search-container"></div> | ||
| 404 | </> | ||
| 405 | )} | ||
| 267 | </div> | 406 | </div> |
| 268 | <div className='upload-run-buttons-container'> | 407 | <div className="upload-run-buttons-container"> |
| 269 | <button onClick={_upload_run}>Submit</button> | 408 | <button onClick={_upload_run}>Submit</button> |
| 270 | <button onClick={() => { | 409 | <button |
| 271 | onClose(false); | 410 | onClick={() => { |
| 272 | setUploadRunContent({ | 411 | onClose(false); |
| 273 | host_demo: null, | 412 | setUploadRunContent({ |
| 274 | partner_demo: null, | 413 | host_demo: null, |
| 275 | }); | 414 | partner_demo: null, |
| 276 | }}>Cancel</button> | 415 | }); |
| 416 | }} | ||
| 417 | > | ||
| 418 | Cancel | ||
| 419 | </button> | ||
| 277 | </div> | 420 | </div> |
| 278 | </div> | 421 | </div> |
| 279 | </div> | 422 | </div> |
| @@ -281,10 +424,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 281 | ); | 424 | ); |
| 282 | } | 425 | } |
| 283 | 426 | ||
| 284 | return ( | 427 | return <></>; |
| 285 | <></> | ||
| 286 | ); | ||
| 287 | |||
| 288 | }; | 428 | }; |
| 289 | 429 | ||
| 290 | export default UploadRunDialog; | 430 | export default UploadRunDialog; |