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