diff options
Diffstat (limited to 'frontend/src')
48 files changed, 3918 insertions, 2231 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css index 14a9972..464b759 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css | |||
| @@ -1,42 +1,76 @@ | |||
| 1 | main { | 1 | @import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Montserrat+Alternates:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); |
| 2 | overflow: auto; | 2 | @import "tailwindcss"; |
| 3 | overflow-x: hidden; | ||
| 4 | position: relative; | ||
| 5 | 3 | ||
| 6 | width: calc(100% - 380px); | 4 | @theme { |
| 7 | height: 100vh; | 5 | --color-rosewater: #f2d5cf; |
| 8 | left: 350px; | 6 | --color-flamingo: #eebebe; |
| 7 | --color-pink: #f4b8e4; | ||
| 8 | --color-mauve: #ca9ee6; | ||
| 9 | --color-red: #e78284; | ||
| 10 | --color-maroon: #ea999c; | ||
| 11 | --color-peach: #ef9f76; | ||
| 12 | --color-yellow: #e5c890; | ||
| 13 | --color-green: #a6d189; | ||
| 14 | --color-teal: #81c8be; | ||
| 15 | --color-sky: #99d1db; | ||
| 16 | --color-sapphire: #85c1dc; | ||
| 17 | --color-blue: #8caaee; | ||
| 18 | --color-lavender: #babbf1; | ||
| 19 | --color-text: #c6d0f5; | ||
| 20 | --color-subtext1: #b5bfe2; | ||
| 21 | --color-subtext0: #a5adce; | ||
| 22 | --color-overlay2: #949cbb; | ||
| 23 | --color-overlay1: #838ba7; | ||
| 24 | --color-overlay0: #737994; | ||
| 25 | --color-surface2: #626880; | ||
| 26 | --color-surface1: #51576d; | ||
| 27 | --color-surface0: #414559; | ||
| 28 | --color-base: #303446; | ||
| 29 | --color-mantle: #292c3c; | ||
| 30 | --color-crust: #232634; | ||
| 9 | 31 | ||
| 10 | padding-right: 30px; | 32 | --color-primary: var(--color-mauve); |
| 11 | 33 | --color-secondary: var(--color-blue); | |
| 12 | font-size: 40px; | 34 | --color-accent: var(--color-peach); |
| 13 | font-family: BarlowSemiCondensed-Regular; | 35 | --color-background: var(--color-base); |
| 14 | color: #cdcfdf; | 36 | --color-surface: var(--color-surface0); |
| 37 | --color-muted: var(--color-overlay0); | ||
| 38 | --color-border: var(--color-surface2); | ||
| 39 | --color-input: var(--color-surface1); | ||
| 40 | --color-foreground: var(--color-text); | ||
| 41 | --color-success: var(--color-green); | ||
| 42 | --color-warning: var(--color-yellow); | ||
| 43 | --color-error: var(--color-red); | ||
| 44 | --color-info: var(--color-blue); | ||
| 15 | 45 | ||
| 46 | --font-barlow-condensed-regular: 'BarlowCondensed-Regular'; | ||
| 47 | --font-barlow-condensed-bold: 'BarlowCondensed-Bold'; | ||
| 48 | --font-barlow-semicondensed-regular: 'BarlowSemiCondensed-Regular'; | ||
| 49 | --font-barlow-semicondensed-semibold: 'BarlowSemiCondensed-SemiBold'; | ||
| 16 | } | 50 | } |
| 17 | 51 | ||
| 52 | |||
| 18 | a { | 53 | a { |
| 19 | color: inherit; | 54 | color: inherit; |
| 20 | width: fit-content; | ||
| 21 | } | 55 | } |
| 22 | 56 | ||
| 23 | body { | 57 | body { |
| 24 | overflow: hidden; | 58 | overflow: hidden; |
| 25 | background-color: #141520; | 59 | background-color: var(--color-crust); |
| 26 | margin: 0; | 60 | margin: 0; |
| 27 | } | 61 | } |
| 28 | 62 | ||
| 29 | .loader { | 63 | .loader { |
| 30 | animation: loader 1.2s ease infinite; | 64 | animation: loader 1.2s ease infinite; |
| 31 | background-size: 400% 300%; | 65 | background-size: 400% 300%; |
| 32 | background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); | 66 | background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); |
| 33 | user-select: none; | 67 | user-select: none; |
| 34 | } | 68 | } |
| 35 | 69 | ||
| 36 | .loader-text { | 70 | .loader-text { |
| 37 | animation: loader 1.2s ease infinite; | 71 | animation: loader 1.2s ease infinite; |
| 38 | background-size: 400% 300%; | 72 | background-size: 400% 300%; |
| 39 | background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); | 73 | background-image: linear-gradient(-90deg, var(--color-mantle) 0%, var(--color-mantle) 25%, var(--color-surface1) 50%, var(--color-mantle) 75%, var(--color-mantle) 100%); |
| 40 | user-select: none; | 74 | user-select: none; |
| 41 | color: #00000000; | 75 | color: #00000000; |
| 42 | border-radius: 1000px; | 76 | border-radius: 1000px; |
| @@ -76,6 +110,173 @@ body { | |||
| 76 | } | 110 | } |
| 77 | } | 111 | } |
| 78 | 112 | ||
| 113 | /* Custom Tailwind utilities for Catppuccin Frappe theme */ | ||
| 114 | @layer utilities { | ||
| 115 | .bg-primary { | ||
| 116 | background-color: var(--color-primary); | ||
| 117 | } | ||
| 118 | |||
| 119 | .bg-secondary { | ||
| 120 | background-color: var(--color-secondary); | ||
| 121 | } | ||
| 122 | |||
| 123 | .bg-accent { | ||
| 124 | background-color: var(--color-accent); | ||
| 125 | } | ||
| 126 | |||
| 127 | .bg-background { | ||
| 128 | background-color: var(--color-background); | ||
| 129 | } | ||
| 130 | |||
| 131 | .bg-surface { | ||
| 132 | background-color: var(--color-surface); | ||
| 133 | } | ||
| 134 | |||
| 135 | .bg-muted { | ||
| 136 | background-color: var(--color-muted); | ||
| 137 | } | ||
| 138 | |||
| 139 | .text-primary { | ||
| 140 | color: var(--color-primary); | ||
| 141 | } | ||
| 142 | |||
| 143 | .text-secondary { | ||
| 144 | color: var(--color-secondary); | ||
| 145 | } | ||
| 146 | |||
| 147 | .text-accent { | ||
| 148 | color: var(--color-accent); | ||
| 149 | } | ||
| 150 | |||
| 151 | .text-foreground { | ||
| 152 | color: var(--color-foreground); | ||
| 153 | } | ||
| 154 | |||
| 155 | .text-muted { | ||
| 156 | color: var(--color-muted); | ||
| 157 | } | ||
| 158 | |||
| 159 | .border-primary { | ||
| 160 | border-color: var(--color-primary); | ||
| 161 | } | ||
| 162 | |||
| 163 | .border-secondary { | ||
| 164 | border-color: var(--color-secondary); | ||
| 165 | } | ||
| 166 | |||
| 167 | .border-muted { | ||
| 168 | border-color: var(--color-border); | ||
| 169 | } | ||
| 170 | |||
| 171 | .hover\:bg-primary:hover { | ||
| 172 | background-color: var(--color-primary); | ||
| 173 | } | ||
| 174 | |||
| 175 | .hover\:bg-secondary:hover { | ||
| 176 | background-color: var(--color-secondary); | ||
| 177 | } | ||
| 178 | |||
| 179 | .hover\:bg-surface:hover { | ||
| 180 | background-color: var(--color-surface); | ||
| 181 | } | ||
| 182 | |||
| 183 | .hover\:text-primary:hover { | ||
| 184 | color: var(--color-primary); | ||
| 185 | } | ||
| 186 | |||
| 187 | .focus\:ring-primary:focus { | ||
| 188 | --tw-ring-color: var(--color-primary); | ||
| 189 | } | ||
| 190 | |||
| 191 | .triangle { | ||
| 192 | width: 0; | ||
| 193 | height: 0; | ||
| 194 | border-left: 5px solid transparent; | ||
| 195 | border-right: 5px solid transparent; | ||
| 196 | border-bottom: 8px solid var(--color-foreground); | ||
| 197 | display: inline-block; | ||
| 198 | } | ||
| 199 | |||
| 200 | .sidebar-button-selected { | ||
| 201 | background-color: var(--color-primary) !important; | ||
| 202 | color: var(--color-background) !important; | ||
| 203 | } | ||
| 204 | |||
| 205 | .sidebar-button-deselected { | ||
| 206 | background-color: var(--color-surface) !important; | ||
| 207 | color: var(--color-foreground) !important; | ||
| 208 | } | ||
| 209 | |||
| 210 | .profileboard-record { | ||
| 211 | background-color: var(--color-surface); | ||
| 212 | border: 1px solid var(--color-border); | ||
| 213 | border-radius: 0.5rem; | ||
| 214 | padding: 0.75rem; | ||
| 215 | margin-bottom: 0.5rem; | ||
| 216 | transition: all 0.2s ease; | ||
| 217 | } | ||
| 218 | |||
| 219 | .profileboard-record:hover { | ||
| 220 | background-color: var(--color-surface1); | ||
| 221 | } | ||
| 222 | |||
| 223 | .difficulty-rating { | ||
| 224 | width: 20px; | ||
| 225 | height: 20px; | ||
| 226 | background-color: var(--color-muted); | ||
| 227 | border-radius: 50%; | ||
| 228 | margin: 0 2px; | ||
| 229 | display: inline-block; | ||
| 230 | } | ||
| 231 | |||
| 232 | .nav-button { | ||
| 233 | background-color: var(--color-surface); | ||
| 234 | color: var(--color-foreground); | ||
| 235 | border: 1px solid var(--color-border); | ||
| 236 | border-radius: 0.5rem; | ||
| 237 | padding: 0.5rem 1rem; | ||
| 238 | transition: all 0.2s ease; | ||
| 239 | display: inline-flex; | ||
| 240 | align-items: center; | ||
| 241 | gap: 0.5rem; | ||
| 242 | text-decoration: none; | ||
| 243 | } | ||
| 244 | |||
| 245 | .nav-button:hover { | ||
| 246 | background-color: var(--color-surface1); | ||
| 247 | } | ||
| 248 | |||
| 249 | .record { | ||
| 250 | background-color: var(--color-surface); | ||
| 251 | border: 1px solid var(--color-border); | ||
| 252 | border-radius: 0.5rem; | ||
| 253 | padding: 0.5rem; | ||
| 254 | margin: 0.25rem; | ||
| 255 | cursor: pointer; | ||
| 256 | transition: all 0.2s ease; | ||
| 257 | } | ||
| 258 | |||
| 259 | .record:hover { | ||
| 260 | background-color: var(--color-surface1); | ||
| 261 | } | ||
| 262 | |||
| 263 | .portal-count { | ||
| 264 | font-size: 3rem; | ||
| 265 | font-weight: bold; | ||
| 266 | color: var(--color-primary); | ||
| 267 | } | ||
| 268 | |||
| 269 | .titles { | ||
| 270 | background-color: var(--color-accent); | ||
| 271 | color: var(--color-background); | ||
| 272 | padding: 0.25rem 0.5rem; | ||
| 273 | border-radius: 1rem; | ||
| 274 | font-size: 0.875rem; | ||
| 275 | margin-right: 0.5rem; | ||
| 276 | display: inline-block; | ||
| 277 | } | ||
| 278 | } | ||
| 279 | |||
| 79 | @font-face { | 280 | @font-face { |
| 80 | font-family: 'BarlowCondensed-Bold'; | 281 | font-family: 'BarlowCondensed-Bold'; |
| 81 | src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); | 282 | src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); |
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bdd3adc..fbfa59f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx | |||
| @@ -1,32 +1,32 @@ | |||
| 1 | import React from 'react'; | 1 | import React, { useCallback } from "react"; |
| 2 | import { Routes, Route } from "react-router-dom"; | 2 | import { Routes, Route } from "react-router-dom"; |
| 3 | import { Helmet } from "react-helmet"; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { UserProfile } from '@customTypes/Profile'; | 5 | import { UserProfile } from "@customTypes/Profile"; |
| 6 | import Sidebar from './components/Sidebar'; | 6 | import Sidebar from "./components/Sidebar"; |
| 7 | import "./App.css"; | 7 | import "./App.css"; |
| 8 | 8 | ||
| 9 | import Profile from '@pages/Profile'; | 9 | import Profile from "@pages/Profile"; |
| 10 | import Games from '@pages/Games'; | 10 | import Games from "@pages/Games"; |
| 11 | import Maps from '@pages/Maps'; | 11 | import Maps from "@pages/Maps"; |
| 12 | import User from '@pages/User'; | 12 | import User from "@pages/User"; |
| 13 | import Homepage from '@pages/Homepage'; | 13 | import Homepage from "@pages/Homepage"; |
| 14 | import UploadRunDialog from './components/UploadRunDialog'; | 14 | import UploadRunDialog from "./components/UploadRunDialog"; |
| 15 | import Rules from '@pages/Rules'; | 15 | import Rules from "@pages/Rules"; |
| 16 | import About from '@pages/About'; | 16 | import About from "@pages/About"; |
| 17 | import { Game } from '@customTypes/Game'; | 17 | import { Game } from "@customTypes/Game"; |
| 18 | import { API } from './api/Api'; | 18 | import { API } from "./api/Api"; |
| 19 | import Maplist from '@pages/Maplist'; | 19 | import Maplist from "@pages/Maplist"; |
| 20 | import Rankings from '@pages/Rankings'; | 20 | import Rankings from "@pages/Rankings"; |
| 21 | import { get_user_id_from_token, get_user_mod_from_token } from './utils/Jwt'; | 21 | import { get_user_id_from_token, get_user_mod_from_token } from "./utils/Jwt"; |
| 22 | 22 | ||
| 23 | const App: React.FC = () => { | 23 | const App: React.FC = () => { |
| 24 | const [token, setToken] = React.useState<string | undefined>(undefined); | 24 | const [token, setToken] = React.useState<string | undefined>(undefined); |
| 25 | const [profile, setProfile] = React.useState<UserProfile | undefined>(undefined); | 25 | const [profile, setProfile] = React.useState<UserProfile | undefined>( |
| 26 | undefined | ||
| 27 | ); | ||
| 26 | const [isModerator, setIsModerator] = React.useState<boolean>(false); | 28 | const [isModerator, setIsModerator] = React.useState<boolean>(false); |
| 27 | |||
| 28 | const [games, setGames] = React.useState<Game[]>([]); | 29 | const [games, setGames] = React.useState<Game[]>([]); |
| 29 | |||
| 30 | const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false); | 30 | const [uploadRunDialog, setUploadRunDialog] = React.useState<boolean>(false); |
| 31 | 31 | ||
| 32 | const _fetch_token = async () => { | 32 | const _fetch_token = async () => { |
| @@ -39,12 +39,15 @@ const App: React.FC = () => { | |||
| 39 | setGames(games); | 39 | setGames(games); |
| 40 | }; | 40 | }; |
| 41 | 41 | ||
| 42 | const _set_profile = async (user_id?: string) => { | 42 | const _set_profile = useCallback( |
| 43 | if (user_id && token) { | 43 | async (user_id?: string) => { |
| 44 | const user = await API.get_profile(token); | 44 | if (user_id && token) { |
| 45 | setProfile(user); | 45 | const user = await API.get_profile(token); |
| 46 | } | 46 | setProfile(user); |
| 47 | }; | 47 | } |
| 48 | }, | ||
| 49 | [token] | ||
| 50 | ); | ||
| 48 | 51 | ||
| 49 | React.useEffect(() => { | 52 | React.useEffect(() => { |
| 50 | if (token === undefined) { | 53 | if (token === undefined) { |
| @@ -52,15 +55,15 @@ const App: React.FC = () => { | |||
| 52 | setIsModerator(false); | 55 | setIsModerator(false); |
| 53 | } else { | 56 | } else { |
| 54 | setProfile({} as UserProfile); // placeholder before we call actual user profile | 57 | setProfile({} as UserProfile); // placeholder before we call actual user profile |
| 55 | _set_profile(get_user_id_from_token(token)) | 58 | _set_profile(get_user_id_from_token(token)); |
| 56 | const modStatus = get_user_mod_from_token(token) | 59 | const modStatus = get_user_mod_from_token(token); |
| 57 | if (modStatus) { | 60 | if (modStatus) { |
| 58 | setIsModerator(true); | 61 | setIsModerator(true); |
| 59 | } else { | 62 | } else { |
| 60 | setIsModerator(false); | 63 | setIsModerator(false); |
| 61 | } | 64 | } |
| 62 | } | 65 | } |
| 63 | }, [token]); | 66 | }, [token, _set_profile]); |
| 64 | 67 | ||
| 65 | React.useEffect(() => { | 68 | React.useEffect(() => { |
| 66 | _fetch_token(); | 69 | _fetch_token(); |
| @@ -73,23 +76,49 @@ const App: React.FC = () => { | |||
| 73 | <title>LPHUB</title> | 76 | <title>LPHUB</title> |
| 74 | <meta name="description" content="Least Portals Hub" /> | 77 | <meta name="description" content="Least Portals Hub" /> |
| 75 | </Helmet> | 78 | </Helmet> |
| 76 | <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { | 79 | <UploadRunDialog |
| 77 | setUploadRunDialog(false); | 80 | token={token} |
| 78 | if (updateProfile) { | 81 | open={uploadRunDialog} |
| 79 | _set_profile(get_user_id_from_token(token)); | 82 | onClose={updateProfile => { |
| 80 | } | 83 | setUploadRunDialog(false); |
| 81 | }} games={games} /> | 84 | if (updateProfile) { |
| 82 | <Sidebar setToken={setToken} profile={profile} setProfile={setProfile} onUploadRun={() => setUploadRunDialog(true)} /> | 85 | _set_profile(get_user_id_from_token(token)); |
| 86 | } | ||
| 87 | }} | ||
| 88 | games={games} | ||
| 89 | /> | ||
| 90 | <Sidebar | ||
| 91 | setToken={setToken} | ||
| 92 | profile={profile} | ||
| 93 | setProfile={setProfile} | ||
| 94 | onUploadRun={() => setUploadRunDialog(true)} | ||
| 95 | /> | ||
| 83 | <Routes> | 96 | <Routes> |
| 84 | <Route path="/" element={<Homepage />} /> | 97 | <Route path="/" element={<Homepage />} /> |
| 85 | <Route path="/profile" element={<Profile profile={profile} token={token} gameData={games} onDeleteRecord={() => _set_profile(get_user_id_from_token(token))} />} /> | 98 | <Route |
| 86 | <Route path="/users/*" element={<User profile={profile} token={token} gameData={games} />} /> | 99 | path="/profile" |
| 100 | element={ | ||
| 101 | <Profile | ||
| 102 | profile={profile} | ||
| 103 | token={token} | ||
| 104 | gameData={games} | ||
| 105 | onDeleteRecord={() => _set_profile(get_user_id_from_token(token))} | ||
| 106 | /> | ||
| 107 | } | ||
| 108 | /> | ||
| 109 | <Route | ||
| 110 | path="/users/*" | ||
| 111 | element={<User profile={profile} token={token} gameData={games} />} | ||
| 112 | /> | ||
| 87 | <Route path="/games" element={<Games games={games} />} /> | 113 | <Route path="/games" element={<Games games={games} />} /> |
| 88 | <Route path='/games/:id' element={<Maplist />}></Route> | 114 | <Route path="/games/:id" element={<Maplist />}></Route> |
| 89 | <Route path="/maps/*" element={<Maps token={token} isModerator={isModerator} />} /> | 115 | <Route |
| 116 | path="/maps/*" | ||
| 117 | element={<Maps token={token} isModerator={isModerator} />} | ||
| 118 | /> | ||
| 90 | <Route path="/rules" element={<Rules />} /> | 119 | <Route path="/rules" element={<Rules />} /> |
| 91 | <Route path="/about" element={<About />} /> | 120 | <Route path="/about" element={<About />} /> |
| 92 | <Route path='/rankings' element={<Rankings />}></Route> | 121 | <Route path="/rankings" element={<Rankings />}></Route> |
| 93 | <Route path="*" element={"404"} /> | 122 | <Route path="*" element={"404"} /> |
| 94 | </Routes> | 123 | </Routes> |
| 95 | </> | 124 | </> |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 862e688..b782d17 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -1,18 +1,39 @@ | |||
| 1 | import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '@customTypes/Content'; | 1 | import { MapDiscussionContent, ModMenuContent } from "@customTypes/Content"; |
| 2 | import { delete_token, get_token } from '@api/Auth'; | 2 | import { delete_token, get_token } from "@api/Auth"; |
| 3 | import { get_user, get_profile, post_profile } from '@api/User'; | 3 | import { get_user, get_profile, post_profile } from "@api/User"; |
| 4 | import { get_games, get_chapters, get_games_chapters, get_game_maps, get_search } from '@api/Games'; | 4 | import { |
| 5 | import { get_official_rankings, get_unofficial_rankings } from '@api/Rankings'; | 5 | get_games, |
| 6 | import { get_map_summary, get_map_leaderboard, get_map_discussions, get_map_discussion, post_map_discussion, post_map_discussion_comment, delete_map_discussion, post_record, delete_map_record } from '@api/Maps'; | 6 | get_chapters, |
| 7 | import { delete_map_summary, post_map_summary, put_map_image, put_map_summary } from '@api/Mod'; | 7 | get_games_chapters, |
| 8 | import { UploadRunContent } from '@customTypes/Content'; | 8 | get_game_maps, |
| 9 | get_search, | ||
| 10 | } from "@api/Games"; | ||
| 11 | import { get_official_rankings, get_unofficial_rankings } from "@api/Rankings"; | ||
| 12 | import { | ||
| 13 | get_map_summary, | ||
| 14 | get_map_leaderboard, | ||
| 15 | get_map_discussions, | ||
| 16 | get_map_discussion, | ||
| 17 | post_map_discussion, | ||
| 18 | post_map_discussion_comment, | ||
| 19 | delete_map_discussion, | ||
| 20 | post_record, | ||
| 21 | delete_map_record, | ||
| 22 | } from "@api/Maps"; | ||
| 23 | import { | ||
| 24 | delete_map_summary, | ||
| 25 | post_map_summary, | ||
| 26 | put_map_image, | ||
| 27 | put_map_summary, | ||
| 28 | } from "@api/Mod"; | ||
| 29 | import { UploadRunContent } from "@customTypes/Content"; | ||
| 9 | 30 | ||
| 10 | // add new api call function entries here | 31 | // add new api call function entries here |
| 11 | // example usage: API.get_games(); | 32 | // example usage: API.get_games(); |
| 12 | export const API = { | 33 | export const API = { |
| 13 | // Auth | 34 | // Auth |
| 14 | get_token: () => get_token(), | 35 | get_token: () => get_token(), |
| 15 | 36 | ||
| 16 | delete_token: () => delete_token(), | 37 | delete_token: () => delete_token(), |
| 17 | // User | 38 | // User |
| 18 | get_user: (user_id: string) => get_user(user_id), | 39 | get_user: (user_id: string) => get_user(user_id), |
| @@ -29,28 +50,49 @@ export const API = { | |||
| 29 | get_unofficial_rankings: () => get_unofficial_rankings(), | 50 | get_unofficial_rankings: () => get_unofficial_rankings(), |
| 30 | // Maps | 51 | // Maps |
| 31 | get_map_summary: (map_id: string) => get_map_summary(map_id), | 52 | get_map_summary: (map_id: string) => get_map_summary(map_id), |
| 32 | get_map_leaderboard: (map_id: string, page: string) => get_map_leaderboard(map_id, page), | 53 | get_map_leaderboard: (map_id: string, page: string) => |
| 54 | get_map_leaderboard(map_id, page), | ||
| 33 | get_map_discussions: (map_id: string) => get_map_discussions(map_id), | 55 | get_map_discussions: (map_id: string) => get_map_discussions(map_id), |
| 34 | get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), | 56 | get_map_discussion: (map_id: string, discussion_id: number) => |
| 57 | get_map_discussion(map_id, discussion_id), | ||
| 35 | 58 | ||
| 36 | post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), | 59 | post_map_discussion: ( |
| 37 | post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), | 60 | token: string, |
| 38 | post_record: (token: string, run: UploadRunContent, map_id: number) => post_record(token, run, map_id), | 61 | map_id: string, |
| 62 | content: MapDiscussionContent | ||
| 63 | ) => post_map_discussion(token, map_id, content), | ||
| 64 | post_map_discussion_comment: ( | ||
| 65 | token: string, | ||
| 66 | map_id: string, | ||
| 67 | discussion_id: number, | ||
| 68 | comment: string | ||
| 69 | ) => post_map_discussion_comment(token, map_id, discussion_id, comment), | ||
| 70 | post_record: (token: string, run: UploadRunContent, map_id: number) => | ||
| 71 | post_record(token, run, map_id), | ||
| 39 | 72 | ||
| 40 | delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), | 73 | delete_map_discussion: ( |
| 74 | token: string, | ||
| 75 | map_id: string, | ||
| 76 | discussion_id: number | ||
| 77 | ) => delete_map_discussion(token, map_id, discussion_id), | ||
| 41 | 78 | ||
| 42 | delete_map_record: (token: string, map_id: number, record_id: number) => delete_map_record(token, map_id, record_id), | 79 | delete_map_record: (token: string, map_id: number, record_id: number) => |
| 80 | delete_map_record(token, map_id, record_id), | ||
| 43 | // Mod | 81 | // Mod |
| 44 | post_map_summary: (token: string, map_id: string, content: ModMenuContent) => post_map_summary(token, map_id, content), | 82 | post_map_summary: (token: string, map_id: string, content: ModMenuContent) => |
| 45 | 83 | post_map_summary(token, map_id, content), | |
| 46 | put_map_image: (token: string, map_id: string, image: string) => put_map_image(token, map_id, image), | 84 | |
| 47 | put_map_summary: (token: string, map_id: string, content: ModMenuContent) => put_map_summary(token, map_id, content), | 85 | put_map_image: (token: string, map_id: string, image: string) => |
| 48 | 86 | put_map_image(token, map_id, image), | |
| 49 | delete_map_summary: (token: string, map_id: string, route_id: number) => delete_map_summary(token, map_id, route_id), | 87 | put_map_summary: (token: string, map_id: string, content: ModMenuContent) => |
| 88 | put_map_summary(token, map_id, content), | ||
| 89 | |||
| 90 | delete_map_summary: (token: string, map_id: string, route_id: number) => | ||
| 91 | delete_map_summary(token, map_id, route_id), | ||
| 50 | }; | 92 | }; |
| 51 | 93 | ||
| 52 | const BASE_API_URL: string = "/api/v1/" | 94 | const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/" |
| 53 | 95 | ||
| 54 | export function url(path: string): string { | 96 | export function url(path: string): string { |
| 55 | return BASE_API_URL + path; | 97 | return BASE_API_URL + path; |
| 56 | }; | 98 | } |
diff --git a/frontend/src/api/Auth.ts b/frontend/src/api/Auth.ts index 875c7e5..98c6d36 100644 --- a/frontend/src/api/Auth.ts +++ b/frontend/src/api/Auth.ts | |||
| @@ -2,7 +2,7 @@ import axios from "axios"; | |||
| 2 | import { url } from "@api/Api"; | 2 | import { url } from "@api/Api"; |
| 3 | 3 | ||
| 4 | export const get_token = async (): Promise<string | undefined> => { | 4 | export const get_token = async (): Promise<string | undefined> => { |
| 5 | const response = await axios.get(url(`token`)) | 5 | const response = await axios.get(url(`token`)); |
| 6 | if (!response.data.success) { | 6 | if (!response.data.success) { |
| 7 | return undefined; | 7 | return undefined; |
| 8 | } | 8 | } |
diff --git a/frontend/src/api/Games.ts b/frontend/src/api/Games.ts index 72bb4b3..b739f80 100644 --- a/frontend/src/api/Games.ts +++ b/frontend/src/api/Games.ts | |||
| @@ -6,26 +6,30 @@ import { Map } from "@customTypes/Map"; | |||
| 6 | import { Search } from "@customTypes/Search"; | 6 | import { Search } from "@customTypes/Search"; |
| 7 | 7 | ||
| 8 | export const get_games = async (): Promise<Game[]> => { | 8 | export const get_games = async (): Promise<Game[]> => { |
| 9 | const response = await axios.get(url(`games`)) | 9 | const response = await axios.get(url(`games`)); |
| 10 | return response.data.data; | 10 | return response.data.data; |
| 11 | }; | 11 | }; |
| 12 | 12 | ||
| 13 | export const get_chapters = async (chapter_id: string): Promise<GameChapter> => { | 13 | export const get_chapters = async ( |
| 14 | chapter_id: string | ||
| 15 | ): Promise<GameChapter> => { | ||
| 14 | const response = await axios.get(url(`chapters/${chapter_id}`)); | 16 | const response = await axios.get(url(`chapters/${chapter_id}`)); |
| 15 | return response.data.data; | 17 | return response.data.data; |
| 16 | } | 18 | }; |
| 17 | 19 | ||
| 18 | export const get_games_chapters = async (game_id: string): Promise<GamesChapters> => { | 20 | export const get_games_chapters = async ( |
| 21 | game_id: string | ||
| 22 | ): Promise<GamesChapters> => { | ||
| 19 | const response = await axios.get(url(`games/${game_id}`)); | 23 | const response = await axios.get(url(`games/${game_id}`)); |
| 20 | return response.data.data; | 24 | return response.data.data; |
| 21 | }; | 25 | }; |
| 22 | 26 | ||
| 23 | export const get_game_maps = async (game_id: string): Promise<Map[]> => { | 27 | export const get_game_maps = async (game_id: string): Promise<Map[]> => { |
| 24 | const response = await axios.get(url(`games/${game_id}/maps`)) | 28 | const response = await axios.get(url(`games/${game_id}/maps`)); |
| 25 | return response.data.data.maps; | 29 | return response.data.data.maps; |
| 26 | }; | 30 | }; |
| 27 | 31 | ||
| 28 | export const get_search = async (q: string): Promise<Search> => { | 32 | export const get_search = async (q: string): Promise<Search> => { |
| 29 | const response = await axios.get(url(`search?q=${q}`)) | 33 | const response = await axios.get(url(`search?q=${q}`)); |
| 30 | return response.data.data; | 34 | return response.data.data; |
| 31 | }; | 35 | }; |
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts index aa967ce..2485941 100644 --- a/frontend/src/api/Maps.ts +++ b/frontend/src/api/Maps.ts | |||
| @@ -1,15 +1,25 @@ | |||
| 1 | import axios from "axios"; | 1 | import axios from "axios"; |
| 2 | import { url } from "@api/Api"; | 2 | import { url } from "@api/Api"; |
| 3 | import { MapDiscussionContent, UploadRunContent } from "@customTypes/Content"; | 3 | import { MapDiscussionContent, UploadRunContent } from "@customTypes/Content"; |
| 4 | import { MapSummary, MapLeaderboard, MapDiscussions, MapDiscussion } from "@customTypes/Map"; | 4 | import { |
| 5 | MapSummary, | ||
| 6 | MapLeaderboard, | ||
| 7 | MapDiscussions, | ||
| 8 | MapDiscussion, | ||
| 9 | } from "@customTypes/Map"; | ||
| 5 | 10 | ||
| 6 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { | 11 | export const get_map_summary = async (map_id: string): Promise<MapSummary> => { |
| 7 | const response = await axios.get(url(`maps/${map_id}/summary`)) | 12 | const response = await axios.get(url(`maps/${map_id}/summary`)); |
| 8 | return response.data.data; | 13 | return response.data.data; |
| 9 | }; | 14 | }; |
| 10 | 15 | ||
| 11 | export const get_map_leaderboard = async (map_id: string, page: string): Promise<MapLeaderboard | undefined> => { | 16 | export const get_map_leaderboard = async ( |
| 12 | const response = await axios.get(url(`maps/${map_id}/leaderboards?page=${page}`)); | 17 | map_id: string, |
| 18 | page: string | ||
| 19 | ): Promise<MapLeaderboard | undefined> => { | ||
| 20 | const response = await axios.get( | ||
| 21 | url(`maps/${map_id}/leaderboards?page=${page}`) | ||
| 22 | ); | ||
| 13 | if (!response.data.success) { | 23 | if (!response.data.success) { |
| 14 | return undefined; | 24 | return undefined; |
| 15 | } | 25 | } |
| @@ -17,15 +27,17 @@ export const get_map_leaderboard = async (map_id: string, page: string): Promise | |||
| 17 | // map the kind of leaderboard | 27 | // map the kind of leaderboard |
| 18 | data.records = data.records.map((record: any) => { | 28 | data.records = data.records.map((record: any) => { |
| 19 | if (record.host && record.partner) { | 29 | if (record.host && record.partner) { |
| 20 | return { ...record, kind: 'multiplayer' }; | 30 | return { ...record, kind: "multiplayer" }; |
| 21 | } else { | 31 | } else { |
| 22 | return { ...record, kind: 'singleplayer' }; | 32 | return { ...record, kind: "singleplayer" }; |
| 23 | } | 33 | } |
| 24 | }); | 34 | }); |
| 25 | return data; | 35 | return data; |
| 26 | }; | 36 | }; |
| 27 | 37 | ||
| 28 | export const get_map_discussions = async (map_id: string): Promise<MapDiscussions | undefined> => { | 38 | export const get_map_discussions = async ( |
| 39 | map_id: string | ||
| 40 | ): Promise<MapDiscussions | undefined> => { | ||
| 29 | const response = await axios.get(url(`maps/${map_id}/discussions`)); | 41 | const response = await axios.get(url(`maps/${map_id}/discussions`)); |
| 30 | if (!response.data.data.discussions) { | 42 | if (!response.data.data.discussions) { |
| 31 | return undefined; | 43 | return undefined; |
| @@ -33,74 +45,122 @@ export const get_map_discussions = async (map_id: string): Promise<MapDiscussion | |||
| 33 | return response.data.data; | 45 | return response.data.data; |
| 34 | }; | 46 | }; |
| 35 | 47 | ||
| 36 | export const get_map_discussion = async (map_id: string, discussion_id: number): Promise<MapDiscussion | undefined> => { | 48 | export const get_map_discussion = async ( |
| 37 | const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`)); | 49 | map_id: string, |
| 50 | discussion_id: number | ||
| 51 | ): Promise<MapDiscussion | undefined> => { | ||
| 52 | const response = await axios.get( | ||
| 53 | url(`maps/${map_id}/discussions/${discussion_id}`) | ||
| 54 | ); | ||
| 38 | if (!response.data.data.discussion) { | 55 | if (!response.data.data.discussion) { |
| 39 | return undefined; | 56 | return undefined; |
| 40 | } | 57 | } |
| 41 | return response.data.data; | 58 | return response.data.data; |
| 42 | }; | 59 | }; |
| 43 | 60 | ||
| 44 | export const post_map_discussion = async (token: string, map_id: string, content: MapDiscussionContent): Promise<boolean> => { | 61 | export const post_map_discussion = async ( |
| 45 | const response = await axios.post(url(`maps/${map_id}/discussions`), { | 62 | token: string, |
| 46 | "title": content.title, | 63 | map_id: string, |
| 47 | "content": content.content, | 64 | content: MapDiscussionContent |
| 48 | }, { | 65 | ): Promise<boolean> => { |
| 49 | headers: { | 66 | const response = await axios.post( |
| 50 | "Authorization": token, | 67 | url(`maps/${map_id}/discussions`), |
| 68 | { | ||
| 69 | title: content.title, | ||
| 70 | content: content.content, | ||
| 71 | }, | ||
| 72 | { | ||
| 73 | headers: { | ||
| 74 | Authorization: token, | ||
| 75 | }, | ||
| 51 | } | 76 | } |
| 52 | }); | 77 | ); |
| 53 | return response.data.success; | 78 | return response.data.success; |
| 54 | }; | 79 | }; |
| 55 | 80 | ||
| 56 | export const post_map_discussion_comment = async (token: string, map_id: string, discussion_id: number, comment: string): Promise<boolean> => { | 81 | export const post_map_discussion_comment = async ( |
| 57 | const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), { | 82 | token: string, |
| 58 | "comment": comment, | 83 | map_id: string, |
| 59 | }, { | 84 | discussion_id: number, |
| 60 | headers: { | 85 | comment: string |
| 61 | "Authorization": token, | 86 | ): Promise<boolean> => { |
| 87 | const response = await axios.post( | ||
| 88 | url(`maps/${map_id}/discussions/${discussion_id}`), | ||
| 89 | { | ||
| 90 | comment: comment, | ||
| 91 | }, | ||
| 92 | { | ||
| 93 | headers: { | ||
| 94 | Authorization: token, | ||
| 95 | }, | ||
| 62 | } | 96 | } |
| 63 | }); | 97 | ); |
| 64 | return response.data.success; | 98 | return response.data.success; |
| 65 | }; | 99 | }; |
| 66 | 100 | ||
| 67 | export const delete_map_discussion = async (token: string, map_id: string, discussion_id: number): Promise<boolean> => { | 101 | export const delete_map_discussion = async ( |
| 68 | const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`), { | 102 | token: string, |
| 69 | headers: { | 103 | map_id: string, |
| 70 | "Authorization": token, | 104 | discussion_id: number |
| 105 | ): Promise<boolean> => { | ||
| 106 | const response = await axios.delete( | ||
| 107 | url(`maps/${map_id}/discussions/${discussion_id}`), | ||
| 108 | { | ||
| 109 | headers: { | ||
| 110 | Authorization: token, | ||
| 111 | }, | ||
| 71 | } | 112 | } |
| 72 | }); | 113 | ); |
| 73 | return response.data.success; | 114 | return response.data.success; |
| 74 | }; | 115 | }; |
| 75 | 116 | ||
| 76 | export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => { | 117 | export const post_record = async ( |
| 118 | token: string, | ||
| 119 | run: UploadRunContent, | ||
| 120 | map_id: number | ||
| 121 | ): Promise<[boolean, string]> => { | ||
| 77 | if (run.partner_demo) { | 122 | if (run.partner_demo) { |
| 78 | const response = await axios.postForm(url(`maps/${map_id}/record`), { | 123 | const response = await axios.postForm( |
| 79 | "host_demo": run.host_demo, | 124 | url(`maps/${map_id}/record`), |
| 80 | "partner_demo": run.partner_demo, | 125 | { |
| 81 | }, { | 126 | host_demo: run.host_demo, |
| 82 | headers: { | 127 | partner_demo: run.partner_demo, |
| 83 | "Authorization": token, | 128 | }, |
| 129 | { | ||
| 130 | headers: { | ||
| 131 | Authorization: token, | ||
| 132 | }, | ||
| 84 | } | 133 | } |
| 85 | }); | 134 | ); |
| 86 | return [response.data.success, response.data.message]; | 135 | return [response.data.success, response.data.message]; |
| 87 | } else { | 136 | } else { |
| 88 | const response = await axios.postForm(url(`maps/${map_id}/record`), { | 137 | const response = await axios.postForm( |
| 89 | "host_demo": run.host_demo, | 138 | url(`maps/${map_id}/record`), |
| 90 | }, { | 139 | { |
| 91 | headers: { | 140 | host_demo: run.host_demo, |
| 92 | "Authorization": token, | 141 | }, |
| 142 | { | ||
| 143 | headers: { | ||
| 144 | Authorization: token, | ||
| 145 | }, | ||
| 93 | } | 146 | } |
| 94 | }); | 147 | ); |
| 95 | return [response.data.success, response.data.message]; | 148 | return [response.data.success, response.data.message]; |
| 96 | } | 149 | } |
| 97 | } | 150 | }; |
| 98 | 151 | ||
| 99 | export const delete_map_record = async (token: string, map_id: number, record_id: number): Promise<boolean> => { | 152 | export const delete_map_record = async ( |
| 100 | const response = await axios.delete(url(`maps/${map_id}/record/${record_id}`), { | 153 | token: string, |
| 101 | headers: { | 154 | map_id: number, |
| 102 | "Authorization": token, | 155 | record_id: number |
| 156 | ): Promise<boolean> => { | ||
| 157 | const response = await axios.delete( | ||
| 158 | url(`maps/${map_id}/record/${record_id}`), | ||
| 159 | { | ||
| 160 | headers: { | ||
| 161 | Authorization: token, | ||
| 162 | }, | ||
| 103 | } | 163 | } |
| 104 | }); | 164 | ); |
| 105 | return response.data.success; | 165 | return response.data.success; |
| 106 | }; | 166 | }; |
diff --git a/frontend/src/api/Mod.ts b/frontend/src/api/Mod.ts index 1511f8b..d682f1a 100644 --- a/frontend/src/api/Mod.ts +++ b/frontend/src/api/Mod.ts | |||
| @@ -2,57 +2,85 @@ import axios from "axios"; | |||
| 2 | import { url } from "@api/Api"; | 2 | import { url } from "@api/Api"; |
| 3 | import { ModMenuContent } from "@customTypes/Content"; | 3 | import { ModMenuContent } from "@customTypes/Content"; |
| 4 | 4 | ||
| 5 | export const put_map_image = async (token: string, map_id: string, image: string): Promise<boolean> => { | 5 | export const put_map_image = async ( |
| 6 | const response = await axios.put(url(`maps/${map_id}/image`), { | 6 | token: string, |
| 7 | "image": image, | 7 | map_id: string, |
| 8 | }, { | 8 | image: string |
| 9 | headers: { | 9 | ): Promise<boolean> => { |
| 10 | "Authorization": token, | 10 | const response = await axios.put( |
| 11 | url(`maps/${map_id}/image`), | ||
| 12 | { | ||
| 13 | image: image, | ||
| 14 | }, | ||
| 15 | { | ||
| 16 | headers: { | ||
| 17 | Authorization: token, | ||
| 18 | }, | ||
| 11 | } | 19 | } |
| 12 | }); | 20 | ); |
| 13 | return response.data.success; | 21 | return response.data.success; |
| 14 | }; | 22 | }; |
| 15 | 23 | ||
| 16 | export const post_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { | 24 | export const post_map_summary = async ( |
| 17 | const response = await axios.post(url(`maps/${map_id}/summary`), { | 25 | token: string, |
| 18 | "category_id": content.category_id, | 26 | map_id: string, |
| 19 | "user_name": content.name, | 27 | content: ModMenuContent |
| 20 | "score_count": content.score, | 28 | ): Promise<boolean> => { |
| 21 | "record_date": content.date, | 29 | const response = await axios.post( |
| 22 | "showcase": content.showcase, | 30 | url(`maps/${map_id}/summary`), |
| 23 | "description": content.description, | 31 | { |
| 24 | }, { | 32 | category_id: content.category_id, |
| 25 | headers: { | 33 | user_name: content.name, |
| 26 | "Authorization": token, | 34 | score_count: content.score, |
| 35 | record_date: content.date, | ||
| 36 | showcase: content.showcase, | ||
| 37 | description: content.description, | ||
| 38 | }, | ||
| 39 | { | ||
| 40 | headers: { | ||
| 41 | Authorization: token, | ||
| 42 | }, | ||
| 27 | } | 43 | } |
| 28 | }); | 44 | ); |
| 29 | return response.data.success; | 45 | return response.data.success; |
| 30 | }; | 46 | }; |
| 31 | 47 | ||
| 32 | export const put_map_summary = async (token: string, map_id: string, content: ModMenuContent): Promise<boolean> => { | 48 | export const put_map_summary = async ( |
| 33 | const response = await axios.put(url(`maps/${map_id}/summary`), { | 49 | token: string, |
| 34 | "route_id": content.id, | 50 | map_id: string, |
| 35 | "user_name": content.name, | 51 | content: ModMenuContent |
| 36 | "score_count": content.score, | 52 | ): Promise<boolean> => { |
| 37 | "record_date": content.date, | 53 | const response = await axios.put( |
| 38 | "showcase": content.showcase, | 54 | url(`maps/${map_id}/summary`), |
| 39 | "description": content.description, | 55 | { |
| 40 | }, { | 56 | route_id: content.id, |
| 41 | headers: { | 57 | user_name: content.name, |
| 42 | "Authorization": token, | 58 | score_count: content.score, |
| 59 | record_date: content.date, | ||
| 60 | showcase: content.showcase, | ||
| 61 | description: content.description, | ||
| 62 | }, | ||
| 63 | { | ||
| 64 | headers: { | ||
| 65 | Authorization: token, | ||
| 66 | }, | ||
| 43 | } | 67 | } |
| 44 | }); | 68 | ); |
| 45 | return response.data.success; | 69 | return response.data.success; |
| 46 | }; | 70 | }; |
| 47 | 71 | ||
| 48 | export const delete_map_summary = async (token: string, map_id: string, route_id: number): Promise<boolean> => { | 72 | export const delete_map_summary = async ( |
| 73 | token: string, | ||
| 74 | map_id: string, | ||
| 75 | route_id: number | ||
| 76 | ): Promise<boolean> => { | ||
| 49 | const response = await axios.delete(url(`maps/${map_id}/summary`), { | 77 | const response = await axios.delete(url(`maps/${map_id}/summary`), { |
| 50 | data: { | 78 | data: { |
| 51 | "route_id": route_id, | 79 | route_id: route_id, |
| 52 | }, | 80 | }, |
| 53 | headers: { | 81 | headers: { |
| 54 | "Authorization": token, | 82 | Authorization: token, |
| 55 | } | 83 | }, |
| 56 | }); | 84 | }); |
| 57 | return response.data.success; | 85 | return response.data.success; |
| 58 | }; | 86 | }; |
diff --git a/frontend/src/api/User.ts b/frontend/src/api/User.ts index 88da0f2..004aa22 100644 --- a/frontend/src/api/User.ts +++ b/frontend/src/api/User.ts | |||
| @@ -10,16 +10,20 @@ export const get_user = async (user_id: string): Promise<UserProfile> => { | |||
| 10 | export const get_profile = async (token: string): Promise<UserProfile> => { | 10 | export const get_profile = async (token: string): Promise<UserProfile> => { |
| 11 | const response = await axios.get(url(`profile`), { | 11 | const response = await axios.get(url(`profile`), { |
| 12 | headers: { | 12 | headers: { |
| 13 | "Authorization": token, | 13 | Authorization: token, |
| 14 | } | 14 | }, |
| 15 | }); | 15 | }); |
| 16 | return response.data.data; | 16 | return response.data.data; |
| 17 | }; | 17 | }; |
| 18 | 18 | ||
| 19 | export const post_profile = async (token: string) => { | 19 | export const post_profile = async (token: string): Promise<void> => { |
| 20 | const _ = await axios.post(url(`profile`), {}, { | 20 | await axios.post( |
| 21 | headers: { | 21 | url(`profile`), |
| 22 | "Authorization": token, | 22 | {}, |
| 23 | { | ||
| 24 | headers: { | ||
| 25 | Authorization: token, | ||
| 26 | }, | ||
| 23 | } | 27 | } |
| 24 | }); | 28 | ); |
| 25 | }; | 29 | }; |
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; |
diff --git a/frontend/src/hooks/UseConfirm.tsx b/frontend/src/hooks/UseConfirm.tsx index e86d70d..4692d53 100644 --- a/frontend/src/hooks/UseConfirm.tsx +++ b/frontend/src/hooks/UseConfirm.tsx | |||
| @@ -1,40 +1,47 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import ConfirmDialog from '@components/ConfirmDialog'; | 2 | import ConfirmDialog from "@components/ConfirmDialog"; |
| 3 | 3 | ||
| 4 | const useConfirm = () => { | 4 | const useConfirm = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | const [title, setTitle] = useState<string>(""); | 6 | const [title, setTitle] = useState<string>(""); |
| 7 | const [subtitle, setSubtitle] = useState<string>(""); | 7 | const [subtitle, setSubtitle] = useState<string>(""); |
| 8 | const [resolvePromise, setResolvePromise] = useState<((value: boolean) => void) | null>(null); | 8 | const [resolvePromise, setResolvePromise] = useState< |
| 9 | ((value: boolean) => void) | null | ||
| 10 | >(null); | ||
| 9 | 11 | ||
| 10 | const confirm = ( titleN: string, subtitleN: string ) => { | 12 | const confirm = (titleN: string, subtitleN: string) => { |
| 11 | setIsOpen(true); | 13 | setIsOpen(true); |
| 12 | setTitle(titleN); | 14 | setTitle(titleN); |
| 13 | setSubtitle(subtitleN); | 15 | setSubtitle(subtitleN); |
| 14 | return new Promise<boolean>((resolve) => { | 16 | return new Promise<boolean>(resolve => { |
| 15 | setResolvePromise(() => resolve); | 17 | setResolvePromise(() => resolve); |
| 16 | }); | 18 | }); |
| 17 | }; | 19 | }; |
| 18 | 20 | ||
| 19 | const handleConfirm = () => { | 21 | const handleConfirm = () => { |
| 20 | setIsOpen(false); | 22 | setIsOpen(false); |
| 21 | if (resolvePromise) { | 23 | if (resolvePromise) { |
| 22 | resolvePromise(true); | 24 | resolvePromise(true); |
| 23 | } | ||
| 24 | } | 25 | } |
| 26 | }; | ||
| 25 | 27 | ||
| 26 | const handleCancel = () => { | 28 | const handleCancel = () => { |
| 27 | setIsOpen(false); | 29 | setIsOpen(false); |
| 28 | if (resolvePromise) { | 30 | if (resolvePromise) { |
| 29 | resolvePromise(false); | 31 | resolvePromise(false); |
| 30 | } | ||
| 31 | } | 32 | } |
| 33 | }; | ||
| 32 | 34 | ||
| 33 | const ConfirmDialogComponent = isOpen && ( | 35 | const ConfirmDialogComponent = isOpen && ( |
| 34 | <ConfirmDialog title={title} subtitle={subtitle} onConfirm={handleConfirm} onCancel={handleCancel}></ConfirmDialog> | 36 | <ConfirmDialog |
| 35 | ); | 37 | title={title} |
| 38 | subtitle={subtitle} | ||
| 39 | onConfirm={handleConfirm} | ||
| 40 | onCancel={handleCancel} | ||
| 41 | ></ConfirmDialog> | ||
| 42 | ); | ||
| 36 | 43 | ||
| 37 | return { confirm, ConfirmDialogComponent }; | 44 | return { confirm, ConfirmDialogComponent }; |
| 38 | } | 45 | }; |
| 39 | 46 | ||
| 40 | export default useConfirm; | 47 | export default useConfirm; |
diff --git a/frontend/src/hooks/UseMessage.tsx b/frontend/src/hooks/UseMessage.tsx index 97ec746..b639fac 100644 --- a/frontend/src/hooks/UseMessage.tsx +++ b/frontend/src/hooks/UseMessage.tsx | |||
| @@ -1,37 +1,43 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import MessageDialog from "@components/MessageDialog"; | 2 | import MessageDialog from "@components/MessageDialog"; |
| 3 | 3 | ||
| 4 | const useMessage = () => { | 4 | const useMessage = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | 6 | ||
| 7 | const [title, setTitle] = useState<string>(""); | 7 | const [title, setTitle] = useState<string>(""); |
| 8 | const [subtitle, setSubtitle] = useState<string>(""); | 8 | const [subtitle, setSubtitle] = useState<string>(""); |
| 9 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); | 9 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>( |
| 10 | null | ||
| 11 | ); | ||
| 10 | 12 | ||
| 11 | const message = (title: string, subtitle: string) => { | 13 | const message = (title: string, subtitle: string) => { |
| 12 | setIsOpen(true); | 14 | setIsOpen(true); |
| 13 | setTitle(title); | 15 | setTitle(title); |
| 14 | setSubtitle(subtitle); | 16 | setSubtitle(subtitle); |
| 15 | return new Promise((resolve) => { | 17 | return new Promise(resolve => { |
| 16 | setResolvePromise(() => resolve); | 18 | setResolvePromise(() => resolve); |
| 17 | }); | 19 | }); |
| 18 | }; | 20 | }; |
| 19 | 21 | ||
| 20 | const handleClose = () => { | 22 | const handleClose = () => { |
| 21 | setIsOpen(false); | 23 | setIsOpen(false); |
| 22 | if (resolvePromise) { | 24 | if (resolvePromise) { |
| 23 | resolvePromise(); | 25 | resolvePromise(); |
| 24 | setResolvePromise(null); | 26 | setResolvePromise(null); |
| 25 | } | 27 | } |
| 26 | }; | 28 | }; |
| 27 | 29 | ||
| 28 | const MessageDialogComponent = isOpen && ( | 30 | const MessageDialogComponent = isOpen && ( |
| 29 | <div className="dialog-container"> | 31 | <div className="dialog-container"> |
| 30 | <MessageDialog title={title} subtitle={subtitle} onClose={handleClose}></MessageDialog> | 32 | <MessageDialog |
| 31 | </div> | 33 | title={title} |
| 32 | ); | 34 | subtitle={subtitle} |
| 35 | onClose={handleClose} | ||
| 36 | ></MessageDialog> | ||
| 37 | </div> | ||
| 38 | ); | ||
| 33 | 39 | ||
| 34 | return { message, MessageDialogComponent }; | 40 | return { message, MessageDialogComponent }; |
| 35 | } | 41 | }; |
| 36 | 42 | ||
| 37 | export default useMessage; | 43 | export default useMessage; |
diff --git a/frontend/src/hooks/UseMessageLoad.tsx b/frontend/src/hooks/UseMessageLoad.tsx index 228c2b4..4cbd0f7 100644 --- a/frontend/src/hooks/UseMessageLoad.tsx +++ b/frontend/src/hooks/UseMessageLoad.tsx | |||
| @@ -1,35 +1,40 @@ | |||
| 1 | import React, { useState } from 'react'; | 1 | import React, { useState } from "react"; |
| 2 | import MessageDialogLoad from "@components/MessageDialogLoad"; | 2 | import MessageDialogLoad from "@components/MessageDialogLoad"; |
| 3 | 3 | ||
| 4 | const useMessageLoad = () => { | 4 | const useMessageLoad = () => { |
| 5 | const [isOpen, setIsOpen] = useState(false); | 5 | const [isOpen, setIsOpen] = useState(false); |
| 6 | 6 | ||
| 7 | const [title, setTitle] = useState<string>(""); | 7 | const [title, setTitle] = useState<string>(""); |
| 8 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>(null); | 8 | const [resolvePromise, setResolvePromise] = useState<(() => void) | null>( |
| 9 | null | ||
| 10 | ); | ||
| 9 | 11 | ||
| 10 | const messageLoad = (title: string) => { | 12 | const messageLoad = (title: string) => { |
| 11 | setIsOpen(true); | 13 | setIsOpen(true); |
| 12 | setTitle(title); | 14 | setTitle(title); |
| 13 | return new Promise((resolve) => { | 15 | return new Promise(resolve => { |
| 14 | setResolvePromise(() => resolve); | 16 | setResolvePromise(() => resolve); |
| 15 | }); | 17 | }); |
| 16 | }; | 18 | }; |
| 17 | 19 | ||
| 18 | const messageLoadClose = () => { | 20 | const messageLoadClose = () => { |
| 19 | setIsOpen(false); | 21 | setIsOpen(false); |
| 20 | if (resolvePromise) { | 22 | if (resolvePromise) { |
| 21 | resolvePromise(); | 23 | resolvePromise(); |
| 22 | setResolvePromise(null); | 24 | setResolvePromise(null); |
| 23 | } | 25 | } |
| 24 | }; | 26 | }; |
| 25 | 27 | ||
| 26 | const MessageDialogLoadComponent = isOpen && ( | 28 | const MessageDialogLoadComponent = isOpen && ( |
| 27 | <div className="dialog-container"> | 29 | <div className="dialog-container"> |
| 28 | <MessageDialogLoad title={title} onClose={messageLoadClose}></MessageDialogLoad> | 30 | <MessageDialogLoad |
| 29 | </div> | 31 | title={title} |
| 30 | ); | 32 | onClose={messageLoadClose} |
| 33 | ></MessageDialogLoad> | ||
| 34 | </div> | ||
| 35 | ); | ||
| 31 | 36 | ||
| 32 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; | 37 | return { messageLoad, messageLoadClose, MessageDialogLoadComponent }; |
| 33 | } | 38 | }; |
| 34 | 39 | ||
| 35 | export default useMessageLoad; | 40 | export default useMessageLoad; |
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx index 198431b..6b46893 100644 --- a/frontend/src/images/Images.tsx +++ b/frontend/src/images/Images.tsx | |||
| @@ -1,29 +1,29 @@ | |||
| 1 | import logo from "./png/logo.png" | 1 | import logo from "./png/logo.png"; |
| 2 | import login from "./png/login.png" | 2 | import { LoginIcon as Login } from "./svgs/steam.tsx"; |
| 3 | import img1 from './png/1.png'; | 3 | import img1 from "./png/1.png"; |
| 4 | import img2 from './png/2.png'; | 4 | import img2 from "./png/2.png"; |
| 5 | import img3 from './png/3.png'; | 5 | import img3 from "./png/3.png"; |
| 6 | import img4 from './png/4.png'; | 6 | import img4 from "./png/4.png"; |
| 7 | import img5 from './png/5.png'; | 7 | import img5 from "./png/5.png"; |
| 8 | import img6 from './png/6.png'; | 8 | import img6 from "./png/6.png"; |
| 9 | import img7 from './png/7.png'; | 9 | import img7 from "./png/7.png"; |
| 10 | import img8 from './png/8.png'; | 10 | import img8 from "./png/8.png"; |
| 11 | import img9 from './png/9.png'; | 11 | import img9 from "./png/9.png"; |
| 12 | import img10 from './png/10.png'; | 12 | import img10 from "./png/10.png"; |
| 13 | import img11 from './png/11.png'; | 13 | import img11 from "./png/11.png"; |
| 14 | import img12 from './png/12.png'; | 14 | import img12 from "./png/12.png"; |
| 15 | import img13 from './png/13.png'; | 15 | import img13 from "./png/13.png"; |
| 16 | import img14 from './png/14.png'; | 16 | import img14 from "./png/14.png"; |
| 17 | import img15 from './png/15.png'; | 17 | import img15 from "./png/15.png"; |
| 18 | import img16 from './png/16.png'; | 18 | import img16 from "./png/16.png"; |
| 19 | import img17 from './png/17.png'; | 19 | import img17 from "./png/17.png"; |
| 20 | import img18 from './png/18.png'; | 20 | import img18 from "./png/18.png"; |
| 21 | import img19 from './png/19.png'; | 21 | import img19 from "./png/19.png"; |
| 22 | import img20 from './png/20.png'; | 22 | import img20 from "./png/20.png"; |
| 23 | import img21 from "./png/21.png"; | 23 | import img21 from "./png/21.png"; |
| 24 | 24 | ||
| 25 | export const LogoIcon = logo; | 25 | export const LogoIcon = logo; |
| 26 | export const LoginIcon = login; | 26 | export const LoginIcon = Login; |
| 27 | 27 | ||
| 28 | export const SearchIcon = img1; | 28 | export const SearchIcon = img1; |
| 29 | export const HomeIcon = img2; | 29 | export const HomeIcon = img2; |
| @@ -45,4 +45,4 @@ export const SteamIcon = img17; | |||
| 45 | export const HistoryIcon = img18; | 45 | export const HistoryIcon = img18; |
| 46 | export const SortIcon = img19; | 46 | export const SortIcon = img19; |
| 47 | export const UploadIcon = img20; | 47 | export const UploadIcon = img20; |
| 48 | export const DeleteIcon = img21; \ No newline at end of file | 48 | export const DeleteIcon = img21; |
diff --git a/frontend/src/images/svgs/steam.tsx b/frontend/src/images/svgs/steam.tsx new file mode 100644 index 0000000..0dc9a04 --- /dev/null +++ b/frontend/src/images/svgs/steam.tsx | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | export function LoginIcon(){ | ||
| 2 | return ( | ||
| 3 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="text-white" height={32} width={32}> | ||
| 4 | <path d="M504 256c0 137-111.2 248-248.4 248-113.8 0-209.6-76.3-239-180.4l95.2 39.3c6.4 32.1 34.9 56.4 68.9 56.4 39.2 0 71.9-32.4 70.2-73.5l84.5-60.2c52.1 1.3 95.8-40.9 95.8-93.5 0-51.6-42-93.5-93.7-93.5s-93.7 42-93.7 93.5l0 1.2-59.2 85.7c-15.5-.9-30.7 3.4-43.5 12.1L8 236.1C18.2 108.4 125.1 8 255.6 8 392.8 8 504 119 504 256zM163.7 384.3l-30.5-12.6c5.6 11.6 15.3 20.8 27.2 25.8 26.9 11.2 57.8-1.6 69-28.4 5.4-13 5.5-27.3 .1-40.3S214 305.6 201 300.2c-12.9-5.4-26.7-5.2-38.9-.6l31.5 13c19.8 8.2 29.2 30.9 20.9 50.7-8.3 19.9-31 29.2-50.8 21zM337.5 129.8a62.3 62.3 0 1 1 0 124.6 62.3 62.3 0 1 1 0-124.6zm.1 109a46.8 46.8 0 1 0 0-93.6 46.8 46.8 0 1 0 0 93.6z" fill="currentColor"/> | ||
| 5 | </svg> | ||
| 6 | ) | ||
| 7 | } \ No newline at end of file | ||
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index eec2ff4..13d180c 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx | |||
| @@ -1,11 +1,11 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactDOM from 'react-dom/client'; | 2 | import ReactDOM from "react-dom/client"; |
| 3 | import { BrowserRouter } from "react-router-dom"; | 3 | import { BrowserRouter } from "react-router-dom"; |
| 4 | 4 | ||
| 5 | import App from './App'; | 5 | import App from "./App"; |
| 6 | 6 | ||
| 7 | const root = ReactDOM.createRoot( | 7 | const root = ReactDOM.createRoot( |
| 8 | document.getElementById('root') as HTMLElement | 8 | document.getElementById("root") as HTMLElement |
| 9 | ); | 9 | ); |
| 10 | 10 | ||
| 11 | root.render( | 11 | root.render( |
diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index a8b7826..7802d75 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx | |||
| @@ -1,40 +1,36 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | |||
| 5 | import '@css/About.css'; | ||
| 6 | 4 | ||
| 7 | const About: React.FC = () => { | 5 | const About: React.FC = () => { |
| 6 | const [aboutText, setAboutText] = React.useState<string>(""); | ||
| 8 | 7 | ||
| 9 | const [aboutText, setAboutText] = React.useState<string>(""); | 8 | React.useEffect(() => { |
| 10 | 9 | const fetchReadme = async () => { | |
| 11 | React.useEffect(() => { | 10 | try { |
| 12 | const fetchReadme = async () => { | 11 | const response = await fetch( |
| 13 | try { | 12 | "https://raw.githubusercontent.com/pektezol/lphub/main/README.md" |
| 14 | const response = await fetch( | 13 | ); |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/README.md' | 14 | if (!response.ok) { |
| 16 | ); | 15 | throw new Error("Failed to fetch README"); |
| 17 | if (!response.ok) { | 16 | } |
| 18 | throw new Error('Failed to fetch README'); | 17 | const readmeText = await response.text(); |
| 19 | } | 18 | setAboutText(readmeText); |
| 20 | const readmeText = await response.text(); | 19 | } catch (error) { |
| 21 | setAboutText(readmeText); | 20 | console.error("Error fetching README:", error); |
| 22 | } catch (error) { | 21 | } |
| 23 | console.error('Error fetching README:', error); | 22 | }; |
| 24 | } | 23 | fetchReadme(); |
| 25 | }; | 24 | }, []); |
| 26 | fetchReadme(); | ||
| 27 | }, []); | ||
| 28 | |||
| 29 | 25 | ||
| 30 | return ( | 26 | return ( |
| 31 | <div id="about"> | 27 | <div className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none"> |
| 32 | <Helmet> | 28 | <Helmet> |
| 33 | <title>LPHUB | About</title> | 29 | <title>LPHUB | About</title> |
| 34 | </Helmet> | 30 | </Helmet> |
| 35 | <ReactMarkdown>{aboutText}</ReactMarkdown> | 31 | <ReactMarkdown className={"overflow-auto"}>{aboutText}</ReactMarkdown> |
| 36 | </div> | 32 | </div> |
| 37 | ); | 33 | ); |
| 38 | }; | 34 | }; |
| 39 | 35 | ||
| 40 | export default About; | 36 | export default About; |
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx index 15cc891..8587635 100644 --- a/frontend/src/pages/Games.tsx +++ b/frontend/src/pages/Games.tsx | |||
| @@ -1,46 +1,29 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Helmet } from 'react-helmet'; | 2 | import { Helmet } from "react-helmet"; |
| 3 | 3 | ||
| 4 | import GameEntry from '@components/GameEntry'; | 4 | import GameEntry from "@components/GameEntry"; |
| 5 | import { Game } from '@customTypes/Game'; | 5 | import { Game } from "@customTypes/Game"; |
| 6 | import "@css/Maps.css" | ||
| 7 | 6 | ||
| 8 | interface GamesProps { | 7 | interface GamesProps { |
| 9 | games: Game[]; | 8 | games: Game[]; |
| 10 | } | 9 | } |
| 11 | 10 | ||
| 12 | const Games: React.FC<GamesProps> = ({ games }) => { | 11 | const Games: React.FC<GamesProps> = ({ games }) => { |
| 13 | 12 | return ( | |
| 14 | const _page_load = () => { | 13 | <div className="ml-20 min-h-screen text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin"> |
| 15 | const loaders = document.querySelectorAll(".loader"); | 14 | <Helmet> |
| 16 | loaders.forEach((loader) => { | 15 | <title>LPHUB | Games</title> |
| 17 | (loader as HTMLElement).style.display = "none"; | 16 | </Helmet> |
| 18 | }); | 17 | <section className="py-12 px-12 w-full"> |
| 19 | } | 18 | <h1 className="text-3xl font-bold mb-8">Games</h1> |
| 20 | 19 | <div className="flex flex-col w-full"> | |
| 21 | React.useEffect(() => { | 20 | {games.map((game, index) => ( |
| 22 | document.querySelectorAll(".games-page-item-body").forEach((game, index) => { | 21 | <GameEntry game={game} key={index} /> |
| 23 | game.innerHTML = ""; | 22 | ))} |
| 24 | }); | ||
| 25 | _page_load(); | ||
| 26 | }, []); | ||
| 27 | |||
| 28 | return ( | ||
| 29 | <div className='games-page'> | ||
| 30 | <Helmet> | ||
| 31 | <title>LPHUB | Games</title> | ||
| 32 | </Helmet> | ||
| 33 | <section> | ||
| 34 | <div className='games-page-content'> | ||
| 35 | <div className='games-page-item-content'> | ||
| 36 | {games.map((game, index) => ( | ||
| 37 | <GameEntry game={game} key={index} /> | ||
| 38 | ))} | ||
| 39 | </div> | ||
| 40 | </div> | ||
| 41 | </section> | ||
| 42 | </div> | 23 | </div> |
| 43 | ); | 24 | </section> |
| 25 | </div> | ||
| 26 | ); | ||
| 44 | }; | 27 | }; |
| 45 | 28 | ||
| 46 | export default Games; | 29 | export default Games; |
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx index 4f46af5..b4ac3b0 100644 --- a/frontend/src/pages/Homepage.tsx +++ b/frontend/src/pages/Homepage.tsx | |||
| @@ -1,22 +1,31 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Helmet } from 'react-helmet'; | 2 | import { Helmet } from "react-helmet"; |
| 3 | 3 | ||
| 4 | const Homepage: React.FC = () => { | 4 | const Homepage: React.FC = () => { |
| 5 | 5 | return ( | |
| 6 | return ( | 6 | <main className="ml-12 relative left-0 w-fullmin-h-screen p-4 sm:p-8 text-foreground font-[--font-barlow-semicondensed-regular]"> |
| 7 | <main> | 7 | <Helmet> |
| 8 | <Helmet> | 8 | <title>LPHUB | Homepage</title> |
| 9 | <title>LPHUB | Homepage</title> | 9 | </Helmet> |
| 10 | </Helmet> | 10 | <section className="p-8"> |
| 11 | <section> | 11 | <p /> |
| 12 | <p /> | 12 | <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1> |
| 13 | <h1>Welcome to Least Portals Hub!</h1> | 13 | <p className="text-lg mb-4 leading-relaxed"> |
| 14 | <p>At the moment, LPHUB is in beta state. This means that the site has only the core functionalities enabled for providing both collaborative information and competitive leaderboards.</p> | 14 | At the moment, LPHUB is in beta state. This means that the site has |
| 15 | <p>The website should feel intuitive to navigate around. For any type of feedback, reach us at LPHUB Discord server.</p> | 15 | only the core functionalities enabled for providing both collaborative |
| 16 | <p>By using LPHUB, you agree that you have read the 'Leaderboard Rules' and the 'About LPHUB' pages.</p> | 16 | information and competitive leaderboards. |
| 17 | </section> | 17 | </p> |
| 18 | </main> | 18 | <p className="text-lg mb-4 leading-relaxed"> |
| 19 | ); | 19 | The website should feel intuitive to navigate around. For any type of |
| 20 | feedback, reach us at LPHUB Discord server. | ||
| 21 | </p> | ||
| 22 | <p className="text-lg mb-4 leading-relaxed"> | ||
| 23 | By using LPHUB, you agree that you have read the 'Leaderboard Rules' | ||
| 24 | and the 'About LPHUB' pages. | ||
| 25 | </p> | ||
| 26 | </section> | ||
| 27 | </main> | ||
| 28 | ); | ||
| 20 | }; | 29 | }; |
| 21 | 30 | ||
| 22 | export default Homepage; | 31 | export default Homepage; |
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx index 76f9a52..2f0491e 100644 --- a/frontend/src/pages/Maplist.tsx +++ b/frontend/src/pages/Maplist.tsx | |||
| @@ -2,7 +2,6 @@ import React, { useEffect } from "react"; | |||
| 2 | import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; | 2 | import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; |
| 3 | import { Helmet } from "react-helmet"; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import "@css/Maplist.css"; | ||
| 6 | import { API } from "@api/Api"; | 5 | import { API } from "@api/Api"; |
| 7 | import { Game } from "@customTypes/Game"; | 6 | import { Game } from "@customTypes/Game"; |
| 8 | import { GameChapter, GamesChapters } from "@customTypes/Chapters"; | 7 | import { GameChapter, GamesChapters } from "@customTypes/Chapters"; |
| @@ -11,7 +10,6 @@ const Maplist: React.FC = () => { | |||
| 11 | const [game, setGame] = React.useState<Game | null>(null); | 10 | const [game, setGame] = React.useState<Game | null>(null); |
| 12 | const [catNum, setCatNum] = React.useState(0); | 11 | const [catNum, setCatNum] = React.useState(0); |
| 13 | const [id, setId] = React.useState(0); | 12 | const [id, setId] = React.useState(0); |
| 14 | const [category, setCategory] = React.useState(0); | ||
| 15 | const [load, setLoad] = React.useState(false); | 13 | const [load, setLoad] = React.useState(false); |
| 16 | const [currentlySelected, setCurrentlySelected] = React.useState<number>(0); | 14 | const [currentlySelected, setCurrentlySelected] = React.useState<number>(0); |
| 17 | const [hasClicked, setHasClicked] = React.useState(false); | 15 | const [hasClicked, setHasClicked] = React.useState(false); |
| @@ -21,7 +19,7 @@ const Maplist: React.FC = () => { | |||
| 21 | 19 | ||
| 22 | const [dropdownActive, setDropdownActive] = React.useState("none"); | 20 | const [dropdownActive, setDropdownActive] = React.useState("none"); |
| 23 | 21 | ||
| 24 | const params = useParams<{ id: string, chapter: string }>(); | 22 | const params = useParams<{ id: string; chapter: string }>(); |
| 25 | const location = useLocation(); | 23 | const location = useLocation(); |
| 26 | const navigate = useNavigate(); | 24 | const navigate = useNavigate(); |
| 27 | 25 | ||
| @@ -34,15 +32,15 @@ const Maplist: React.FC = () => { | |||
| 34 | const _fetch_chapters = async (chapter_id: string) => { | 32 | const _fetch_chapters = async (chapter_id: string) => { |
| 35 | const chapters = await API.get_chapters(chapter_id); | 33 | const chapters = await API.get_chapters(chapter_id); |
| 36 | setCurChapter(chapters); | 34 | setCurChapter(chapters); |
| 37 | } | 35 | }; |
| 38 | 36 | ||
| 39 | const _handle_dropdown_click = () => { | 37 | const _handle_dropdown_click = () => { |
| 40 | if (dropdownActive == "none") { | 38 | if (dropdownActive === "none") { |
| 41 | setDropdownActive("block"); | 39 | setDropdownActive("block"); |
| 42 | } else { | 40 | } else { |
| 43 | setDropdownActive("none"); | 41 | setDropdownActive("none"); |
| 44 | } | 42 | } |
| 45 | } | 43 | }; |
| 46 | 44 | ||
| 47 | // im sorry but im too lazy to fix this right now | 45 | // im sorry but im too lazy to fix this right now |
| 48 | useEffect(() => { | 46 | useEffect(() => { |
| @@ -54,7 +52,7 @@ const Maplist: React.FC = () => { | |||
| 54 | const queryParams = new URLSearchParams(location.search); | 52 | const queryParams = new URLSearchParams(location.search); |
| 55 | if (queryParams.get("chapter")) { | 53 | if (queryParams.get("chapter")) { |
| 56 | let cat = parseFloat(queryParams.get("chapter") || ""); | 54 | let cat = parseFloat(queryParams.get("chapter") || ""); |
| 57 | if (gameId == 2) { | 55 | if (gameId === 2) { |
| 58 | cat += 10; | 56 | cat += 10; |
| 59 | } | 57 | } |
| 60 | _fetch_chapters(cat.toString()); | 58 | _fetch_chapters(cat.toString()); |
| @@ -62,7 +60,7 @@ const Maplist: React.FC = () => { | |||
| 62 | 60 | ||
| 63 | const _fetch_game = async () => { | 61 | const _fetch_game = async () => { |
| 64 | const games = await API.get_games(); | 62 | const games = await API.get_games(); |
| 65 | const foundGame = games.find((game) => game.id === gameId); | 63 | const foundGame = games.find(game => game.id === gameId); |
| 66 | // console.log(foundGame) | 64 | // console.log(foundGame) |
| 67 | if (foundGame) { | 65 | if (foundGame) { |
| 68 | setGame(foundGame); | 66 | setGame(foundGame); |
| @@ -74,111 +72,175 @@ const Maplist: React.FC = () => { | |||
| 74 | const games_chapters = await API.get_games_chapters(gameId.toString()); | 72 | const games_chapters = await API.get_games_chapters(gameId.toString()); |
| 75 | setGameChapters(games_chapters); | 73 | setGameChapters(games_chapters); |
| 76 | setNumChapters(games_chapters.chapters.length); | 74 | setNumChapters(games_chapters.chapters.length); |
| 77 | } | 75 | }; |
| 78 | 76 | ||
| 79 | setLoad(true); | 77 | setLoad(true); |
| 80 | _fetch_game(); | 78 | _fetch_game(); |
| 81 | _fetch_game_chapters(); | 79 | _fetch_game_chapters(); |
| 82 | }, []); | 80 | }, [location.search]); |
| 83 | 81 | ||
| 84 | useEffect(() => { | 82 | useEffect(() => { |
| 85 | const queryParams = new URLSearchParams(location.search); | 83 | const queryParams = new URLSearchParams(location.search); |
| 86 | if (gameChapters != undefined && !queryParams.get("chapter")) { | 84 | if (gameChapters !== undefined && !queryParams.get("chapter")) { |
| 87 | _fetch_chapters(gameChapters!.chapters[0].id.toString()); | 85 | _fetch_chapters(gameChapters!.chapters[0].id.toString()); |
| 88 | } | 86 | } |
| 89 | }, [gameChapters]) | 87 | }, [gameChapters, location.search]); |
| 90 | |||
| 91 | |||
| 92 | 88 | ||
| 93 | return ( | 89 | return ( |
| 94 | <main> | 90 | <main className="*:text-foreground w-[calc(100vw-80px)] relative left-0 ml-20 min-h-screen p-4 sm:p-8"> |
| 95 | <Helmet> | 91 | <Helmet> |
| 96 | <title>LPHUB | Maplist</title> | 92 | <title>LPHUB | Maplist</title> |
| 97 | </Helmet> | 93 | </Helmet> |
| 98 | <section style={{ marginTop: "20px" }}> | 94 | |
| 99 | <Link to="/games"> | 95 | <section className="mt-5"> |
| 100 | <button className="nav-button" style={{ borderRadius: "20px" }}> | 96 | <Link to="/games"> |
| 101 | <i className="triangle"></i> | 97 | <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2"> |
| 102 | <span>Games List</span> | 98 | <i className="triangle mr-2"></i> |
| 103 | </button> | 99 | <span className="px-2">Games List</span> |
| 104 | </Link> | 100 | </button> |
| 101 | </Link> | ||
| 105 | </section> | 102 | </section> |
| 103 | |||
| 106 | {load ? ( | 104 | {load ? ( |
| 107 | <div></div> | 105 | <div></div> |
| 108 | ) : ( | 106 | ) : ( |
| 107 | <section> | ||
| 108 | <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground"> | ||
| 109 | {game?.name} | ||
| 110 | </h1> | ||
| 111 | |||
| 112 | <div | ||
| 113 | className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative" | ||
| 114 | style={{ backgroundImage: `url(${game?.image})` }} | ||
| 115 | > | ||
| 116 | <div className="backdrop-blur-sm flex flex-col w-full"> | ||
| 117 | <div className="h-full flex flex-col justify-center items-center py-6"> | ||
| 118 | <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground"> | ||
| 119 | { | ||
| 120 | game?.category_portals.find( | ||
| 121 | obj => obj.category.id === catNum + 1 | ||
| 122 | )?.portal_count | ||
| 123 | } | ||
| 124 | </h2> | ||
| 125 | <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground"> | ||
| 126 | portals | ||
| 127 | </h3> | ||
| 128 | </div> | ||
| 129 | |||
| 130 | <div className="flex h-12 bg-surface gap-0.5"> | ||
| 131 | {game?.category_portals.map((cat, index) => ( | ||
| 132 | <button | ||
| 133 | key={index} | ||
| 134 | className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${ | ||
| 135 | currentlySelected === cat.category.id || | ||
| 136 | (cat.category.id - 1 === catNum && !hasClicked) | ||
| 137 | ? "bg-surface" | ||
| 138 | : "bg-surface1 hover:bg-surface" | ||
| 139 | }`} | ||
| 140 | onClick={() => { | ||
| 141 | setCatNum(cat.category.id - 1); | ||
| 142 | _update_currently_selected(cat.category.id); | ||
| 143 | }} | ||
| 144 | > | ||
| 145 | <span className="truncate">{cat.category.name}</span> | ||
| 146 | </button> | ||
| 147 | ))} | ||
| 148 | </div> | ||
| 149 | </div> | ||
| 150 | </div> | ||
| 151 | |||
| 152 | <div> | ||
| 109 | <section> | 153 | <section> |
| 110 | <h1>{game?.name}</h1> | 154 | <div> |
| 155 | <span className="text-lg sm:text-lg translate-y-1.5 block mt-2.5 text-foreground"> | ||
| 156 | {curChapter?.chapter.name.split(" - ")[0]} | ||
| 157 | </span> | ||
| 158 | </div> | ||
| 159 | <div | ||
| 160 | onClick={_handle_dropdown_click} | ||
| 161 | className="cursor-pointer select-none flex w-fit items-center" | ||
| 162 | > | ||
| 163 | <span className="text-foreground text-base sm:text-2xl"> | ||
| 164 | {curChapter?.chapter.name.split(" - ")[1]} | ||
| 165 | </span> | ||
| 166 | <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i> | ||
| 167 | </div> | ||
| 168 | \ | ||
| 111 | <div | 169 | <div |
| 112 | style={{ backgroundImage: `url(${game?.image})` }} | 170 | className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${ |
| 113 | className="game-header" | 171 | dropdownActive === "none" ? "hidden" : "block" |
| 172 | }`} | ||
| 114 | > | 173 | > |
| 115 | <div className="blur"> | 174 | {gameChapters?.chapters.map((chapter, i) => { |
| 116 | <div className="game-header-portal-count"> | 175 | return ( |
| 117 | <h2 className="portal-count"> | 176 | <div |
| 118 | { | 177 | key={i} |
| 119 | game?.category_portals.find( | 178 | className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground" |
| 120 | (obj) => obj.category.id === catNum + 1 | 179 | onClick={() => { |
| 121 | )?.portal_count | 180 | _fetch_chapters(chapter.id.toString()); |
| 122 | } | 181 | _handle_dropdown_click(); |
| 123 | </h2> | 182 | }} |
| 124 | <h3>portals</h3> | 183 | > |
| 125 | </div> | 184 | {chapter.name} |
| 126 | <div className="game-header-categories"> | ||
| 127 | {game?.category_portals.map((cat, index) => ( | ||
| 128 | <button key={index} className={currentlySelected == cat.category.id || cat.category.id - 1 == catNum && !hasClicked ? "game-cat-button selected" : "game-cat-button"} onClick={() => { setCatNum(cat.category.id - 1); _update_currently_selected(cat.category.id) }}> | ||
| 129 | <span>{cat.category.name}</span> | ||
| 130 | </button> | ||
| 131 | ))} | ||
| 132 | </div> | ||
| 133 | </div> | 185 | </div> |
| 186 | ); | ||
| 187 | })} | ||
| 134 | </div> | 188 | </div> |
| 189 | </section> | ||
| 135 | 190 | ||
| 136 | <div> | 191 | <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5"> |
| 137 | <section className="chapter-select-container"> | 192 | {curChapter?.maps.map((map, i) => { |
| 138 | <div> | 193 | return ( |
| 139 | <span style={{ fontSize: "18px", transform: "translateY(5px)", display: "block", marginTop: "10px" }}>{curChapter?.chapter.name.split(" - ")[0]}</span> | 194 | <div key={i} className="bg-surface rounded-3xl overflow-hidden"> |
| 195 | <Link to={`/maps/${map.id}`}> | ||
| 196 | <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate"> | ||
| 197 | {map.name} | ||
| 198 | </span> | ||
| 199 | <div | ||
| 200 | className="flex h-40 sm:h-48 bg-cover relative" | ||
| 201 | style={{ backgroundImage: `url(${map.image})` }} | ||
| 202 | > | ||
| 203 | <div className="backdrop-blur-sm w-full flex items-center justify-center"> | ||
| 204 | <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5"> | ||
| 205 | {map.is_disabled | ||
| 206 | ? map.category_portals[0].portal_count | ||
| 207 | : map.category_portals.find( | ||
| 208 | obj => obj.category.id === catNum + 1 | ||
| 209 | )?.portal_count} | ||
| 210 | </span> | ||
| 211 | <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white"> | ||
| 212 | portals | ||
| 213 | </span> | ||
| 140 | </div> | 214 | </div> |
| 141 | <div onClick={_handle_dropdown_click} className="dropdown"> | ||
| 142 | <span>{curChapter?.chapter.name.split(" - ")[1]}</span> | ||
| 143 | <i className="triangle"></i> | ||
| 144 | </div> | 215 | </div> |
| 145 | <div className="dropdown-elements" style={{ display: dropdownActive }}> | ||
| 146 | {gameChapters?.chapters.map((chapter, i) => { | ||
| 147 | return <div className="dropdown-element" onClick={() => { _fetch_chapters(chapter.id.toString()); _handle_dropdown_click() }}>{chapter.name}</div> | ||
| 148 | }) | ||
| 149 | 216 | ||
| 150 | } | 217 | <div className="flex mx-2.5 my-4"> |
| 218 | <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px"> | ||
| 219 | {[1, 2, 3, 4, 5].map((point) => ( | ||
| 220 | <div | ||
| 221 | key={point} | ||
| 222 | className={`flex h-0.5 w-full rounded-3xl ${ | ||
| 223 | point <= (map.difficulty + 1) | ||
| 224 | ? map.difficulty === 0 | ||
| 225 | ? "bg-green-500" | ||
| 226 | : map.difficulty === 1 || map.difficulty === 2 | ||
| 227 | ? "bg-lime-500" | ||
| 228 | : map.difficulty === 3 | ||
| 229 | ? "bg-red-400" | ||
| 230 | : "bg-red-600" | ||
| 231 | : "bg-surface1" | ||
| 232 | }`} | ||
| 233 | /> | ||
| 234 | ))} | ||
| 151 | </div> | 235 | </div> |
| 152 | </section> | 236 | </div> |
| 153 | <section className="maplist"> | 237 | </Link> |
| 154 | {curChapter?.maps.map((map, i) => { | 238 | </div> |
| 155 | return <div className="maplist-entry"> | 239 | ); |
| 156 | <Link to={`/maps/${map.id}`}> | 240 | })} |
| 157 | <span>{map.name}</span> | ||
| 158 | <div className="map-entry-image" style={{ backgroundImage: `url(${map.image})` }}> | ||
| 159 | <div className="blur map"> | ||
| 160 | <span>{map.is_disabled ? map.category_portals[0].portal_count : map.category_portals.find( | ||
| 161 | (obj) => obj.category.id === catNum + 1 | ||
| 162 | )?.portal_count}</span> | ||
| 163 | <span>portals</span> | ||
| 164 | </div> | ||
| 165 | </div> | ||
| 166 | <div className="difficulty-bar"> | ||
| 167 | {/* <span>Difficulty:</span> */} | ||
| 168 | <div className={map.difficulty <= 2 ? "one" : map.difficulty <= 4 ? "two" : map.difficulty <= 6 ? "three" : map.difficulty <= 8 ? "four" : map.difficulty <= 10 ? "five" : "one"}> | ||
| 169 | <div className="difficulty-point"></div> | ||
| 170 | <div className="difficulty-point"></div> | ||
| 171 | <div className="difficulty-point"></div> | ||
| 172 | <div className="difficulty-point"></div> | ||
| 173 | <div className="difficulty-point"></div> | ||
| 174 | </div> | ||
| 175 | </div> | ||
| 176 | </Link> | ||
| 177 | </div> | ||
| 178 | })} | ||
| 179 | </section> | ||
| 180 | </div> | ||
| 181 | </section> | 241 | </section> |
| 242 | </div> | ||
| 243 | </section> | ||
| 182 | )} | 244 | )} |
| 183 | </main> | 245 | </main> |
| 184 | ); | 246 | ); |
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx index fb13563..50fe03b 100644 --- a/frontend/src/pages/Maps.tsx +++ b/frontend/src/pages/Maps.tsx | |||
| @@ -1,28 +1,32 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation } from 'react-router-dom'; | 2 | import { Link, useLocation } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { PortalIcon, FlagIcon, ChatIcon } from '@images/Images'; | 5 | import { PortalIcon, FlagIcon, ChatIcon } from "../images/Images"; |
| 6 | import Summary from '@components/Summary'; | 6 | import Summary from "@components/Summary"; |
| 7 | import Leaderboards from '@components/Leaderboards'; | 7 | import Leaderboards from "@components/Leaderboards"; |
| 8 | import Discussions from '@components/Discussions'; | 8 | import Discussions from "@components/Discussions"; |
| 9 | import ModMenu from '@components/ModMenu'; | 9 | import ModMenu from "@components/ModMenu"; |
| 10 | import { MapDiscussions, MapLeaderboard, MapSummary } from '@customTypes/Map'; | 10 | import { MapDiscussions, MapLeaderboard, MapSummary } from "@customTypes/Map"; |
| 11 | import { API } from '@api/Api'; | 11 | import { API } from "@api/Api"; |
| 12 | import "@css/Maps.css"; | ||
| 13 | 12 | ||
| 14 | interface MapProps { | 13 | interface MapProps { |
| 15 | token?: string; | 14 | token?: string; |
| 16 | isModerator: boolean; | 15 | isModerator: boolean; |
| 17 | }; | 16 | } |
| 18 | 17 | ||
| 19 | const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | 18 | const Maps: React.FC<MapProps> = ({ token, isModerator }) => { |
| 20 | |||
| 21 | const [selectedRun, setSelectedRun] = React.useState<number>(0); | 19 | const [selectedRun, setSelectedRun] = React.useState<number>(0); |
| 22 | 20 | ||
| 23 | const [mapSummaryData, setMapSummaryData] = React.useState<MapSummary | undefined>(undefined); | 21 | const [mapSummaryData, setMapSummaryData] = React.useState< |
| 24 | const [mapLeaderboardData, setMapLeaderboardData] = React.useState<MapLeaderboard | undefined>(undefined); | 22 | MapSummary | undefined |
| 25 | const [mapDiscussionsData, setMapDiscussionsData] = React.useState<MapDiscussions | undefined>(undefined); | 23 | >(undefined); |
| 24 | const [mapLeaderboardData, setMapLeaderboardData] = React.useState< | ||
| 25 | MapLeaderboard | undefined | ||
| 26 | >(undefined); | ||
| 27 | const [mapDiscussionsData, setMapDiscussionsData] = React.useState< | ||
| 28 | MapDiscussions | undefined | ||
| 29 | >(undefined); | ||
| 26 | 30 | ||
| 27 | const [navState, setNavState] = React.useState<number>(0); | 31 | const [navState, setNavState] = React.useState<number>(0); |
| 28 | 32 | ||
| @@ -30,45 +34,66 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 30 | 34 | ||
| 31 | const mapID = location.pathname.split("/")[2]; | 35 | const mapID = location.pathname.split("/")[2]; |
| 32 | 36 | ||
| 33 | const _fetch_map_summary = async () => { | 37 | const _fetch_map_summary = React.useCallback(async () => { |
| 34 | const mapSummary = await API.get_map_summary(mapID); | 38 | const mapSummary = await API.get_map_summary(mapID); |
| 35 | setMapSummaryData(mapSummary); | 39 | setMapSummaryData(mapSummary); |
| 36 | }; | 40 | }, [mapID]); |
| 37 | 41 | ||
| 38 | const _fetch_map_leaderboards = async () => { | 42 | const _fetch_map_leaderboards = React.useCallback(async () => { |
| 39 | const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); | 43 | const mapLeaderboards = await API.get_map_leaderboard(mapID, "1"); |
| 40 | setMapLeaderboardData(mapLeaderboards); | 44 | setMapLeaderboardData(mapLeaderboards); |
| 41 | }; | 45 | }, [mapID]); |
| 42 | 46 | ||
| 43 | const _fetch_map_discussions = async () => { | 47 | const _fetch_map_discussions = React.useCallback(async () => { |
| 44 | const mapDiscussions = await API.get_map_discussions(mapID); | 48 | const mapDiscussions = await API.get_map_discussions(mapID); |
| 45 | setMapDiscussionsData(mapDiscussions); | 49 | setMapDiscussionsData(mapDiscussions); |
| 46 | }; | 50 | }, [mapID]); |
| 47 | 51 | ||
| 48 | React.useEffect(() => { | 52 | React.useEffect(() => { |
| 49 | _fetch_map_summary(); | 53 | _fetch_map_summary(); |
| 50 | _fetch_map_leaderboards(); | 54 | _fetch_map_leaderboards(); |
| 51 | _fetch_map_discussions(); | 55 | _fetch_map_discussions(); |
| 52 | }, [mapID]); | 56 | }, [ |
| 57 | mapID, | ||
| 58 | _fetch_map_discussions, | ||
| 59 | _fetch_map_leaderboards, | ||
| 60 | _fetch_map_summary, | ||
| 61 | ]); | ||
| 53 | 62 | ||
| 54 | if (!mapSummaryData) { | 63 | if (!mapSummaryData) { |
| 55 | // loading placeholder | 64 | // loading placeholder |
| 56 | return ( | 65 | return ( |
| 57 | <> | 66 | <> |
| 58 | <main> | 67 | <main className="*:text-foreground relative left-0 w-[calc(100%-20rem)] min-h-screen p-4 sm:p-8"> |
| 59 | <section id='section1' className='summary1'> | 68 | <section id="section1" className="summary1"> |
| 60 | <div> | 69 | <div> |
| 61 | <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 20px 20px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> | 70 | <Link to="/games"> |
| 71 | <button | ||
| 72 | className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2" | ||
| 73 | > | ||
| 74 | <i className="triangle"></i> | ||
| 75 | <span className="px-2">Games List</span> | ||
| 76 | </button> | ||
| 77 | </Link> | ||
| 62 | </div> | 78 | </div> |
| 63 | </section> | 79 | </section> |
| 64 | 80 | ||
| 65 | <section id='section2' className='summary1'> | 81 | <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap"> |
| 66 | <button className='nav-button'><img src={PortalIcon} alt="" /><span>Summary</span></button> | 82 | <button className="nav-button"> |
| 67 | <button className='nav-button'><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> | 83 | <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> |
| 68 | <button className='nav-button'><img src={ChatIcon} alt="" /><span>Discussions</span></button> | 84 | <span>Summary</span> |
| 85 | </button> | ||
| 86 | <button className="nav-button"> | ||
| 87 | <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> | ||
| 88 | <span>Leaderboards</span> | ||
| 89 | </button> | ||
| 90 | <button className="nav-button"> | ||
| 91 | <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> | ||
| 92 | <span>Discussions</span> | ||
| 93 | </button> | ||
| 69 | </section> | 94 | </section> |
| 70 | 95 | ||
| 71 | <section id='section6' className='summary2' /> | 96 | <section id="section6" className="summary2 mt-4" /> |
| 72 | </main> | 97 | </main> |
| 73 | </> | 98 | </> |
| 74 | ); | 99 | ); |
| @@ -80,29 +105,78 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 80 | <title>LPHUB | {mapSummaryData.map.map_name}</title> | 105 | <title>LPHUB | {mapSummaryData.map.map_name}</title> |
| 81 | <meta name="description" content={mapSummaryData.map.map_name} /> | 106 | <meta name="description" content={mapSummaryData.map.map_name} /> |
| 82 | </Helmet> | 107 | </Helmet> |
| 83 | {isModerator && <ModMenu token={token} data={mapSummaryData} selectedRun={selectedRun} mapID={mapID} />} | 108 | {isModerator && ( |
| 84 | 109 | <ModMenu | |
| 85 | <div id='background-image'> | 110 | token={token} |
| 111 | data={mapSummaryData} | ||
| 112 | selectedRun={selectedRun} | ||
| 113 | mapID={mapID} | ||
| 114 | /> | ||
| 115 | )} | ||
| 116 | |||
| 117 | <div id="background-image"> | ||
| 86 | <img src={mapSummaryData.map.image} alt="" /> | 118 | <img src={mapSummaryData.map.image} alt="" /> |
| 87 | </div> | 119 | </div> |
| 88 | <main> | 120 | <main className="relative left-0 w-full sm:ml-80 sm:w-[calc(100%-20rem)] min-h-screen max-h-screen overflow-y-auto p-4 sm:p-8 scrollbar-thin scrollbar-track-surface scrollbar-thumb-muted hover:scrollbar-thumb-surface1"> |
| 89 | <section id='section1' className='summary1'> | 121 | <section id="section1" className="summary1"> |
| 90 | <div> | 122 | <div> |
| 91 | <Link to="/games"><button className='nav-button' style={{ borderRadius: "20px 0px 0px 20px" }}><i className='triangle'></i><span>Games List</span></button></Link> | 123 | <Link to="/games"> |
| 92 | <Link to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`}><button className='nav-button' style={{ borderRadius: "0px 20px 20px 0px", marginLeft: "2px" }}><i className='triangle'></i><span>{mapSummaryData.map.chapter_name}</span></button></Link> | 124 | <button |
| 93 | <br /><span><b>{mapSummaryData.map.map_name}</b></span> | 125 | className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-[--font-barlow-semicondensed-regular] transition-colors duration-100 hover:bg-surface2 flex items-center px-2" |
| 126 | > | ||
| 127 | <i className="triangle"></i> | ||
| 128 | <span className="px-2">Games List</span> | ||
| 129 | </button> | ||
| 130 | </Link> | ||
| 131 | <Link | ||
| 132 | to={`/games/${mapSummaryData.map.is_coop ? "2" : "1"}?chapter=${mapSummaryData.map.chapter_name.split(" ")[1]}`} | ||
| 133 | > | ||
| 134 | <button | ||
| 135 | className="nav-button ml-2" | ||
| 136 | > | ||
| 137 | <i className="triangle"></i> | ||
| 138 | <span className="px-2">{mapSummaryData.map.chapter_name}</span> | ||
| 139 | </button> | ||
| 140 | </Link> | ||
| 141 | <br /> | ||
| 142 | <span className="block mt-2 text-lg sm:text-xl text-foreground"> | ||
| 143 | <b>{mapSummaryData.map.map_name}</b> | ||
| 144 | </span> | ||
| 94 | </div> | 145 | </div> |
| 95 | </section> | 146 | </section> |
| 96 | 147 | ||
| 97 | <section id='section2' className='summary1'> | 148 | <section id="section2" className="summary1 mt-4 flex gap-2 flex-wrap"> |
| 98 | <button className='nav-button' onClick={() => setNavState(0)}><img src={PortalIcon} alt="" /><span>Summary</span></button> | 149 | <button className="nav-button" onClick={() => setNavState(0)}> |
| 99 | <button className='nav-button' onClick={() => setNavState(1)}><img src={FlagIcon} alt="" /><span>Leaderboards</span></button> | 150 | <img src={PortalIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> |
| 100 | <button className='nav-button' onClick={() => setNavState(2)}><img src={ChatIcon} alt="" /><span>Discussions</span></button> | 151 | <span>Summary</span> |
| 152 | </button> | ||
| 153 | <button className="nav-button" onClick={() => setNavState(1)}> | ||
| 154 | <img src={FlagIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> | ||
| 155 | <span>Leaderboards</span> | ||
| 156 | </button> | ||
| 157 | <button className="nav-button" onClick={() => setNavState(2)}> | ||
| 158 | <img src={ChatIcon} alt="" className="w-5 h-5 sm:w-6 sm:h-6" /> | ||
| 159 | <span>Discussions</span> | ||
| 160 | </button> | ||
| 101 | </section> | 161 | </section> |
| 102 | 162 | ||
| 103 | {navState === 0 && <Summary selectedRun={selectedRun} setSelectedRun={setSelectedRun} data={mapSummaryData} />} | 163 | {navState === 0 && ( |
| 164 | <Summary | ||
| 165 | selectedRun={selectedRun} | ||
| 166 | setSelectedRun={setSelectedRun} | ||
| 167 | data={mapSummaryData} | ||
| 168 | /> | ||
| 169 | )} | ||
| 104 | {navState === 1 && <Leaderboards mapID={mapID} />} | 170 | {navState === 1 && <Leaderboards mapID={mapID} />} |
| 105 | {navState === 2 && <Discussions data={mapDiscussionsData} token={token} isModerator={isModerator} mapID={mapID} onRefresh={() => _fetch_map_discussions()} />} | 171 | {navState === 2 && ( |
| 172 | <Discussions | ||
| 173 | data={mapDiscussionsData} | ||
| 174 | token={token} | ||
| 175 | isModerator={isModerator} | ||
| 176 | mapID={mapID} | ||
| 177 | onRefresh={() => _fetch_map_discussions()} | ||
| 178 | /> | ||
| 179 | )} | ||
| 106 | </main> | 180 | </main> |
| 107 | </> | 181 | </> |
| 108 | ); | 182 | ); |
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 48233bf..f44f587 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -1,16 +1,27 @@ | |||
| 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 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon, DeleteIcon } from '@images/Images'; | 5 | import { |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | SteamIcon, |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 7 | TwitchIcon, |
| 8 | import { Map } from '@customTypes/Map'; | 8 | YouTubeIcon, |
| 9 | import { ticks_to_time } from '@utils/Time'; | 9 | PortalIcon, |
| 10 | import "@css/Profile.css"; | 10 | FlagIcon, |
| 11 | import { API } from '@api/Api'; | 11 | StatisticsIcon, |
| 12 | import useConfirm from '@hooks/UseConfirm'; | 12 | SortIcon, |
| 13 | import useMessage from '@hooks/UseMessage'; | 13 | ThreedotIcon, |
| 14 | DownloadIcon, | ||
| 15 | HistoryIcon, | ||
| 16 | DeleteIcon, | ||
| 17 | } from "@images/Images"; | ||
| 18 | import { UserProfile } from "@customTypes/Profile"; | ||
| 19 | import { Game, GameChapters } from "@customTypes/Game"; | ||
| 20 | import { Map } from "@customTypes/Map"; | ||
| 21 | import { ticks_to_time } from "@utils/Time"; | ||
| 22 | import { API } from "@api/Api"; | ||
| 23 | import useConfirm from "@hooks/UseConfirm"; | ||
| 24 | import useMessage from "@hooks/UseMessage"; | ||
| 14 | import useMessageLoad from "@hooks/UseMessageLoad"; | 25 | import useMessageLoad from "@hooks/UseMessageLoad"; |
| 15 | 26 | ||
| 16 | interface ProfileProps { | 27 | interface ProfileProps { |
| @@ -20,17 +31,25 @@ interface ProfileProps { | |||
| 20 | onDeleteRecord: () => void; | 31 | onDeleteRecord: () => void; |
| 21 | } | 32 | } |
| 22 | 33 | ||
| 23 | const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRecord }) => { | 34 | const Profile: React.FC<ProfileProps> = ({ |
| 35 | profile, | ||
| 36 | token, | ||
| 37 | gameData, | ||
| 38 | onDeleteRecord, | ||
| 39 | }) => { | ||
| 24 | const { confirm, ConfirmDialogComponent } = useConfirm(); | 40 | const { confirm, ConfirmDialogComponent } = useConfirm(); |
| 25 | const { message, MessageDialogComponent } = useMessage(); | 41 | const { message, MessageDialogComponent } = useMessage(); |
| 26 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = useMessageLoad(); | 42 | const { messageLoad, messageLoadClose, MessageDialogLoadComponent } = |
| 43 | useMessageLoad(); | ||
| 27 | const [navState, setNavState] = React.useState(0); | 44 | const [navState, setNavState] = React.useState(0); |
| 28 | const [pageNumber, setPageNumber] = React.useState(1); | 45 | const [pageNumber, setPageNumber] = React.useState(1); |
| 29 | const [pageMax, setPageMax] = React.useState(0); | 46 | const [pageMax, setPageMax] = React.useState(0); |
| 30 | 47 | ||
| 31 | const [game, setGame] = React.useState("0") | 48 | const [game, setGame] = React.useState("0"); |
| 32 | const [chapter, setChapter] = React.useState("0") | 49 | const [chapter, setChapter] = React.useState("0"); |
| 33 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | 50 | const [chapterData, setChapterData] = React.useState<GameChapters | null>( |
| 51 | null | ||
| 52 | ); | ||
| 34 | const [maps, setMaps] = React.useState<Map[]>([]); | 53 | const [maps, setMaps] = React.useState<Map[]>([]); |
| 35 | 54 | ||
| 36 | const navigate = useNavigate(); | 55 | const navigate = useNavigate(); |
| @@ -41,7 +60,7 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 41 | } | 60 | } |
| 42 | }; | 61 | }; |
| 43 | 62 | ||
| 44 | const _get_game_chapters = async () => { | 63 | const _get_game_chapters = React.useCallback(async () => { |
| 45 | if (game && game !== "0") { | 64 | if (game && game !== "0") { |
| 46 | const gameChapters = await API.get_games_chapters(game); | 65 | const gameChapters = await API.get_games_chapters(game); |
| 47 | setChapterData(gameChapters); | 66 | setChapterData(gameChapters); |
| @@ -49,9 +68,9 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 49 | setPageMax(Math.ceil(profile!.records.length / 20)); | 68 | setPageMax(Math.ceil(profile!.records.length / 20)); |
| 50 | setPageNumber(1); | 69 | setPageNumber(1); |
| 51 | } | 70 | } |
| 52 | }; | 71 | }, [game, profile]); |
| 53 | 72 | ||
| 54 | const _get_game_maps = async () => { | 73 | const _get_game_maps = React.useCallback(async () => { |
| 55 | if (chapter === "0") { | 74 | if (chapter === "0") { |
| 56 | const gameMaps = await API.get_game_maps(game); | 75 | const gameMaps = await API.get_game_maps(game); |
| 57 | setMaps(gameMaps); | 76 | setMaps(gameMaps); |
| @@ -63,10 +82,13 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 63 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); | 82 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); |
| 64 | setPageNumber(1); | 83 | setPageNumber(1); |
| 65 | } | 84 | } |
| 66 | }; | 85 | }, [chapter, game]); |
| 67 | 86 | ||
| 68 | const _delete_submission = async (map_id: number, record_id: number) => { | 87 | const _delete_submission = async (map_id: number, record_id: number) => { |
| 69 | const userConfirmed = await confirm("Delete Record", "Are you sure you want to delete this record?"); | 88 | const userConfirmed = await confirm( |
| 89 | "Delete Record", | ||
| 90 | "Are you sure you want to delete this record?" | ||
| 91 | ); | ||
| 70 | 92 | ||
| 71 | if (!userConfirmed) { | 93 | if (!userConfirmed) { |
| 72 | return; | 94 | return; |
| @@ -87,26 +109,24 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 87 | React.useEffect(() => { | 109 | React.useEffect(() => { |
| 88 | if (!profile) { | 110 | if (!profile) { |
| 89 | navigate("/"); | 111 | navigate("/"); |
| 90 | }; | 112 | } |
| 91 | }, [profile]); | 113 | }, [profile, navigate]); |
| 92 | 114 | ||
| 93 | React.useEffect(() => { | 115 | React.useEffect(() => { |
| 94 | if (profile) { | 116 | if (profile) { |
| 95 | _get_game_chapters(); | 117 | _get_game_chapters(); |
| 96 | } | 118 | } |
| 97 | }, [profile, game]); | 119 | }, [profile, game, _get_game_chapters]); |
| 98 | 120 | ||
| 99 | React.useEffect(() => { | 121 | React.useEffect(() => { |
| 100 | if (profile && game !== "0") { | 122 | if (profile && game !== "0") { |
| 101 | _get_game_maps(); | 123 | _get_game_maps(); |
| 102 | } | 124 | } |
| 103 | }, [profile, game, chapter, chapterData]) | 125 | }, [profile, game, chapter, chapterData, _get_game_maps]); |
| 104 | 126 | ||
| 105 | if (!profile) { | 127 | if (!profile) { |
| 106 | return ( | 128 | return <></>; |
| 107 | <></> | 129 | } |
| 108 | ); | ||
| 109 | }; | ||
| 110 | 130 | ||
| 111 | return ( | 131 | return ( |
| 112 | <div> | 132 | <div> |
| @@ -119,230 +139,490 @@ const Profile: React.FC<ProfileProps> = ({ profile, token, gameData, onDeleteRec | |||
| 119 | {ConfirmDialogComponent} | 139 | {ConfirmDialogComponent} |
| 120 | 140 | ||
| 121 | <main> | 141 | <main> |
| 122 | <section id='section1' className='profile'> | 142 | <section id="section1" className="profile"> |
| 123 | 143 | {profile.profile ? ( | |
| 124 | {profile.profile | 144 | <div id="profile-image" onClick={_update_profile}> |
| 125 | ? ( | 145 | <img src={profile.avatar_link} alt="profile-image"></img> |
| 126 | <div id='profile-image' onClick={_update_profile}> | 146 | <span>Refresh</span> |
| 127 | <img src={profile.avatar_link} alt="profile-image"></img> | 147 | </div> |
| 128 | <span>Refresh</span> | 148 | ) : ( |
| 129 | </div> | 149 | <div> |
| 130 | ) : ( | 150 | <img src={profile.avatar_link} alt="profile-image"></img> |
| 131 | <div> | 151 | </div> |
| 132 | <img src={profile.avatar_link} alt="profile-image"></img> | 152 | )} |
| 133 | </div> | ||
| 134 | )} | ||
| 135 | 153 | ||
| 136 | <div id='profile-top'> | 154 | <div id="profile-top"> |
| 137 | <div> | 155 | <div> |
| 138 | <div>{profile.user_name}</div> | 156 | <div>{profile.user_name}</div> |
| 139 | <div> | 157 | <div> |
| 140 | {profile.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} alt={profile.country_code} />} | 158 | {profile.country_code === "XX" ? ( |
| 159 | "" | ||
| 160 | ) : ( | ||
| 161 | <img | ||
| 162 | src={`https://flagcdn.com/w80/${profile.country_code.toLowerCase()}.jpg`} | ||
| 163 | alt={profile.country_code} | ||
| 164 | /> | ||
| 165 | )} | ||
| 141 | </div> | 166 | </div> |
| 142 | <div> | 167 | <div> |
| 143 | {profile.titles.map(e => ( | 168 | {profile.titles.map(e => ( |
| 144 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | 169 | <span |
| 170 | className="titles" | ||
| 171 | style={{ backgroundColor: `#${e.color}` }} | ||
| 172 | > | ||
| 145 | {e.name} | 173 | {e.name} |
| 146 | </span> | 174 | </span> |
| 147 | ))} | 175 | ))} |
| 148 | </div> | 176 | </div> |
| 149 | </div> | 177 | </div> |
| 150 | <div> | 178 | <div> |
| 151 | {profile.links.steam === "-" ? "" : <a href={profile.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | 179 | {profile.links.steam === "-" ? ( |
| 152 | {profile.links.twitch === "-" ? "" : <a href={profile.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | 180 | "" |
| 153 | {profile.links.youtube === "-" ? "" : <a href={profile.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | 181 | ) : ( |
| 154 | {profile.links.p2sr === "-" ? "" : <a href={profile.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | 182 | <a href={profile.links.steam}> |
| 183 | <img src={SteamIcon} alt="Steam" /> | ||
| 184 | </a> | ||
| 185 | )} | ||
| 186 | {profile.links.twitch === "-" ? ( | ||
| 187 | "" | ||
| 188 | ) : ( | ||
| 189 | <a href={profile.links.twitch}> | ||
| 190 | <img src={TwitchIcon} alt="Twitch" /> | ||
| 191 | </a> | ||
| 192 | )} | ||
| 193 | {profile.links.youtube === "-" ? ( | ||
| 194 | "" | ||
| 195 | ) : ( | ||
| 196 | <a href={profile.links.youtube}> | ||
| 197 | <img src={YouTubeIcon} alt="Youtube" /> | ||
| 198 | </a> | ||
| 199 | )} | ||
| 200 | {profile.links.p2sr === "-" ? ( | ||
| 201 | "" | ||
| 202 | ) : ( | ||
| 203 | <a href={profile.links.p2sr}> | ||
| 204 | <img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /> | ||
| 205 | </a> | ||
| 206 | )} | ||
| 155 | </div> | 207 | </div> |
| 156 | |||
| 157 | </div> | 208 | </div> |
| 158 | <div id='profile-bottom'> | 209 | <div id="profile-bottom"> |
| 159 | <div> | 210 | <div> |
| 160 | <span>Overall</span> | 211 | <span>Overall</span> |
| 161 | <span>{profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} | 212 | <span> |
| 162 | <span>({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total})</span> | 213 | {profile.rankings.overall.rank === 0 |
| 214 | ? "N/A " | ||
| 215 | : "#" + profile.rankings.overall.rank + " "} | ||
| 216 | <span> | ||
| 217 | ({profile.rankings.overall.completion_count}/ | ||
| 218 | {profile.rankings.overall.completion_total}) | ||
| 219 | </span> | ||
| 163 | </span> | 220 | </span> |
| 164 | </div> | 221 | </div> |
| 165 | <div> | 222 | <div> |
| 166 | <span>Singleplayer</span> | 223 | <span>Singleplayer</span> |
| 167 | <span>{profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} | 224 | <span> |
| 168 | <span>({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total})</span> | 225 | {profile.rankings.singleplayer.rank === 0 |
| 226 | ? "N/A " | ||
| 227 | : "#" + profile.rankings.singleplayer.rank + " "} | ||
| 228 | <span> | ||
| 229 | ({profile.rankings.singleplayer.completion_count}/ | ||
| 230 | {profile.rankings.singleplayer.completion_total}) | ||
| 231 | </span> | ||
| 169 | </span> | 232 | </span> |
| 170 | </div> | 233 | </div> |
| 171 | <div> | 234 | <div> |
| 172 | <span>Cooperative</span> | 235 | <span>Cooperative</span> |
| 173 | <span>{profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} | 236 | <span> |
| 174 | <span>({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total})</span> | 237 | {profile.rankings.cooperative.rank === 0 |
| 238 | ? "N/A " | ||
| 239 | : "#" + profile.rankings.cooperative.rank + " "} | ||
| 240 | <span> | ||
| 241 | ({profile.rankings.cooperative.completion_count}/ | ||
| 242 | {profile.rankings.cooperative.completion_total}) | ||
| 243 | </span> | ||
| 175 | </span> | 244 | </span> |
| 176 | </div> | 245 | </div> |
| 177 | </div> | 246 | </div> |
| 178 | </section> | 247 | </section> |
| 179 | 248 | ||
| 180 | 249 | <section id="section2" className="profile"> | |
| 181 | <section id='section2' className='profile'> | 250 | <button onClick={() => setNavState(0)}> |
| 182 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | 251 | <img src={FlagIcon} alt="" /> |
| 183 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | 252 | Player Records |
| 253 | </button> | ||
| 254 | <button onClick={() => setNavState(1)}> | ||
| 255 | <img src={StatisticsIcon} alt="" /> | ||
| 256 | Statistics | ||
| 257 | </button> | ||
| 184 | </section> | 258 | </section> |
| 185 | 259 | ||
| 186 | 260 | <section id="section3" className="profile1"> | |
| 187 | 261 | <div id="profileboard-nav"> | |
| 188 | 262 | {gameData === null ? ( | |
| 189 | 263 | <select>error</select> | |
| 190 | <section id='section3' className='profile1'> | 264 | ) : ( |
| 191 | <div id='profileboard-nav'> | 265 | <select |
| 192 | {gameData === null ? <select>error</select> : | 266 | id="select-game" |
| 193 | |||
| 194 | <select id='select-game' | ||
| 195 | onChange={() => { | 267 | onChange={() => { |
| 196 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | 268 | setGame( |
| 269 | (document.querySelector("#select-game") as HTMLInputElement) | ||
| 270 | .value | ||
| 271 | ); | ||
| 197 | setChapter("0"); | 272 | setChapter("0"); |
| 198 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 273 | const chapterSelect = document.querySelector( |
| 274 | "#select-chapter" | ||
| 275 | ) as HTMLSelectElement; | ||
| 199 | if (chapterSelect) { | 276 | if (chapterSelect) { |
| 200 | chapterSelect.value = "0"; | 277 | chapterSelect.value = "0"; |
| 201 | } | 278 | } |
| 202 | }}> | 279 | }} |
| 203 | <option value={0} key={0}>All Scores</option> | 280 | > |
| 281 | <option value={0} key={0}> | ||
| 282 | All Scores | ||
| 283 | </option> | ||
| 204 | {gameData.map((e, i) => ( | 284 | {gameData.map((e, i) => ( |
| 205 | <option value={e.id} key={i + 1}>{e.name}</option> | 285 | <option value={e.id} key={i + 1}> |
| 206 | ))}</select> | 286 | {e.name} |
| 207 | } | 287 | </option> |
| 288 | ))} | ||
| 289 | </select> | ||
| 290 | )} | ||
| 208 | 291 | ||
| 209 | {game === "0" ? | 292 | {game === "0" ? ( |
| 210 | <select disabled> | 293 | <select disabled> |
| 211 | <option>All Chapters</option> | 294 | <option>All Chapters</option> |
| 212 | </select> | 295 | </select> |
| 213 | : chapterData === null ? <select></select> : | 296 | ) : chapterData === null ? ( |
| 214 | 297 | <select></select> | |
| 215 | <select id='select-chapter' | 298 | ) : ( |
| 216 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 299 | <select |
| 217 | <option value="0" key="0">All Chapters</option> | 300 | id="select-chapter" |
| 218 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 301 | onChange={() => |
| 219 | <option value={e.id} key={i + 1}>{e.name}</option> | 302 | setChapter( |
| 220 | ))}</select> | 303 | ( |
| 221 | } | 304 | document.querySelector( |
| 305 | "#select-chapter" | ||
| 306 | ) as HTMLInputElement | ||
| 307 | ).value | ||
| 308 | ) | ||
| 309 | } | ||
| 310 | > | ||
| 311 | <option value="0" key="0"> | ||
| 312 | All Chapters | ||
| 313 | </option> | ||
| 314 | {chapterData.chapters | ||
| 315 | .filter(e => e.is_disabled === false) | ||
| 316 | .map((e, i) => ( | ||
| 317 | <option value={e.id} key={i + 1}> | ||
| 318 | {e.name} | ||
| 319 | </option> | ||
| 320 | ))} | ||
| 321 | </select> | ||
| 322 | )} | ||
| 222 | </div> | 323 | </div> |
| 223 | <div id='profileboard-top'> | 324 | <div id="profileboard-top"> |
| 224 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 325 | <span> |
| 225 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 326 | <span>Map Name</span> |
| 226 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 327 | <img src={SortIcon} alt="" /> |
| 227 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 328 | </span> |
| 329 | <span style={{ justifyContent: "center" }}> | ||
| 330 | <span>Portals</span> | ||
| 331 | <img src={SortIcon} alt="" /> | ||
| 332 | </span> | ||
| 333 | <span style={{ justifyContent: "center" }}> | ||
| 334 | <span>WRΔ </span> | ||
| 335 | <img src={SortIcon} alt="" /> | ||
| 336 | </span> | ||
| 337 | <span style={{ justifyContent: "center" }}> | ||
| 338 | <span>Time</span> | ||
| 339 | <img src={SortIcon} alt="" /> | ||
| 340 | </span> | ||
| 228 | <span> </span> | 341 | <span> </span> |
| 229 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 342 | <span> |
| 230 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 343 | <span>Rank</span> |
| 231 | <div id='page-number'> | 344 | <img src={SortIcon} alt="" /> |
| 345 | </span> | ||
| 346 | <span> | ||
| 347 | <span>Date</span> | ||
| 348 | <img src={SortIcon} alt="" /> | ||
| 349 | </span> | ||
| 350 | <div id="page-number"> | ||
| 232 | <div> | 351 | <div> |
| 233 | <button onClick={() => { | 352 | <button |
| 234 | if (pageNumber !== 1) { | 353 | onClick={() => { |
| 235 | setPageNumber(prevPageNumber => prevPageNumber - 1); | 354 | if (pageNumber !== 1) { |
| 236 | const records = document.querySelectorAll(".profileboard-record"); | 355 | setPageNumber(prevPageNumber => prevPageNumber - 1); |
| 237 | records.forEach((r) => { | 356 | const records = document.querySelectorAll( |
| 238 | (r as HTMLInputElement).style.height = "44px"; | 357 | ".profileboard-record" |
| 239 | }); | 358 | ); |
| 240 | } | 359 | records.forEach(r => { |
| 241 | }} | 360 | (r as HTMLInputElement).style.height = "44px"; |
| 242 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 361 | }); |
| 243 | <span>{pageNumber}/{pageMax}</span> | 362 | } |
| 244 | <button onClick={() => { | 363 | }} |
| 245 | if (pageNumber !== pageMax) { | 364 | > |
| 246 | setPageNumber(prevPageNumber => prevPageNumber + 1); | 365 | <i |
| 247 | const records = document.querySelectorAll(".profileboard-record"); | 366 | className="triangle" |
| 248 | records.forEach((r) => { | 367 | style={{ position: "relative", left: "-5px" }} |
| 249 | (r as HTMLInputElement).style.height = "44px"; | 368 | ></i>{" "} |
| 250 | }); | 369 | </button> |
| 251 | } | 370 | <span> |
| 252 | }} | 371 | {pageNumber}/{pageMax} |
| 253 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 372 | </span> |
| 373 | <button | ||
| 374 | onClick={() => { | ||
| 375 | if (pageNumber !== pageMax) { | ||
| 376 | setPageNumber(prevPageNumber => prevPageNumber + 1); | ||
| 377 | const records = document.querySelectorAll( | ||
| 378 | ".profileboard-record" | ||
| 379 | ); | ||
| 380 | records.forEach(r => { | ||
| 381 | (r as HTMLInputElement).style.height = "44px"; | ||
| 382 | }); | ||
| 383 | } | ||
| 384 | }} | ||
| 385 | > | ||
| 386 | <i | ||
| 387 | className="triangle" | ||
| 388 | style={{ | ||
| 389 | position: "relative", | ||
| 390 | left: "5px", | ||
| 391 | transform: "rotate(180deg)", | ||
| 392 | }} | ||
| 393 | ></i>{" "} | ||
| 394 | </button> | ||
| 254 | </div> | 395 | </div> |
| 255 | </div> | 396 | </div> |
| 256 | </div> | 397 | </div> |
| 257 | <hr /> | 398 | <hr /> |
| 258 | <div id='profileboard-records'> | 399 | <div id="profileboard-records"> |
| 259 | 400 | {game === "0" ? ( | |
| 260 | {game === "0" | 401 | profile.records |
| 261 | ? ( | 402 | .sort((a, b) => a.map_id - b.map_id) |
| 262 | 403 | .map((r, index) => | |
| 263 | profile.records.sort((a, b) => a.map_id - b.map_id) | 404 | Math.ceil((index + 1) / 20) === pageNumber ? ( |
| 264 | .map((r, index) => ( | 405 | <button className="profileboard-record" key={index}> |
| 265 | 406 | {r.scores.map((e, i) => ( | |
| 266 | Math.ceil((index + 1) / 20) === pageNumber ? ( | 407 | <> |
| 267 | <button className="profileboard-record" key={index}> | 408 | {i !== 0 ? ( |
| 268 | {r.scores.map((e, i) => (<> | 409 | <hr style={{ gridColumn: "1 / span 8" }} /> |
| 269 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 410 | ) : ( |
| 270 | 411 | "" | |
| 271 | <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> | 412 | )} |
| 272 | 413 | ||
| 273 | <span style={{ display: "grid" }}>{e.score_count}</span> | 414 | <Link to={`/maps/${r.map_id}`}> |
| 415 | <span>{r.map_name}</span> | ||
| 416 | </Link> | ||
| 417 | |||
| 418 | <span style={{ display: "grid" }}> | ||
| 419 | {e.score_count} | ||
| 420 | </span> | ||
| 274 | 421 | ||
| 275 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 422 | <span style={{ display: "grid" }}> |
| 276 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 423 | {e.score_count - r.map_wr_count > 0 |
| 424 | ? `+${e.score_count - r.map_wr_count}` | ||
| 425 | : `-`} | ||
| 426 | </span> | ||
| 427 | <span style={{ display: "grid" }}> | ||
| 428 | {ticks_to_time(e.score_time)} | ||
| 429 | </span> | ||
| 277 | <span> </span> | 430 | <span> </span> |
| 278 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 431 | {i === 0 ? ( |
| 432 | <span>#{r.placement}</span> | ||
| 433 | ) : ( | ||
| 434 | <span> </span> | ||
| 435 | )} | ||
| 279 | <span>{e.date.split("T")[0]}</span> | 436 | <span>{e.date.split("T")[0]}</span> |
| 280 | <span style={{ flexDirection: "row-reverse" }}> | 437 | <span style={{ flexDirection: "row-reverse" }}> |
| 281 | 438 | <button | |
| 282 | <button style={{ marginRight: "10px" }} onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 439 | style={{ marginRight: "10px" }} |
| 283 | <button onClick={() => { _delete_submission(r.map_id, e.record_id) }}><img src={DeleteIcon}></img></button> | 440 | onClick={() => { |
| 284 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 441 | message( |
| 285 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | 442 | "Demo Information", |
| 286 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 443 | `Demo ID: ${e.demo_id}` |
| 287 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 444 | ); |
| 288 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | 445 | }} |
| 289 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 446 | > |
| 290 | } | 447 | <img src={ThreedotIcon} alt="demo_id" /> |
| 291 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 448 | </button> |
| 292 | 449 | <button | |
| 450 | onClick={() => { | ||
| 451 | _delete_submission(r.map_id, e.record_id); | ||
| 452 | }} | ||
| 453 | > | ||
| 454 | <img src={DeleteIcon} alt="delete icon"></img> | ||
| 455 | </button> | ||
| 456 | <button | ||
| 457 | onClick={() => | ||
| 458 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 459 | } | ||
| 460 | > | ||
| 461 | <img src={DownloadIcon} alt="download" /> | ||
| 462 | </button> | ||
| 463 | {i === 0 && r.scores.length > 1 ? ( | ||
| 464 | <button | ||
| 465 | onClick={() => { | ||
| 466 | ( | ||
| 467 | document.querySelectorAll( | ||
| 468 | ".profileboard-record" | ||
| 469 | )[index % 20] as HTMLInputElement | ||
| 470 | ).style.height === "44px" || | ||
| 471 | ( | ||
| 472 | document.querySelectorAll( | ||
| 473 | ".profileboard-record" | ||
| 474 | )[index % 20] as HTMLInputElement | ||
| 475 | ).style.height === "" | ||
| 476 | ? (( | ||
| 477 | document.querySelectorAll( | ||
| 478 | ".profileboard-record" | ||
| 479 | )[index % 20] as HTMLInputElement | ||
| 480 | ).style.height = | ||
| 481 | `${r.scores.length * 46}px`) | ||
| 482 | : (( | ||
| 483 | document.querySelectorAll( | ||
| 484 | ".profileboard-record" | ||
| 485 | )[index % 20] as HTMLInputElement | ||
| 486 | ).style.height = "44px"); | ||
| 487 | }} | ||
| 488 | > | ||
| 489 | <img src={HistoryIcon} alt="history" /> | ||
| 490 | </button> | ||
| 491 | ) : ( | ||
| 492 | "" | ||
| 493 | )} | ||
| 293 | </span> | 494 | </span> |
| 294 | </>))} | 495 | </> |
| 295 | 496 | ))} | |
| 497 | </button> | ||
| 498 | ) : ( | ||
| 499 | "" | ||
| 500 | ) | ||
| 501 | ) | ||
| 502 | ) : maps ? ( | ||
| 503 | maps | ||
| 504 | .filter(e => e.is_disabled === false) | ||
| 505 | .sort((a, b) => a.id - b.id) | ||
| 506 | .map((r, index) => { | ||
| 507 | if (Math.ceil((index + 1) / 20) === pageNumber) { | ||
| 508 | let record = profile.records.find(e => e.map_id === r.id); | ||
| 509 | return record === undefined ? ( | ||
| 510 | <button | ||
| 511 | className="profileboard-record" | ||
| 512 | key={index} | ||
| 513 | style={{ backgroundColor: "#1b1b20" }} | ||
| 514 | > | ||
| 515 | <Link to={`/maps/${r.id}`}> | ||
| 516 | <span>{r.name}</span> | ||
| 517 | </Link> | ||
| 518 | <span style={{ display: "grid" }}>N/A</span> | ||
| 519 | <span style={{ display: "grid" }}>N/A</span> | ||
| 520 | <span>N/A</span> | ||
| 521 | <span> </span> | ||
| 522 | <span>N/A</span> | ||
| 523 | <span>N/A</span> | ||
| 524 | <span style={{ flexDirection: "row-reverse" }}></span> | ||
| 296 | </button> | 525 | </button> |
| 297 | ) : "" | 526 | ) : ( |
| 298 | ))) : maps ? | 527 | <button className="profileboard-record" key={index}> |
| 299 | 528 | {record.scores.map((e, i) => ( | |
| 300 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 529 | <> |
| 301 | .map((r, index) => { | 530 | {i !== 0 ? ( |
| 302 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 531 | <hr style={{ gridColumn: "1 / span 8" }} /> |
| 303 | let record = profile.records.find((e) => e.map_id === r.id); | 532 | ) : ( |
| 304 | return record === undefined ? ( | 533 | "" |
| 305 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 534 | )} |
| 306 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 535 | <Link to={`/maps/${r.id}`}> |
| 307 | <span style={{ display: "grid" }}>N/A</span> | 536 | <span>{r.name}</span> |
| 308 | <span style={{ display: "grid" }}>N/A</span> | 537 | </Link> |
| 309 | <span>N/A</span> | 538 | <span style={{ display: "grid" }}> |
| 310 | <span> </span> | 539 | {record!.scores[i].score_count} |
| 311 | <span>N/A</span> | 540 | </span> |
| 312 | <span>N/A</span> | 541 | <span style={{ display: "grid" }}> |
| 313 | <span style={{ flexDirection: "row-reverse" }}></span> | 542 | {record!.scores[i].score_count - |
| 314 | </button> | 543 | record!.map_wr_count > |
| 315 | ) : ( | 544 | 0 |
| 316 | <button className="profileboard-record" key={index}> | 545 | ? `+${record!.scores[i].score_count - record!.map_wr_count}` |
| 317 | {record.scores.map((e, i) => (<> | 546 | : `-`} |
| 318 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 547 | </span> |
| 319 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 548 | <span style={{ display: "grid" }}> |
| 320 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 549 | {ticks_to_time(record!.scores[i].score_time)} |
| 321 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> | 550 | </span> |
| 322 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | ||
| 323 | <span> </span> | 551 | <span> </span> |
| 324 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 552 | {i === 0 ? ( |
| 553 | <span>#{record!.placement}</span> | ||
| 554 | ) : ( | ||
| 555 | <span> </span> | ||
| 556 | )} | ||
| 325 | <span>{record!.scores[i].date.split("T")[0]}</span> | 557 | <span>{record!.scores[i].date.split("T")[0]}</span> |
| 326 | <span style={{ flexDirection: "row-reverse" }}> | 558 | <span style={{ flexDirection: "row-reverse" }}> |
| 327 | 559 | <button | |
| 328 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 560 | onClick={() => { |
| 329 | <button onClick={() => { _delete_submission(r.id, e.record_id) }}><img src={DeleteIcon}></img></button> | 561 | message( |
| 330 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 562 | "Demo Information", |
| 331 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | 563 | `Demo ID: ${e.demo_id}` |
| 332 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 564 | ); |
| 333 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 565 | }} |
| 334 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | 566 | > |
| 335 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 567 | <img src={ThreedotIcon} alt="demo_id" /> |
| 336 | } | 568 | </button> |
| 337 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 569 | <button |
| 338 | 570 | onClick={() => { | |
| 571 | _delete_submission(r.id, e.record_id); | ||
| 572 | }} | ||
| 573 | > | ||
| 574 | <img src={DeleteIcon} alt="delete icon"></img> | ||
| 575 | </button> | ||
| 576 | <button | ||
| 577 | onClick={() => | ||
| 578 | (window.location.href = `/api/v1/demos?uuid=${e.demo_id}`) | ||
| 579 | } | ||
| 580 | > | ||
| 581 | <img src={DownloadIcon} alt="download" /> | ||
| 582 | </button> | ||
| 583 | {i === 0 && record!.scores.length > 1 ? ( | ||
| 584 | <button | ||
| 585 | onClick={() => { | ||
| 586 | ( | ||
| 587 | document.querySelectorAll( | ||
| 588 | ".profileboard-record" | ||
| 589 | )[index % 20] as HTMLInputElement | ||
| 590 | ).style.height === "44px" || | ||
| 591 | ( | ||
| 592 | document.querySelectorAll( | ||
| 593 | ".profileboard-record" | ||
| 594 | )[index % 20] as HTMLInputElement | ||
| 595 | ).style.height === "" | ||
| 596 | ? (( | ||
| 597 | document.querySelectorAll( | ||
| 598 | ".profileboard-record" | ||
| 599 | )[index % 20] as HTMLInputElement | ||
| 600 | ).style.height = | ||
| 601 | `${record!.scores.length * 46}px`) | ||
| 602 | : (( | ||
| 603 | document.querySelectorAll( | ||
| 604 | ".profileboard-record" | ||
| 605 | )[index % 20] as HTMLInputElement | ||
| 606 | ).style.height = "44px"); | ||
| 607 | }} | ||
| 608 | > | ||
| 609 | <img src={HistoryIcon} alt="history" /> | ||
| 610 | </button> | ||
| 611 | ) : ( | ||
| 612 | "" | ||
| 613 | )} | ||
| 339 | </span> | 614 | </span> |
| 340 | </>))} | 615 | </> |
| 341 | </button> | 616 | ))} |
| 342 | 617 | </button> | |
| 343 | ) | 618 | ); |
| 344 | } else { return null } | 619 | } else { |
| 345 | }) : (<>{console.warn(maps)}</>)} | 620 | return null; |
| 621 | } | ||
| 622 | }) | ||
| 623 | ) : ( | ||
| 624 | <>{console.warn(maps)}</> | ||
| 625 | )} | ||
| 346 | </div> | 626 | </div> |
| 347 | </section> | 627 | </section> |
| 348 | </main> | 628 | </main> |
diff --git a/frontend/src/pages/Rankings.tsx b/frontend/src/pages/Rankings.tsx index 71aa427..dec0e17 100644 --- a/frontend/src/pages/Rankings.tsx +++ b/frontend/src/pages/Rankings.tsx | |||
| @@ -2,146 +2,202 @@ import React, { useEffect } from "react"; | |||
| 2 | import { Helmet } from "react-helmet"; | 2 | import { Helmet } from "react-helmet"; |
| 3 | 3 | ||
| 4 | import RankingEntry from "@components/RankingEntry"; | 4 | import RankingEntry from "@components/RankingEntry"; |
| 5 | import { Ranking, SteamRanking, RankingType, SteamRankingType } from "@customTypes/Ranking"; | 5 | import { |
| 6 | Ranking, | ||
| 7 | SteamRanking, | ||
| 8 | RankingType, | ||
| 9 | SteamRankingType, | ||
| 10 | } from "@customTypes/Ranking"; | ||
| 6 | import { API } from "@api/Api"; | 11 | import { API } from "@api/Api"; |
| 7 | 12 | ||
| 8 | import "@css/Rankings.css"; | 13 | import "@css/Rankings.css"; |
| 9 | 14 | ||
| 10 | const Rankings: React.FC = () => { | 15 | enum LeaderboardTypes { |
| 11 | const [leaderboardData, setLeaderboardData] = React.useState<Ranking | SteamRanking>(); | 16 | official, |
| 12 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState<RankingType[] | SteamRankingType[]>(); | 17 | unofficial, |
| 13 | enum LeaderboardTypes { | 18 | } |
| 14 | official, | ||
| 15 | unofficial | ||
| 16 | } | ||
| 17 | const [currentRankingType, setCurrentRankingType] = React.useState<LeaderboardTypes>(LeaderboardTypes.official); | ||
| 18 | 19 | ||
| 19 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | 20 | enum RankingCategories { |
| 21 | rankings_overall, | ||
| 22 | rankings_multiplayer, | ||
| 23 | rankings_singleplayer, | ||
| 24 | } | ||
| 20 | 25 | ||
| 21 | enum RankingCategories { | 26 | const Rankings: React.FC = () => { |
| 22 | rankings_overall, | 27 | const [leaderboardData, setLeaderboardData] = React.useState< |
| 23 | rankings_multiplayer, | 28 | Ranking | SteamRanking |
| 24 | rankings_singleplayer | 29 | >(); |
| 30 | const [currentLeaderboard, setCurrentLeaderboard] = React.useState< | ||
| 31 | RankingType[] | SteamRankingType[] | ||
| 32 | >(); | ||
| 33 | const [currentRankingType, setCurrentRankingType] = | ||
| 34 | React.useState<LeaderboardTypes>(LeaderboardTypes.official); | ||
| 35 | |||
| 36 | const [leaderboardLoad, setLeaderboardLoad] = React.useState<boolean>(false); | ||
| 37 | |||
| 38 | const [currentLeaderboardType, setCurrentLeaderboardType] = | ||
| 39 | React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | ||
| 40 | const [load, setLoad] = React.useState<boolean>(false); | ||
| 41 | |||
| 42 | const _fetch_rankings = React.useCallback(async () => { | ||
| 43 | setLeaderboardLoad(false); | ||
| 44 | const rankings = await API.get_official_rankings(); | ||
| 45 | setLeaderboardData(rankings); | ||
| 46 | if (currentLeaderboardType === RankingCategories.rankings_singleplayer) { | ||
| 47 | setCurrentLeaderboard(rankings.rankings_singleplayer); | ||
| 48 | } else if ( | ||
| 49 | currentLeaderboardType === RankingCategories.rankings_multiplayer | ||
| 50 | ) { | ||
| 51 | setCurrentLeaderboard(rankings.rankings_multiplayer); | ||
| 52 | } else { | ||
| 53 | setCurrentLeaderboard(rankings.rankings_overall); | ||
| 25 | } | 54 | } |
| 26 | const [currentLeaderboardType, setCurrentLeaderboardType] = React.useState<RankingCategories>(RankingCategories.rankings_singleplayer); | 55 | setLoad(true); |
| 27 | const [load, setLoad] = React.useState<boolean>(false); | 56 | setLeaderboardLoad(true); |
| 28 | 57 | }, [currentLeaderboardType]); | |
| 29 | const _fetch_rankings = async () => { | 58 | |
| 30 | setLeaderboardLoad(false); | 59 | const __dev_fetch_unofficial_rankings = async () => { |
| 31 | const rankings = await API.get_official_rankings(); | 60 | try { |
| 32 | setLeaderboardData(rankings); | 61 | setLeaderboardLoad(false); |
| 33 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | 62 | const rankings = await API.get_unofficial_rankings(); |
| 34 | setCurrentLeaderboard(rankings.rankings_singleplayer) | 63 | setLeaderboardData(rankings); |
| 35 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | 64 | if (currentLeaderboardType === RankingCategories.rankings_singleplayer) { |
| 36 | setCurrentLeaderboard(rankings.rankings_multiplayer) | 65 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) |
| 37 | } else { | 66 | setCurrentLeaderboard(rankings.rankings_singleplayer); |
| 38 | setCurrentLeaderboard(rankings.rankings_overall) | 67 | } else if ( |
| 39 | } | 68 | currentLeaderboardType === RankingCategories.rankings_multiplayer |
| 40 | setLoad(true); | 69 | ) { |
| 41 | setLeaderboardLoad(true); | 70 | setCurrentLeaderboard(rankings.rankings_multiplayer); |
| 71 | } else { | ||
| 72 | setCurrentLeaderboard(rankings.rankings_overall); | ||
| 73 | } | ||
| 74 | setLeaderboardLoad(true); | ||
| 75 | } catch (e) { | ||
| 76 | console.log(e); | ||
| 42 | } | 77 | } |
| 43 | 78 | }; | |
| 44 | const __dev_fetch_unofficial_rankings = async () => { | 79 | |
| 45 | try { | 80 | const _set_current_leaderboard = React.useCallback( |
| 46 | setLeaderboardLoad(false); | 81 | (ranking_cat: RankingCategories) => { |
| 47 | const rankings = await API.get_unofficial_rankings(); | 82 | if (ranking_cat === RankingCategories.rankings_singleplayer) { |
| 48 | setLeaderboardData(rankings); | 83 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); |
| 49 | if (currentLeaderboardType == RankingCategories.rankings_singleplayer) { | 84 | } else if (ranking_cat === RankingCategories.rankings_multiplayer) { |
| 50 | // console.log(_sort_rankings_steam(unofficialRanking.rankings_singleplayer)) | 85 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); |
| 51 | setCurrentLeaderboard(rankings.rankings_singleplayer) | 86 | } else { |
| 52 | } else if (currentLeaderboardType == RankingCategories.rankings_multiplayer) { | 87 | setCurrentLeaderboard(leaderboardData!.rankings_overall); |
| 53 | setCurrentLeaderboard(rankings.rankings_multiplayer) | 88 | } |
| 54 | } else { | 89 | |
| 55 | setCurrentLeaderboard(rankings.rankings_overall) | 90 | setCurrentLeaderboardType(ranking_cat); |
| 91 | }, | ||
| 92 | [leaderboardData] | ||
| 93 | ); | ||
| 94 | |||
| 95 | // unused func | ||
| 96 | // const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { | ||
| 97 | // if (leaderboard_type === LeaderboardTypes.official) { | ||
| 98 | // _fetch_rankings(); | ||
| 99 | // } else { | ||
| 100 | // } | ||
| 101 | // }; | ||
| 102 | |||
| 103 | useEffect(() => { | ||
| 104 | _fetch_rankings(); | ||
| 105 | }, [_fetch_rankings]); | ||
| 106 | |||
| 107 | return ( | ||
| 108 | <main className="*:text-foreground"> | ||
| 109 | <Helmet> | ||
| 110 | <title>LPHUB | Rankings</title> | ||
| 111 | </Helmet> | ||
| 112 | <section className="nav-container nav-1"> | ||
| 113 | <div> | ||
| 114 | <button | ||
| 115 | onClick={() => { | ||
| 116 | _fetch_rankings(); | ||
| 117 | setCurrentRankingType(LeaderboardTypes.official); | ||
| 118 | }} | ||
| 119 | className={`nav-1-btn ${currentRankingType === LeaderboardTypes.official ? "selected" : ""}`} | ||
| 120 | > | ||
| 121 | <span>Official (LPHUB)</span> | ||
| 122 | </button> | ||
| 123 | <button | ||
| 124 | onClick={() => { | ||
| 125 | __dev_fetch_unofficial_rankings(); | ||
| 126 | setCurrentRankingType(LeaderboardTypes.unofficial); | ||
| 127 | }} | ||
| 128 | className={`nav-1-btn ${currentRankingType === LeaderboardTypes.unofficial ? "selected" : ""}`} | ||
| 129 | > | ||
| 130 | <span>Unofficial (Steam)</span> | ||
| 131 | </button> | ||
| 132 | </div> | ||
| 133 | </section> | ||
| 134 | <section className="nav-container nav-2"> | ||
| 135 | <div> | ||
| 136 | <button | ||
| 137 | onClick={() => | ||
| 138 | _set_current_leaderboard(RankingCategories.rankings_singleplayer) | ||
| 56 | } | 139 | } |
| 57 | setLeaderboardLoad(true); | 140 | className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_singleplayer ? "selected" : ""}`} |
| 58 | } catch (e) { | 141 | > |
| 59 | console.log(e) | 142 | <span>Singleplayer</span> |
| 60 | } | 143 | </button> |
| 61 | } | 144 | <button |
| 62 | 145 | onClick={() => | |
| 63 | const _set_current_leaderboard = (ranking_cat: RankingCategories) => { | 146 | _set_current_leaderboard(RankingCategories.rankings_multiplayer) |
| 64 | if (ranking_cat == RankingCategories.rankings_singleplayer) { | 147 | } |
| 65 | setCurrentLeaderboard(leaderboardData!.rankings_singleplayer); | 148 | className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_multiplayer ? "selected" : ""}`} |
| 66 | } else if (ranking_cat == RankingCategories.rankings_multiplayer) { | 149 | > |
| 67 | setCurrentLeaderboard(leaderboardData!.rankings_multiplayer); | 150 | <span>Cooperative</span> |
| 68 | } else { | 151 | </button> |
| 69 | setCurrentLeaderboard(leaderboardData!.rankings_overall); | 152 | <button |
| 70 | } | 153 | onClick={() => |
| 71 | 154 | _set_current_leaderboard(RankingCategories.rankings_overall) | |
| 72 | setCurrentLeaderboardType(ranking_cat); | 155 | } |
| 73 | } | 156 | className={`nav-2-btn ${currentLeaderboardType === RankingCategories.rankings_overall ? "selected" : ""}`} |
| 74 | 157 | > | |
| 75 | const _set_leaderboard_type = (leaderboard_type: LeaderboardTypes) => { | 158 | <span>Overall</span> |
| 76 | if (leaderboard_type == LeaderboardTypes.official) { | 159 | </button> |
| 77 | _fetch_rankings(); | 160 | </div> |
| 78 | } else { | 161 | </section> |
| 79 | 162 | ||
| 80 | } | 163 | {load ? ( |
| 81 | } | 164 | <section className="rankings-leaderboard"> |
| 82 | 165 | <div className="ranks-container"> | |
| 83 | useEffect(() => { | 166 | <div className="leaderboard-entry header"> |
| 84 | _fetch_rankings(); | 167 | <span>Rank</span> |
| 85 | if (load) { | 168 | <span>Player</span> |
| 86 | _set_current_leaderboard(RankingCategories.rankings_singleplayer); | 169 | <span>Portals</span> |
| 87 | } | 170 | </div> |
| 88 | }, [load]) | 171 | |
| 89 | 172 | <div className="splitter"></div> | |
| 90 | return ( | 173 | |
| 91 | <main> | 174 | {leaderboardLoad && |
| 92 | <Helmet> | 175 | currentLeaderboard?.map((curRankingData, i) => { |
| 93 | <title>LPHUB | Rankings</title> | 176 | return ( |
| 94 | </Helmet> | 177 | <RankingEntry |
| 95 | <section className="nav-container nav-1"> | 178 | currentLeaderboardType={currentLeaderboardType} |
| 96 | <div> | 179 | curRankingData={curRankingData} |
| 97 | <button onClick={() => { _fetch_rankings(); setCurrentRankingType(LeaderboardTypes.official) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.official ? "selected" : ""}`}> | 180 | key={i} |
| 98 | <span>Official (LPHUB)</span> | 181 | ></RankingEntry> |
| 99 | </button> | 182 | ); |
| 100 | <button onClick={() => { __dev_fetch_unofficial_rankings(); setCurrentRankingType(LeaderboardTypes.unofficial) }} className={`nav-1-btn ${currentRankingType == LeaderboardTypes.unofficial ? "selected" : ""}`}> | 183 | })} |
| 101 | <span>Unofficial (Steam)</span> | 184 | |
| 102 | </button> | 185 | {leaderboardLoad ? null : ( |
| 103 | </div> | 186 | <div |
| 104 | </section> | 187 | style={{ |
| 105 | <section className="nav-container nav-2"> | 188 | display: "flex", |
| 106 | <div> | 189 | justifyContent: "center", |
| 107 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_singleplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_singleplayer ? "selected" : ""}`}> | 190 | margin: "30px 0px", |
| 108 | <span>Singleplayer</span> | 191 | }} |
| 109 | </button> | 192 | > |
| 110 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_multiplayer)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_multiplayer ? "selected" : ""}`}> | 193 | <span className="loader"></span> |
| 111 | <span>Cooperative</span> | 194 | </div> |
| 112 | </button> | 195 | )} |
| 113 | <button onClick={() => _set_current_leaderboard(RankingCategories.rankings_overall)} className={`nav-2-btn ${currentLeaderboardType == RankingCategories.rankings_overall ? "selected" : ""}`}> | 196 | </div> |
| 114 | <span>Overall</span> | 197 | </section> |
| 115 | </button> | 198 | ) : null} |
| 116 | </div> | 199 | </main> |
| 117 | </section> | 200 | ); |
| 118 | 201 | }; | |
| 119 | {load ? | ||
| 120 | <section className="rankings-leaderboard"> | ||
| 121 | <div className="ranks-container"> | ||
| 122 | <div className="leaderboard-entry header"> | ||
| 123 | <span>Rank</span> | ||
| 124 | <span>Player</span> | ||
| 125 | <span>Portals</span> | ||
| 126 | </div> | ||
| 127 | |||
| 128 | <div className="splitter"></div> | ||
| 129 | |||
| 130 | {leaderboardLoad && currentLeaderboard?.map((curRankingData, i) => { | ||
| 131 | return <RankingEntry currentLeaderboardType={currentLeaderboardType} curRankingData={curRankingData} key={i}></RankingEntry> | ||
| 132 | }) | ||
| 133 | } | ||
| 134 | |||
| 135 | {leaderboardLoad ? null : | ||
| 136 | <div style={{ display: "flex", justifyContent: "center", margin: "30px 0px" }}> | ||
| 137 | <span className="loader"></span> | ||
| 138 | </div> | ||
| 139 | } | ||
| 140 | </div> | ||
| 141 | </section> | ||
| 142 | : null} | ||
| 143 | </main> | ||
| 144 | ) | ||
| 145 | } | ||
| 146 | 202 | ||
| 147 | export default Rankings; | 203 | export default Rankings; |
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx index 9f57b7e..9c7885c 100644 --- a/frontend/src/pages/Rules.tsx +++ b/frontend/src/pages/Rules.tsx | |||
| @@ -1,41 +1,37 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import ReactMarkdown from 'react-markdown'; | 2 | import ReactMarkdown from "react-markdown"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | |||
| 5 | import '@css/Rules.css'; | ||
| 6 | 4 | ||
| 7 | const Rules: React.FC = () => { | 5 | const Rules: React.FC = () => { |
| 6 | const [rulesText, setRulesText] = React.useState<string>(""); | ||
| 8 | 7 | ||
| 9 | const [rulesText, setRulesText] = React.useState<string>(""); | 8 | React.useEffect(() => { |
| 10 | 9 | const fetchRules = async () => { | |
| 11 | React.useEffect(() => { | 10 | try { |
| 12 | const fetchRules = async () => { | 11 | const response = await fetch( |
| 13 | try { | 12 | "https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md" |
| 14 | const response = await fetch( | 13 | ); |
| 15 | 'https://raw.githubusercontent.com/pektezol/lphub/main/RULES.md' | 14 | if (!response.ok) { |
| 16 | ); | 15 | throw new Error("Failed to fetch README"); |
| 17 | if (!response.ok) { | 16 | } |
| 18 | throw new Error('Failed to fetch README'); | 17 | const rulesText = await response.text(); |
| 19 | } | 18 | setRulesText(rulesText); |
| 20 | const rulesText = await response.text(); | 19 | } catch (error) { |
| 21 | setRulesText(rulesText); | 20 | console.error("Error fetching Rules:", error); |
| 22 | } catch (error) { | 21 | } |
| 23 | console.error('Error fetching Rules:', error); | 22 | // setRulesText(rulesText) |
| 24 | } | 23 | }; |
| 25 | // setRulesText(rulesText) | 24 | fetchRules(); |
| 26 | }; | 25 | }, []); |
| 27 | fetchRules(); | ||
| 28 | }, []); | ||
| 29 | |||
| 30 | 26 | ||
| 31 | return ( | 27 | return ( |
| 32 | <main> | 28 | <main className="ml-16 p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none"> |
| 33 | <Helmet> | 29 | <Helmet> |
| 34 | <title>LPHUB | Rules</title> | 30 | <title>LPHUB | Rules</title> |
| 35 | </Helmet> | 31 | </Helmet> |
| 36 | <ReactMarkdown>{rulesText}</ReactMarkdown> | 32 | <ReactMarkdown>{rulesText}</ReactMarkdown> |
| 37 | </main> | 33 | </main> |
| 38 | ); | 34 | ); |
| 39 | }; | 35 | }; |
| 40 | 36 | ||
| 41 | export default Rules; | 37 | export default Rules; |
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx index d43c0c6..8c699b1 100644 --- a/frontend/src/pages/User.tsx +++ b/frontend/src/pages/User.tsx | |||
| @@ -1,15 +1,25 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation, useNavigate } from 'react-router-dom'; | 2 | import { Link, useLocation, useNavigate } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; | 5 | import { |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | SteamIcon, |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 7 | TwitchIcon, |
| 8 | import { Map } from '@customTypes/Map'; | 8 | YouTubeIcon, |
| 9 | import { API } from '@api/Api'; | 9 | PortalIcon, |
| 10 | import { ticks_to_time } from '@utils/Time'; | 10 | FlagIcon, |
| 11 | import "@css/Profile.css"; | 11 | StatisticsIcon, |
| 12 | import useMessage from '@hooks/UseMessage'; | 12 | SortIcon, |
| 13 | ThreedotIcon, | ||
| 14 | DownloadIcon, | ||
| 15 | HistoryIcon, | ||
| 16 | } from "@images/Images"; | ||
| 17 | import { UserProfile } from "@customTypes/Profile"; | ||
| 18 | import { Game, GameChapters } from "@customTypes/Game"; | ||
| 19 | import { Map } from "@customTypes/Map"; | ||
| 20 | import { API } from "@api/Api"; | ||
| 21 | import { ticks_to_time } from "@utils/Time"; | ||
| 22 | import useMessage from "@hooks/UseMessage"; | ||
| 13 | 23 | ||
| 14 | interface UserProps { | 24 | interface UserProps { |
| 15 | profile?: UserProfile; | 25 | profile?: UserProfile; |
| @@ -18,7 +28,6 @@ interface UserProps { | |||
| 18 | } | 28 | } |
| 19 | 29 | ||
| 20 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | 30 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { |
| 21 | |||
| 22 | const { message, MessageDialogComponent } = useMessage(); | 31 | const { message, MessageDialogComponent } = useMessage(); |
| 23 | 32 | ||
| 24 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); | 33 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); |
| @@ -29,13 +38,15 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 29 | 38 | ||
| 30 | const [game, setGame] = React.useState("0"); | 39 | const [game, setGame] = React.useState("0"); |
| 31 | const [chapter, setChapter] = React.useState("0"); | 40 | const [chapter, setChapter] = React.useState("0"); |
| 32 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | 41 | const [chapterData, setChapterData] = React.useState<GameChapters | null>( |
| 42 | null | ||
| 43 | ); | ||
| 33 | const [maps, setMaps] = React.useState<Map[]>([]); | 44 | const [maps, setMaps] = React.useState<Map[]>([]); |
| 34 | 45 | ||
| 35 | const location = useLocation(); | 46 | const location = useLocation(); |
| 36 | const navigate = useNavigate(); | 47 | const navigate = useNavigate(); |
| 37 | 48 | ||
| 38 | const _fetch_user = async () => { | 49 | const _fetch_user = React.useCallback(async () => { |
| 39 | const userID = location.pathname.split("/")[2]; | 50 | const userID = location.pathname.split("/")[2]; |
| 40 | if (token && profile && profile.profile && profile.steam_id === userID) { | 51 | if (token && profile && profile.profile && profile.steam_id === userID) { |
| 41 | navigate("/profile"); | 52 | navigate("/profile"); |
| @@ -43,9 +54,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 43 | } | 54 | } |
| 44 | const userData = await API.get_user(userID); | 55 | const userData = await API.get_user(userID); |
| 45 | setUser(userData); | 56 | setUser(userData); |
| 46 | }; | 57 | }, [location.pathname, token, profile, navigate]); |
| 47 | 58 | ||
| 48 | const _get_game_chapters = async () => { | 59 | const _get_game_chapters = React.useCallback(async () => { |
| 49 | if (game !== "0") { | 60 | if (game !== "0") { |
| 50 | const gameChapters = await API.get_games_chapters(game); | 61 | const gameChapters = await API.get_games_chapters(game); |
| 51 | setChapterData(gameChapters); | 62 | setChapterData(gameChapters); |
| @@ -53,9 +64,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 53 | setPageMax(Math.ceil(user!.records.length / 20)); | 64 | setPageMax(Math.ceil(user!.records.length / 20)); |
| 54 | setPageNumber(1); | 65 | setPageNumber(1); |
| 55 | } | 66 | } |
| 56 | }; | 67 | }, [game, user]); |
| 57 | 68 | ||
| 58 | const _get_game_maps = async () => { | 69 | const _get_game_maps = React.useCallback(async () => { |
| 59 | if (chapter === "0") { | 70 | if (chapter === "0") { |
| 60 | const gameMaps = await API.get_game_maps(game); | 71 | const gameMaps = await API.get_game_maps(game); |
| 61 | setMaps(gameMaps); | 72 | setMaps(gameMaps); |
| @@ -67,251 +78,331 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 67 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); | 78 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); |
| 68 | setPageNumber(1); | 79 | setPageNumber(1); |
| 69 | } | 80 | } |
| 70 | }; | 81 | }, [chapter, game]); |
| 71 | 82 | ||
| 72 | React.useEffect(() => { | 83 | React.useEffect(() => { |
| 73 | _fetch_user(); | 84 | _fetch_user(); |
| 74 | }, [location]); | 85 | }, [location, _fetch_user]); |
| 75 | 86 | ||
| 76 | React.useEffect(() => { | 87 | React.useEffect(() => { |
| 77 | if (user) { | 88 | if (user) { |
| 78 | _get_game_chapters(); | 89 | _get_game_chapters(); |
| 79 | } | 90 | } |
| 80 | }, [user, game, location]); | 91 | }, [user, game, location, _get_game_chapters]); |
| 81 | 92 | ||
| 82 | React.useEffect(() => { | 93 | React.useEffect(() => { |
| 83 | if (user && game !== "0") { | 94 | if (user && game !== "0") { |
| 84 | _get_game_maps(); | 95 | _get_game_maps(); |
| 85 | } | 96 | } |
| 86 | }, [user, game, chapter, location]) | 97 | }, [user, game, chapter, location, _get_game_maps]); |
| 87 | 98 | ||
| 88 | if (!user) { | 99 | if (!user) { |
| 89 | return ( | 100 | return ( |
| 90 | <></> | 101 | <div className="flex justify-center items-center h-[50vh] text-lg text-foreground"> |
| 102 | Loading... | ||
| 103 | </div> | ||
| 91 | ); | 104 | ); |
| 92 | }; | 105 | } |
| 93 | 106 | ||
| 94 | return ( | 107 | return ( |
| 95 | <main> | 108 | <main className="ml-20 overflow-auto overflow-x-hidden relative w-[calc(100%px)] h-screen font-[--font-barlow-semicondensed-regular] text-foreground text-xl"> |
| 96 | <Helmet> | 109 | <Helmet> |
| 97 | <title>LPHUB | {user.user_name}</title> | 110 | <title>LPHUB | {user.user_name}</title> |
| 98 | <meta name="description" content={user.user_name} /> | 111 | <meta name="description" content={user.user_name} /> |
| 99 | </Helmet> | 112 | </Helmet> |
| 113 | |||
| 100 | {MessageDialogComponent} | 114 | {MessageDialogComponent} |
| 101 | <section id='section1' className='profile'> | 115 | |
| 102 | <div> | 116 | <section className="m-5 bg-gradient-to-t from-[#202232] from-50% to-[#2b2e46] to-50% rounded-3xl p-[30px] mb-[30px] text-foreground"> |
| 103 | <img src={user.avatar_link} alt="profile-image"></img> | 117 | <div className="grid grid-cols-[200px_1fr_auto] items-center gap-[25px] mb-[25px]"> |
| 104 | </div> | 118 | <img |
| 105 | <div id='profile-top'> | 119 | src={user.avatar_link} |
| 120 | alt="Profile" | ||
| 121 | className="w-[120px] h-[120px] rounded-full border-[3px] border-[rgba(205,207,223,0.2)]" | ||
| 122 | /> | ||
| 106 | <div> | 123 | <div> |
| 107 | <div>{user.user_name}</div> | 124 | <h1 className="m-0 mb-[10px] text-[50px] font-bold text-white font-[--font-barlow-semicondensed-regular]"> |
| 108 | <div> | 125 | {user.user_name} |
| 109 | {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} | 126 | </h1> |
| 110 | </div> | 127 | {user.country_code !== "XX" && ( |
| 111 | <div> | 128 | <div className="flex items-center gap-3 mb-[15px]"> |
| 112 | {user.titles.map(e => ( | 129 | <img |
| 113 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | 130 | src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} |
| 114 | {e.name} | 131 | alt={user.country_code} |
| 132 | className="w-6 h-4 rounded-[10px]" | ||
| 133 | /> | ||
| 134 | <span>{user.country_code}</span> | ||
| 135 | </div> | ||
| 136 | )} | ||
| 137 | <div className="flex flex-wrap gap-2"> | ||
| 138 | {user.titles.map((title, index) => ( | ||
| 139 | <span | ||
| 140 | key={index} | ||
| 141 | className="py-[6px] px-5 pt-[6px] rounded-[10px] text-lg font-normal text-white" | ||
| 142 | style={{ backgroundColor: `#${title.color}` }} | ||
| 143 | > | ||
| 144 | {title.name} | ||
| 115 | </span> | 145 | </span> |
| 116 | ))} | 146 | ))} |
| 117 | </div> | 147 | </div> |
| 118 | </div> | 148 | </div> |
| 119 | <div> | 149 | <div className="flex gap-[15px] items-center pr-[10px]"> |
| 120 | {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | 150 | {user.links.steam !== "-" && ( |
| 121 | {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | 151 | <a href={user.links.steam} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> |
| 122 | {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | 152 | <img src={SteamIcon} alt="Steam" className="h-[50px] px-[5px] scale-90 brightness-200" /> |
| 123 | {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | 153 | </a> |
| 154 | )} | ||
| 155 | {user.links.twitch !== "-" && ( | ||
| 156 | <a href={user.links.twitch} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 157 | <img src={TwitchIcon} alt="Twitch" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 158 | </a> | ||
| 159 | )} | ||
| 160 | {user.links.youtube !== "-" && ( | ||
| 161 | <a href={user.links.youtube} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 162 | <img src={YouTubeIcon} alt="YouTube" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 163 | </a> | ||
| 164 | )} | ||
| 165 | {user.links.p2sr !== "-" && ( | ||
| 166 | <a href={user.links.p2sr} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 167 | <img src={PortalIcon} alt="P2SR" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 168 | </a> | ||
| 169 | )} | ||
| 124 | </div> | 170 | </div> |
| 125 | |||
| 126 | </div> | 171 | </div> |
| 127 | <div id='profile-bottom'> | 172 | |
| 128 | <div> | 173 | <div className="grid grid-cols-3 gap-3 mt-24"> |
| 129 | <span>Overall</span> | 174 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 130 | <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} | 175 | <div className="text-inherit text-lg">Overall</div> |
| 131 | <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> | 176 | <div className="text-white text-[40px]"> |
| 132 | </span> | 177 | {user.rankings.overall.rank === 0 ? "N/A" : `#${user.rankings.overall.rank}`} |
| 178 | </div> | ||
| 179 | <div className="text-white text-xl"> | ||
| 180 | {user.rankings.overall.completion_count}/{user.rankings.overall.completion_total} | ||
| 181 | </div> | ||
| 133 | </div> | 182 | </div> |
| 134 | <div> | 183 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 135 | <span>Singleplayer</span> | 184 | <div className="text-inherit text-lg">Singleplayer</div> |
| 136 | <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} | 185 | <div className="text-white text-[40px]"> |
| 137 | <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> | 186 | {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`} |
| 138 | </span> | 187 | </div> |
| 188 | <div className="text-white text-xl"> | ||
| 189 | {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total} | ||
| 190 | </div> | ||
| 139 | </div> | 191 | </div> |
| 140 | <div> | 192 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 141 | <span>Cooperative</span> | 193 | <div className="text-inherit text-lg">Cooperative</div> |
| 142 | <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} | 194 | <div className="text-white text-[40px]"> |
| 143 | <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> | 195 | {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`} |
| 144 | </span> | 196 | </div> |
| 197 | <div className="text-white text-xl"> | ||
| 198 | {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total} | ||
| 199 | </div> | ||
| 145 | </div> | 200 | </div> |
| 146 | </div> | 201 | </div> |
| 147 | </section> | 202 | </section> |
| 148 | 203 | ||
| 149 | 204 | <section className="m-5 h-[60px] grid grid-cols-2"> | |
| 150 | <section id='section2' className='profile'> | 205 | <button |
| 151 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | 206 | className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-l-3xl hover:bg-[#202232] ${ |
| 152 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | 207 | navState === 0 ? 'bg-[#202232]' : '' |
| 208 | }`} | ||
| 209 | onClick={() => setNavState(0)} | ||
| 210 | > | ||
| 211 | <img src={FlagIcon} alt="" className="w-5 h-5 scale-[1.2]" /> | ||
| 212 | Player Records | ||
| 213 | </button> | ||
| 214 | <button | ||
| 215 | className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-r-3xl hover:bg-[#202232] ${ | ||
| 216 | navState === 1 ? 'bg-[#202232]' : '' | ||
| 217 | }`} | ||
| 218 | onClick={() => setNavState(1)} | ||
| 219 | > | ||
| 220 | <img src={StatisticsIcon} alt="" className="w-5 h-5 scale-[1.2]" /> | ||
| 221 | Statistics | ||
| 222 | </button> | ||
| 153 | </section> | 223 | </section> |
| 154 | 224 | ||
| 155 | 225 | {navState === 0 && ( | |
| 156 | 226 | <section className="m-5 block bg-[#202232] rounded-3xl overflow-hidden"> | |
| 157 | 227 | <div className="grid grid-cols-2 mx-5 my-5 mt-[10px] mb-5"> | |
| 158 | 228 | <select | |
| 159 | <section id='section3' className='profile1'> | 229 | className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px]" |
| 160 | <div id='profileboard-nav'> | 230 | value={game} |
| 161 | {gameData === null ? <select>error</select> : | 231 | onChange={(e) => { |
| 162 | 232 | setGame(e.target.value); | |
| 163 | <select id='select-game' | ||
| 164 | onChange={() => { | ||
| 165 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | ||
| 166 | setChapter("0"); | 233 | setChapter("0"); |
| 167 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 234 | }} |
| 168 | if (chapterSelect) { | 235 | > |
| 169 | chapterSelect.value = "0"; | 236 | <option value="0">All Games</option> |
| 170 | } | 237 | {gameData?.map((g) => ( |
| 171 | }}> | 238 | <option key={g.id} value={g.id}> |
| 172 | <option value={0} key={0}>All Scores</option> | 239 | {g.name} |
| 173 | {gameData.map((e, i) => ( | 240 | </option> |
| 174 | <option value={e.id} key={i + 1}>{e.name}</option> | 241 | ))} |
| 175 | ))}</select> | 242 | </select> |
| 176 | } | ||
| 177 | 243 | ||
| 178 | {game === "0" ? | 244 | <select |
| 179 | <select disabled> | 245 | className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px] disabled:opacity-50" |
| 180 | <option>All Chapters</option> | 246 | value={chapter} |
| 247 | onChange={(e) => setChapter(e.target.value)} | ||
| 248 | disabled={game === "0"} | ||
| 249 | > | ||
| 250 | <option value="0">All Chapters</option> | ||
| 251 | {chapterData?.chapters | ||
| 252 | .filter(c => !c.is_disabled) | ||
| 253 | .map((c) => ( | ||
| 254 | <option key={c.id} value={c.id}> | ||
| 255 | {c.name} | ||
| 256 | </option> | ||
| 257 | ))} | ||
| 181 | </select> | 258 | </select> |
| 182 | : chapterData === null ? <select></select> : | 259 | </div> |
| 183 | 260 | ||
| 184 | <select id='select-chapter' | 261 | <div className="h-[34px] grid text-xl pl-[60px] mx-5 my-0 grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%]"> |
| 185 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 262 | <div className="flex place-items-end cursor-pointer"> |
| 186 | <option value="0" key="0">All Chapters</option> | 263 | <span>Map Name</span> |
| 187 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 264 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 188 | <option value={e.id} key={i + 1}>{e.name}</option> | 265 | </div> |
| 189 | ))}</select> | 266 | <div className="flex place-items-end cursor-pointer"> |
| 190 | } | 267 | <span>Portals</span> |
| 191 | </div> | 268 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 192 | <div id='profileboard-top'> | 269 | </div> |
| 193 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 270 | <div className="flex place-items-end cursor-pointer"> |
| 194 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 271 | <span>WRΔ</span> |
| 195 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 272 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 196 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 273 | </div> |
| 197 | <span> </span> | 274 | <div className="flex place-items-end cursor-pointer"> |
| 198 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 275 | <span>Time</span> |
| 199 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 276 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 200 | <div id='page-number'> | 277 | </div> |
| 201 | <div> | 278 | <div></div> |
| 202 | <button onClick={() => { | 279 | <div className="flex place-items-end cursor-pointer"> |
| 203 | if (pageNumber !== 1) { | 280 | <span>Rank</span> |
| 204 | setPageNumber(prevPageNumber => prevPageNumber - 1); | 281 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 205 | const records = document.querySelectorAll(".profileboard-record"); | 282 | </div> |
| 206 | records.forEach((r) => { | 283 | <div className="flex place-items-end cursor-pointer"> |
| 207 | (r as HTMLInputElement).style.height = "44px"; | 284 | <span>Date</span> |
| 208 | }); | 285 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 209 | } | 286 | </div> |
| 210 | }} | 287 | <div className="flex items-center gap-[10px] justify-center"> |
| 211 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 288 | <button |
| 212 | <span>{pageNumber}/{pageMax}</span> | 289 | className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed" |
| 213 | <button onClick={() => { | 290 | onClick={() => setPageNumber(Math.max(1, pageNumber - 1))} |
| 214 | if (pageNumber !== pageMax) { | 291 | disabled={pageNumber === 1} |
| 215 | setPageNumber(prevPageNumber => prevPageNumber + 1); | 292 | > |
| 216 | const records = document.querySelectorAll(".profileboard-record"); | 293 | ← |
| 217 | records.forEach((r) => { | 294 | </button> |
| 218 | (r as HTMLInputElement).style.height = "44px"; | 295 | <span className="text-sm text-foreground">{pageNumber}/{pageMax}</span> |
| 219 | }); | 296 | <button |
| 220 | } | 297 | className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed" |
| 221 | }} | 298 | onClick={() => setPageNumber(Math.min(pageMax, pageNumber + 1))} |
| 222 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 299 | disabled={pageNumber === pageMax} |
| 300 | > | ||
| 301 | → | ||
| 302 | </button> | ||
| 223 | </div> | 303 | </div> |
| 224 | </div> | 304 | </div> |
| 225 | </div> | ||
| 226 | <hr /> | ||
| 227 | <div id='profileboard-records'> | ||
| 228 | |||
| 229 | {game === "0" | ||
| 230 | ? ( | ||
| 231 | |||
| 232 | user.records.sort((a, b) => a.map_id - b.map_id) | ||
| 233 | .map((r, index) => ( | ||
| 234 | 305 | ||
| 306 | <div> | ||
| 307 | {game === "0" ? ( | ||
| 308 | user.records | ||
| 309 | .sort((a, b) => a.map_id - b.map_id) | ||
| 310 | .map((record, index) => | ||
| 235 | Math.ceil((index + 1) / 20) === pageNumber ? ( | 311 | Math.ceil((index + 1) / 20) === pageNumber ? ( |
| 236 | <button className="profileboard-record" key={index}> | 312 | <div key={index} className="w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232]"> |
| 237 | {r.scores.map((e, i) => (<> | 313 | <Link to={`/maps/${record.map_id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline"> |
| 238 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 314 | {record.map_name} |
| 239 | 315 | </Link> | |
| 240 | <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> | 316 | <span className="flex place-items-center h-11">{record.scores[0]?.score_count || 'N/A'}</span> |
| 241 | 317 | <span className={`flex place-items-center h-11 ${record.scores[0]?.score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}> | |
| 242 | <span style={{ display: "grid" }}>{e.score_count}</span> | 318 | {record.scores[0]?.score_count - record.map_wr_count > 0 |
| 243 | 319 | ? `+${record.scores[0].score_count - record.map_wr_count}` | |
| 244 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 320 | : '–'} |
| 245 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 321 | </span> |
| 246 | <span> </span> | 322 | <span className="flex place-items-center h-11">{record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span> |
| 247 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 323 | <span className="flex place-items-center h-11"></span> |
| 248 | <span>{e.date.split("T")[0]}</span> | 324 | <span className="flex place-items-center h-11 font-semibold">#{record.placement}</span> |
| 249 | <span style={{ flexDirection: "row-reverse" }}> | 325 | <span className="flex place-items-center h-11">{record.scores[0]?.date.split("T")[0] || 'N/A'}</span> |
| 250 | 326 | <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11"> | |
| 251 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 327 | <button |
| 252 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 328 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 253 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | 329 | onClick={() => message("Demo Information", `Demo ID: ${record.scores[0]?.demo_id}`)} |
| 254 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 330 | title="Demo Info" |
| 255 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 331 | > |
| 256 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | 332 | <img src={ThreedotIcon} alt="Info" className="w-4 h-4" /> |
| 257 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 333 | </button> |
| 258 | } | 334 | <button |
| 259 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 335 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 260 | 336 | onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0]?.demo_id}`} | |
| 261 | </span> | 337 | title="Download Demo" |
| 262 | </>))} | 338 | > |
| 263 | 339 | <img src={DownloadIcon} alt="Download" className="w-4 h-4" /> | |
| 264 | </button> | 340 | </button> |
| 265 | ) : "" | 341 | {record.scores.length > 1 && ( |
| 266 | ))) : maps ? | 342 | <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History"> |
| 267 | 343 | <img src={HistoryIcon} alt="History" className="w-4 h-4" /> | |
| 268 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 344 | </button> |
| 269 | .map((r, index) => { | 345 | )} |
| 270 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 346 | </div> |
| 271 | let record = user.records.find((e) => e.map_id === r.id); | 347 | </div> |
| 272 | return record === undefined ? ( | 348 | ) : null |
| 273 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 349 | ) |
| 274 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 350 | ) : ( |
| 275 | <span style={{ display: "grid" }}>N/A</span> | 351 | maps |
| 276 | <span style={{ display: "grid" }}>N/A</span> | 352 | ?.filter(map => !map.is_disabled) |
| 277 | <span>N/A</span> | 353 | .sort((a, b) => a.id - b.id) |
| 278 | <span> </span> | 354 | .map((map, index) => { |
| 279 | <span>N/A</span> | 355 | if (Math.ceil((index + 1) / 20) !== pageNumber) return null; |
| 280 | <span>N/A</span> | 356 | |
| 281 | <span style={{ flexDirection: "row-reverse" }}></span> | 357 | const record = user.records.find(r => r.map_id === map.id); |
| 282 | </button> | 358 | |
| 283 | ) : ( | 359 | return ( |
| 284 | <button className="profileboard-record" key={index}> | 360 | <div key={index} className={`w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232] ${!record ? 'opacity-65' : ''}`}> |
| 285 | {record.scores.map((e, i) => (<> | 361 | <Link to={`/maps/${map.id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline"> |
| 286 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 362 | {map.name} |
| 287 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 363 | </Link> |
| 288 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 364 | <span className="flex place-items-center h-11">{record?.scores[0]?.score_count || 'N/A'}</span> |
| 289 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> | 365 | <span className={`flex place-items-center h-11 ${record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}> |
| 290 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | 366 | {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 |
| 291 | <span> </span> | 367 | ? `+${record.scores[0].score_count - record.map_wr_count}` |
| 292 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 368 | : '–'} |
| 293 | <span>{record!.scores[i].date.split("T")[0]}</span> | 369 | </span> |
| 294 | <span style={{ flexDirection: "row-reverse" }}> | 370 | <span className="flex place-items-center h-11">{record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span> |
| 295 | 371 | <span className="flex place-items-center h-11"></span> | |
| 296 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 372 | <span className="flex place-items-center h-11 font-semibold">{record ? `#${record.placement}` : 'N/A'}</span> |
| 297 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 373 | <span className="flex place-items-center h-11">{record?.scores[0]?.date.split("T")[0] || 'N/A'}</span> |
| 298 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | 374 | <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11"> |
| 299 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 375 | {record?.scores[0] && ( |
| 300 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 376 | <> |
| 301 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | 377 | <button |
| 302 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 378 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 303 | } | 379 | onClick={() => message("Demo Information", `Demo ID: ${record.scores[0].demo_id}`)} |
| 304 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 380 | title="Demo Info" |
| 305 | 381 | > | |
| 306 | </span> | 382 | <img src={ThreedotIcon} alt="Info" className="w-4 h-4" /> |
| 307 | </>))} | 383 | </button> |
| 308 | </button> | 384 | <button |
| 309 | 385 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" | |
| 310 | ) | 386 | onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0].demo_id}`} |
| 311 | } else { return null } | 387 | title="Download Demo" |
| 312 | }) : (<>{console.warn(maps)}</>)} | 388 | > |
| 313 | </div> | 389 | <img src={DownloadIcon} alt="Download" className="w-4 h-4" /> |
| 314 | </section> | 390 | </button> |
| 391 | {record.scores.length > 1 && ( | ||
| 392 | <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History"> | ||
| 393 | <img src={HistoryIcon} alt="History" className="w-4 h-4" /> | ||
| 394 | </button> | ||
| 395 | )} | ||
| 396 | </> | ||
| 397 | )} | ||
| 398 | </div> | ||
| 399 | </div> | ||
| 400 | ); | ||
| 401 | }) | ||
| 402 | )} | ||
| 403 | </div> | ||
| 404 | </section> | ||
| 405 | )} | ||
| 315 | </main> | 406 | </main> |
| 316 | ); | 407 | ); |
| 317 | }; | 408 | }; |
diff --git a/frontend/src/types/Chapters.ts b/frontend/src/types/Chapters.ts index 1d48306..5b494ca 100644 --- a/frontend/src/types/Chapters.ts +++ b/frontend/src/types/Chapters.ts | |||
| @@ -2,18 +2,18 @@ import type { Game } from "@customTypes/Game"; | |||
| 2 | import type { Map } from "@customTypes/Map"; | 2 | import type { Map } from "@customTypes/Map"; |
| 3 | 3 | ||
| 4 | interface Chapter { | 4 | interface Chapter { |
| 5 | id: number; | 5 | id: number; |
| 6 | name: string; | 6 | name: string; |
| 7 | image: string; | 7 | image: string; |
| 8 | is_disabled: boolean; | 8 | is_disabled: boolean; |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | export interface GameChapter { | 11 | export interface GameChapter { |
| 12 | chapter: Chapter; | 12 | chapter: Chapter; |
| 13 | maps: Map[]; | 13 | maps: Map[]; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | export interface GamesChapters { | 16 | export interface GamesChapters { |
| 17 | game: Game; | 17 | game: Game; |
| 18 | chapters: Chapter[]; | 18 | chapters: Chapter[]; |
| 19 | } \ No newline at end of file | 19 | } |
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts index 775fab4..77b3970 100644 --- a/frontend/src/types/Content.ts +++ b/frontend/src/types/Content.ts | |||
| @@ -6,18 +6,18 @@ export interface ModMenuContent { | |||
| 6 | showcase: string; | 6 | showcase: string; |
| 7 | description: string; | 7 | description: string; |
| 8 | category_id: number; | 8 | category_id: number; |
| 9 | }; | 9 | } |
| 10 | 10 | ||
| 11 | export interface MapDiscussionContent { | 11 | export interface MapDiscussionContent { |
| 12 | title: string; | 12 | title: string; |
| 13 | content: string; | 13 | content: string; |
| 14 | }; | 14 | } |
| 15 | 15 | ||
| 16 | export interface MapDiscussionCommentContent { | 16 | export interface MapDiscussionCommentContent { |
| 17 | comment: string; | 17 | comment: string; |
| 18 | }; | 18 | } |
| 19 | 19 | ||
| 20 | export interface UploadRunContent { | 20 | export interface UploadRunContent { |
| 21 | host_demo: File | null; | 21 | host_demo: File | null; |
| 22 | partner_demo: File | null; | 22 | partner_demo: File | null; |
| 23 | }; | 23 | } |
diff --git a/frontend/src/types/Game.ts b/frontend/src/types/Game.ts index 1a80341..0e7dc80 100644 --- a/frontend/src/types/Game.ts +++ b/frontend/src/types/Game.ts | |||
| @@ -1,5 +1,4 @@ | |||
| 1 | import type { Map } from '@customTypes/Map'; | 1 | import type { Map } from "@customTypes/Map"; |
| 2 | |||
| 3 | 2 | ||
| 4 | export interface Game { | 3 | export interface Game { |
| 5 | id: number; | 4 | id: number; |
| @@ -7,31 +6,31 @@ export interface Game { | |||
| 7 | image: string; | 6 | image: string; |
| 8 | is_coop: boolean; | 7 | is_coop: boolean; |
| 9 | category_portals: GameCategoryPortals[]; | 8 | category_portals: GameCategoryPortals[]; |
| 10 | }; | 9 | } |
| 11 | 10 | ||
| 12 | export interface GameChapters { | 11 | export interface GameChapters { |
| 13 | game: Game; | 12 | game: Game; |
| 14 | chapters: Chapter[]; | 13 | chapters: Chapter[]; |
| 15 | }; | 14 | } |
| 16 | 15 | ||
| 17 | export interface GameMaps { | 16 | export interface GameMaps { |
| 18 | game: Game; | 17 | game: Game; |
| 19 | maps: Map[]; | 18 | maps: Map[]; |
| 20 | }; | 19 | } |
| 21 | 20 | ||
| 22 | export interface Category { | 21 | export interface Category { |
| 23 | id: number; | 22 | id: number; |
| 24 | name: string; | 23 | name: string; |
| 25 | }; | 24 | } |
| 26 | 25 | ||
| 27 | interface Chapter { | 26 | interface Chapter { |
| 28 | id: number; | 27 | id: number; |
| 29 | name: string; | 28 | name: string; |
| 30 | image: string; | 29 | image: string; |
| 31 | is_disabled: boolean; | 30 | is_disabled: boolean; |
| 32 | }; | 31 | } |
| 33 | 32 | ||
| 34 | export interface GameCategoryPortals { | 33 | export interface GameCategoryPortals { |
| 35 | category: Category; | 34 | category: Category; |
| 36 | portal_count: number; | 35 | portal_count: number; |
| 37 | }; | 36 | } |
diff --git a/frontend/src/types/Map.ts b/frontend/src/types/Map.ts index 4f8eabf..a5c9404 100644 --- a/frontend/src/types/Map.ts +++ b/frontend/src/types/Map.ts | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | import type { Category, GameCategoryPortals } from '@customTypes/Game'; | 1 | import type { Category, GameCategoryPortals } from "@customTypes/Game"; |
| 2 | import type { Pagination } from '@customTypes/Pagination'; | 2 | import type { Pagination } from "@customTypes/Pagination"; |
| 3 | import type { UserShort } from '@customTypes/Profile'; | 3 | import type { UserShort } from "@customTypes/Profile"; |
| 4 | 4 | ||
| 5 | export interface Map { | 5 | export interface Map { |
| 6 | id: number; | 6 | id: number; |
| @@ -9,15 +9,15 @@ export interface Map { | |||
| 9 | is_disabled: boolean; | 9 | is_disabled: boolean; |
| 10 | difficulty: number; | 10 | difficulty: number; |
| 11 | category_portals: GameCategoryPortals[]; | 11 | category_portals: GameCategoryPortals[]; |
| 12 | }; | 12 | } |
| 13 | 13 | ||
| 14 | export interface MapDiscussion { | 14 | export interface MapDiscussion { |
| 15 | discussion: MapDiscussionsDetail; | 15 | discussion: MapDiscussionsDetail; |
| 16 | }; | 16 | } |
| 17 | 17 | ||
| 18 | export interface MapDiscussions { | 18 | export interface MapDiscussions { |
| 19 | discussions: MapDiscussionsDetail[]; | 19 | discussions: MapDiscussionsDetail[]; |
| 20 | }; | 20 | } |
| 21 | 21 | ||
| 22 | export interface MapDiscussionsDetail { | 22 | export interface MapDiscussionsDetail { |
| 23 | id: number; | 23 | id: number; |
| @@ -27,19 +27,21 @@ export interface MapDiscussionsDetail { | |||
| 27 | comments: MapDiscussionDetailComment[]; | 27 | comments: MapDiscussionDetailComment[]; |
| 28 | created_at: string; | 28 | created_at: string; |
| 29 | updated_at: string; | 29 | updated_at: string; |
| 30 | }; | 30 | } |
| 31 | 31 | ||
| 32 | interface MapDiscussionDetailComment { | 32 | interface MapDiscussionDetailComment { |
| 33 | comment: string; | 33 | comment: string; |
| 34 | date: string; | 34 | date: string; |
| 35 | user: UserShort; | 35 | user: UserShort; |
| 36 | }; | 36 | } |
| 37 | 37 | ||
| 38 | export interface MapLeaderboard { | 38 | export interface MapLeaderboard { |
| 39 | map: MapSummaryMap; | 39 | map: MapSummaryMap; |
| 40 | records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[]; | 40 | records: |
| 41 | | MapLeaderboardRecordSingleplayer[] | ||
| 42 | | MapLeaderboardRecordMultiplayer[]; | ||
| 41 | pagination: Pagination; | 43 | pagination: Pagination; |
| 42 | }; | 44 | } |
| 43 | 45 | ||
| 44 | export interface MapLeaderboardRecordSingleplayer { | 46 | export interface MapLeaderboardRecordSingleplayer { |
| 45 | kind: "singleplayer"; | 47 | kind: "singleplayer"; |
| @@ -50,7 +52,7 @@ export interface MapLeaderboardRecordSingleplayer { | |||
| 50 | user: UserShort; | 52 | user: UserShort; |
| 51 | demo_id: string; | 53 | demo_id: string; |
| 52 | record_date: string; | 54 | record_date: string; |
| 53 | }; | 55 | } |
| 54 | 56 | ||
| 55 | export interface MapLeaderboardRecordMultiplayer { | 57 | export interface MapLeaderboardRecordMultiplayer { |
| 56 | kind: "multiplayer"; | 58 | kind: "multiplayer"; |
| @@ -63,13 +65,12 @@ export interface MapLeaderboardRecordMultiplayer { | |||
| 63 | host_demo_id: string; | 65 | host_demo_id: string; |
| 64 | partner_demo_id: string; | 66 | partner_demo_id: string; |
| 65 | record_date: string; | 67 | record_date: string; |
| 66 | }; | 68 | } |
| 67 | |||
| 68 | 69 | ||
| 69 | export interface MapSummary { | 70 | export interface MapSummary { |
| 70 | map: MapSummaryMap; | 71 | map: MapSummaryMap; |
| 71 | summary: MapSummaryDetails; | 72 | summary: MapSummaryDetails; |
| 72 | }; | 73 | } |
| 73 | 74 | ||
| 74 | interface MapSummaryMap { | 75 | interface MapSummaryMap { |
| 75 | id: number; | 76 | id: number; |
| @@ -84,7 +85,7 @@ interface MapSummaryMap { | |||
| 84 | 85 | ||
| 85 | interface MapSummaryDetails { | 86 | interface MapSummaryDetails { |
| 86 | routes: MapSummaryDetailsRoute[]; | 87 | routes: MapSummaryDetailsRoute[]; |
| 87 | }; | 88 | } |
| 88 | 89 | ||
| 89 | interface MapSummaryDetailsRoute { | 90 | interface MapSummaryDetailsRoute { |
| 90 | route_id: number; | 91 | route_id: number; |
| @@ -94,16 +95,15 @@ interface MapSummaryDetailsRoute { | |||
| 94 | completion_count: number; | 95 | completion_count: number; |
| 95 | description: string; | 96 | description: string; |
| 96 | showcase: string; | 97 | showcase: string; |
| 97 | }; | 98 | } |
| 98 | 99 | ||
| 99 | interface MapSummaryDetailsRouteHistory { | 100 | interface MapSummaryDetailsRouteHistory { |
| 100 | runner_name: string; | 101 | runner_name: string; |
| 101 | score_count: number; | 102 | score_count: number; |
| 102 | date: string; | 103 | date: string; |
| 103 | }; | 104 | } |
| 104 | 105 | ||
| 105 | export interface MapDeleteEndpoint { | 106 | export interface MapDeleteEndpoint { |
| 106 | map_id: number; | 107 | map_id: number; |
| 107 | record_id: number; | 108 | record_id: number; |
| 108 | } | 109 | } |
| 109 | |||
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts index b6313e7..9ea9851 100644 --- a/frontend/src/types/MapNames.ts +++ b/frontend/src/types/MapNames.ts | |||
| @@ -1,127 +1,127 @@ | |||
| 1 | export const MapNames: { [key: string]: number } = { | 1 | export const MapNames: { [key: string]: number } = { |
| 2 | "sp_a1_intro1": 1, | 2 | sp_a1_intro1: 1, |
| 3 | "sp_a1_intro2": 2, | 3 | sp_a1_intro2: 2, |
| 4 | "sp_a1_intro3": 3, | 4 | sp_a1_intro3: 3, |
| 5 | "sp_a1_intro4": 4, | 5 | sp_a1_intro4: 4, |
| 6 | "sp_a1_intro5": 5, | 6 | sp_a1_intro5: 5, |
| 7 | "sp_a1_intro6": 6, | 7 | sp_a1_intro6: 6, |
| 8 | "sp_a1_intro7": 7, | 8 | sp_a1_intro7: 7, |
| 9 | "sp_a1_wakeup": 8, | 9 | sp_a1_wakeup: 8, |
| 10 | "sp_a2_intro": 9, | 10 | sp_a2_intro: 9, |
| 11 | 11 | ||
| 12 | "sp_a2_laser_intro": 10, | 12 | sp_a2_laser_intro: 10, |
| 13 | "sp_a2_laser_stairs": 11, | 13 | sp_a2_laser_stairs: 11, |
| 14 | "sp_a2_dual_lasers": 12, | 14 | sp_a2_dual_lasers: 12, |
| 15 | "sp_a2_laser_over_goo": 13, | 15 | sp_a2_laser_over_goo: 13, |
| 16 | "sp_a2_catapult_intro": 14, | 16 | sp_a2_catapult_intro: 14, |
| 17 | "sp_a2_trust_fling": 15, | 17 | sp_a2_trust_fling: 15, |
| 18 | "sp_a2_pit_flings": 16, | 18 | sp_a2_pit_flings: 16, |
| 19 | "sp_a2_fizzler_intro": 17, | 19 | sp_a2_fizzler_intro: 17, |
| 20 | 20 | ||
| 21 | "sp_a2_sphere_peek": 18, | 21 | sp_a2_sphere_peek: 18, |
| 22 | "sp_a2_ricochet": 19, | 22 | sp_a2_ricochet: 19, |
| 23 | "sp_a2_bridge_intro": 20, | 23 | sp_a2_bridge_intro: 20, |
| 24 | "sp_a2_bridge_the_gap": 21, | 24 | sp_a2_bridge_the_gap: 21, |
| 25 | "sp_a2_turret_intro": 22, | 25 | sp_a2_turret_intro: 22, |
| 26 | "sp_a2_laser_relays": 23, | 26 | sp_a2_laser_relays: 23, |
| 27 | "sp_a2_turret_blocker": 24, | 27 | sp_a2_turret_blocker: 24, |
| 28 | "sp_a2_laser_vs_turret": 25, | 28 | sp_a2_laser_vs_turret: 25, |
| 29 | "sp_a2_pull_the_rug": 26, | 29 | sp_a2_pull_the_rug: 26, |
| 30 | 30 | ||
| 31 | "sp_a2_column_blocker": 27, | 31 | sp_a2_column_blocker: 27, |
| 32 | "sp_a2_laser_chaining": 28, | 32 | sp_a2_laser_chaining: 28, |
| 33 | "sp_a2_triple_laser": 29, | 33 | sp_a2_triple_laser: 29, |
| 34 | "sp_a2_bts1": 30, | 34 | sp_a2_bts1: 30, |
| 35 | "sp_a2_bts2": 31, | 35 | sp_a2_bts2: 31, |
| 36 | 36 | ||
| 37 | "sp_a2_bts3": 32, | 37 | sp_a2_bts3: 32, |
| 38 | "sp_a2_bts4": 33, | 38 | sp_a2_bts4: 33, |
| 39 | "sp_a2_bts5": 34, | 39 | sp_a2_bts5: 34, |
| 40 | "sp_a2_core": 35, | 40 | sp_a2_core: 35, |
| 41 | 41 | ||
| 42 | "sp_a3_01": 36, | 42 | sp_a3_01: 36, |
| 43 | "sp_a3_03": 37, | 43 | sp_a3_03: 37, |
| 44 | "sp_a3_jump_intro": 38, | 44 | sp_a3_jump_intro: 38, |
| 45 | "sp_a3_bomb_flings": 39, | 45 | sp_a3_bomb_flings: 39, |
| 46 | "sp_a3_crazy_box": 40, | 46 | sp_a3_crazy_box: 40, |
| 47 | "sp_a3_transition01": 41, | 47 | sp_a3_transition01: 41, |
| 48 | 48 | ||
| 49 | "sp_a3_speed_ramp": 42, | 49 | sp_a3_speed_ramp: 42, |
| 50 | "sp_a3_speed_flings": 43, | 50 | sp_a3_speed_flings: 43, |
| 51 | "sp_a3_portal_intro": 44, | 51 | sp_a3_portal_intro: 44, |
| 52 | "sp_a3_end": 45, | 52 | sp_a3_end: 45, |
| 53 | 53 | ||
| 54 | "sp_a4_intro": 46, | 54 | sp_a4_intro: 46, |
| 55 | "sp_a4_tb_intro": 47, | 55 | sp_a4_tb_intro: 47, |
| 56 | "sp_a4_tb_trust_drop": 48, | 56 | sp_a4_tb_trust_drop: 48, |
| 57 | "sp_a4_tb_wall_button": 49, | 57 | sp_a4_tb_wall_button: 49, |
| 58 | "sp_a4_tb_polarity": 50, | 58 | sp_a4_tb_polarity: 50, |
| 59 | "sp_a4_tb_catch": 51, | 59 | sp_a4_tb_catch: 51, |
| 60 | "sp_a4_stop_the_box": 52, | 60 | sp_a4_stop_the_box: 52, |
| 61 | "sp_a4_laser_catapult": 53, | 61 | sp_a4_laser_catapult: 53, |
| 62 | "sp_a4_laser_platform": 54, | 62 | sp_a4_laser_platform: 54, |
| 63 | "sp_a4_speed_tb_catch": 55, | 63 | sp_a4_speed_tb_catch: 55, |
| 64 | "sp_a4_jump_polarity": 56, | 64 | sp_a4_jump_polarity: 56, |
| 65 | 65 | ||
| 66 | "sp_a4_finale1": 57, | 66 | sp_a4_finale1: 57, |
| 67 | "sp_a4_finale2": 58, | 67 | sp_a4_finale2: 58, |
| 68 | "sp_a4_finale3": 59, | 68 | sp_a4_finale3: 59, |
| 69 | "sp_a4_finale4": 60, | 69 | sp_a4_finale4: 60, |
| 70 | 70 | ||
| 71 | "mp_coop_start": 61, | 71 | mp_coop_start: 61, |
| 72 | "mp_coop_lobby_3": 62, | 72 | mp_coop_lobby_3: 62, |
| 73 | 73 | ||
| 74 | "mp_coop_doors": 63, | 74 | mp_coop_doors: 63, |
| 75 | "mp_coop_race_2": 64, | 75 | mp_coop_race_2: 64, |
| 76 | "mp_coop_laser_2": 65, | 76 | mp_coop_laser_2: 65, |
| 77 | "mp_coop_rat_maze": 66, | 77 | mp_coop_rat_maze: 66, |
| 78 | "mp_coop_laser_crusher": 67, | 78 | mp_coop_laser_crusher: 67, |
| 79 | "mp_coop_teambts": 68, | 79 | mp_coop_teambts: 68, |
| 80 | 80 | ||
| 81 | "mp_coop_fling_3": 69, | 81 | mp_coop_fling_3: 69, |
| 82 | "mp_coop_infinifling_train": 70, | 82 | mp_coop_infinifling_train: 70, |
| 83 | "mp_coop_come_along": 71, | 83 | mp_coop_come_along: 71, |
| 84 | "mp_coop_fling_1": 72, | 84 | mp_coop_fling_1: 72, |
| 85 | "mp_coop_catapult_1": 73, | 85 | mp_coop_catapult_1: 73, |
| 86 | "mp_coop_multifling_1": 74, | 86 | mp_coop_multifling_1: 74, |
| 87 | "mp_coop_fling_crushers": 75, | 87 | mp_coop_fling_crushers: 75, |
| 88 | "mp_coop_fan": 76, | 88 | mp_coop_fan: 76, |
| 89 | 89 | ||
| 90 | "mp_coop_wall_intro": 77, | 90 | mp_coop_wall_intro: 77, |
| 91 | "mp_coop_wall_2": 78, | 91 | mp_coop_wall_2: 78, |
| 92 | "mp_coop_catapult_wall_intro": 79, | 92 | mp_coop_catapult_wall_intro: 79, |
| 93 | "mp_coop_wall_block": 80, | 93 | mp_coop_wall_block: 80, |
| 94 | "mp_coop_catapult_2": 81, | 94 | mp_coop_catapult_2: 81, |
| 95 | "mp_coop_turret_walls": 82, | 95 | mp_coop_turret_walls: 82, |
| 96 | "mp_coop_turret_ball": 83, | 96 | mp_coop_turret_ball: 83, |
| 97 | "mp_coop_wall_5": 84, | 97 | mp_coop_wall_5: 84, |
| 98 | 98 | ||
| 99 | "mp_coop_tbeam_redirect": 85, | 99 | mp_coop_tbeam_redirect: 85, |
| 100 | "mp_coop_tbeam_drill": 86, | 100 | mp_coop_tbeam_drill: 86, |
| 101 | "mp_coop_tbeam_catch_grind_1": 87, | 101 | mp_coop_tbeam_catch_grind_1: 87, |
| 102 | "mp_coop_tbeam_laser_1": 88, | 102 | mp_coop_tbeam_laser_1: 88, |
| 103 | "mp_coop_tbeam_polarity": 89, | 103 | mp_coop_tbeam_polarity: 89, |
| 104 | "mp_coop_tbeam_polarity2": 90, | 104 | mp_coop_tbeam_polarity2: 90, |
| 105 | "mp_coop_tbeam_polarity3": 91, | 105 | mp_coop_tbeam_polarity3: 91, |
| 106 | "mp_coop_tbeam_maze": 92, | 106 | mp_coop_tbeam_maze: 92, |
| 107 | "mp_coop_tbeam_end": 93, | 107 | mp_coop_tbeam_end: 93, |
| 108 | 108 | ||
| 109 | "mp_coop_paint_come_along": 94, | 109 | mp_coop_paint_come_along: 94, |
| 110 | "mp_coop_paint_redirect": 95, | 110 | mp_coop_paint_redirect: 95, |
| 111 | "mp_coop_paint_bridge": 96, | 111 | mp_coop_paint_bridge: 96, |
| 112 | "mp_coop_paint_walljumps": 97, | 112 | mp_coop_paint_walljumps: 97, |
| 113 | "mp_coop_paint_speed_fling": 98, | 113 | mp_coop_paint_speed_fling: 98, |
| 114 | "mp_coop_paint_red_racer": 99, | 114 | mp_coop_paint_red_racer: 99, |
| 115 | "mp_coop_paint_speed_catch": 100, | 115 | mp_coop_paint_speed_catch: 100, |
| 116 | "mp_coop_paint_longjump_intro": 101, | 116 | mp_coop_paint_longjump_intro: 101, |
| 117 | 117 | ||
| 118 | "mp_coop_separation_1": 102, | 118 | mp_coop_separation_1: 102, |
| 119 | "mp_coop_tripleaxis": 103, | 119 | mp_coop_tripleaxis: 103, |
| 120 | "mp_coop_catapult_catch": 104, | 120 | mp_coop_catapult_catch: 104, |
| 121 | "mp_coop_2paints_1bridge": 105, | 121 | mp_coop_2paints_1bridge: 105, |
| 122 | "mp_coop_paint_conversion": 106, | 122 | mp_coop_paint_conversion: 106, |
| 123 | "mp_coop_bridge_catch": 107, | 123 | mp_coop_bridge_catch: 107, |
| 124 | "mp_coop_laser_tbeam": 108, | 124 | mp_coop_laser_tbeam: 108, |
| 125 | "mp_coop_paint_rat_maze": 109, | 125 | mp_coop_paint_rat_maze: 109, |
| 126 | "mp_coop_paint_crazy_box": 110, | 126 | mp_coop_paint_crazy_box: 110, |
| 127 | }; | 127 | }; |
diff --git a/frontend/src/types/Pagination.ts b/frontend/src/types/Pagination.ts index ccff04b..18494eb 100644 --- a/frontend/src/types/Pagination.ts +++ b/frontend/src/types/Pagination.ts | |||
| @@ -3,4 +3,4 @@ export interface Pagination { | |||
| 3 | total_pages: number; | 3 | total_pages: number; |
| 4 | current_page: number; | 4 | current_page: number; |
| 5 | page_size: number; | 5 | page_size: number; |
| 6 | }; | 6 | } |
diff --git a/frontend/src/types/Profile.ts b/frontend/src/types/Profile.ts index 42e5c3e..3c83d29 100644 --- a/frontend/src/types/Profile.ts +++ b/frontend/src/types/Profile.ts | |||
| @@ -4,7 +4,7 @@ export interface UserShort { | |||
| 4 | steam_id: string; | 4 | steam_id: string; |
| 5 | user_name: string; | 5 | user_name: string; |
| 6 | avatar_link: string; | 6 | avatar_link: string; |
| 7 | }; | 7 | } |
| 8 | 8 | ||
| 9 | export interface UserProfile { | 9 | export interface UserProfile { |
| 10 | profile: boolean; | 10 | profile: boolean; |
| @@ -17,25 +17,25 @@ export interface UserProfile { | |||
| 17 | rankings: UserProfileRankings; | 17 | rankings: UserProfileRankings; |
| 18 | records: UserProfileRecords[]; | 18 | records: UserProfileRecords[]; |
| 19 | pagination: Pagination; | 19 | pagination: Pagination; |
| 20 | }; | 20 | } |
| 21 | 21 | ||
| 22 | interface UserProfileTitles { | 22 | interface UserProfileTitles { |
| 23 | name: string; | 23 | name: string; |
| 24 | color: string; | 24 | color: string; |
| 25 | }; | 25 | } |
| 26 | 26 | ||
| 27 | interface UserProfileLinks { | 27 | interface UserProfileLinks { |
| 28 | p2sr: string; | 28 | p2sr: string; |
| 29 | steam: string; | 29 | steam: string; |
| 30 | youtube: string; | 30 | youtube: string; |
| 31 | twitch: string; | 31 | twitch: string; |
| 32 | }; | 32 | } |
| 33 | 33 | ||
| 34 | interface UserProfileRankings { | 34 | interface UserProfileRankings { |
| 35 | overall: UserProfileRankingsDetail; | 35 | overall: UserProfileRankingsDetail; |
| 36 | singleplayer: UserProfileRankingsDetail; | 36 | singleplayer: UserProfileRankingsDetail; |
| 37 | cooperative: UserProfileRankingsDetail; | 37 | cooperative: UserProfileRankingsDetail; |
| 38 | }; | 38 | } |
| 39 | 39 | ||
| 40 | interface UserProfileRecords { | 40 | interface UserProfileRecords { |
| 41 | game_id: number; | 41 | game_id: number; |
| @@ -44,8 +44,8 @@ interface UserProfileRecords { | |||
| 44 | map_name: string; | 44 | map_name: string; |
| 45 | map_wr_count: number; | 45 | map_wr_count: number; |
| 46 | placement: number; | 46 | placement: number; |
| 47 | scores: UserProfileRecordsScores[] | 47 | scores: UserProfileRecordsScores[]; |
| 48 | }; | 48 | } |
| 49 | 49 | ||
| 50 | interface UserProfileRecordsScores { | 50 | interface UserProfileRecordsScores { |
| 51 | record_id: number; | 51 | record_id: number; |
| @@ -53,11 +53,10 @@ interface UserProfileRecordsScores { | |||
| 53 | score_count: number; | 53 | score_count: number; |
| 54 | score_time: number; | 54 | score_time: number; |
| 55 | date: string; | 55 | date: string; |
| 56 | }; | 56 | } |
| 57 | 57 | ||
| 58 | interface UserProfileRankingsDetail { | 58 | interface UserProfileRankingsDetail { |
| 59 | rank: number; | 59 | rank: number; |
| 60 | completion_count: number; | 60 | completion_count: number; |
| 61 | completion_total: number; | 61 | completion_total: number; |
| 62 | }; | 62 | } |
| 63 | |||
diff --git a/frontend/src/types/Ranking.ts b/frontend/src/types/Ranking.ts index a143355..800f4be 100644 --- a/frontend/src/types/Ranking.ts +++ b/frontend/src/types/Ranking.ts | |||
| @@ -1,31 +1,31 @@ | |||
| 1 | import type { UserShort } from "@customTypes/Profile"; | 1 | import type { UserShort } from "@customTypes/Profile"; |
| 2 | 2 | ||
| 3 | export interface RankingType { | 3 | export interface RankingType { |
| 4 | placement: number; | 4 | placement: number; |
| 5 | user: UserShort; | 5 | user: UserShort; |
| 6 | total_score: number; | 6 | total_score: number; |
| 7 | } | 7 | } |
| 8 | 8 | ||
| 9 | export interface SteamRankingType { | 9 | export interface SteamRankingType { |
| 10 | user_name: string; | 10 | user_name: string; |
| 11 | avatar_link: string; | 11 | avatar_link: string; |
| 12 | steam_id: string; | 12 | steam_id: string; |
| 13 | sp_score: number; | 13 | sp_score: number; |
| 14 | mp_score: number; | 14 | mp_score: number; |
| 15 | overall_score: number; | 15 | overall_score: number; |
| 16 | sp_rank: number; | 16 | sp_rank: number; |
| 17 | mp_rank: number; | 17 | mp_rank: number; |
| 18 | overall_rank: number; | 18 | overall_rank: number; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | export interface Ranking { | 21 | export interface Ranking { |
| 22 | rankings_overall: RankingType[]; | 22 | rankings_overall: RankingType[]; |
| 23 | rankings_singleplayer: RankingType[]; | 23 | rankings_singleplayer: RankingType[]; |
| 24 | rankings_multiplayer: RankingType[]; | 24 | rankings_multiplayer: RankingType[]; |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | export interface SteamRanking { | 27 | export interface SteamRanking { |
| 28 | rankings_overall: SteamRankingType[]; | 28 | rankings_overall: SteamRankingType[]; |
| 29 | rankings_singleplayer: SteamRankingType[]; | 29 | rankings_singleplayer: SteamRankingType[]; |
| 30 | rankings_multiplayer: SteamRankingType[]; | 30 | rankings_multiplayer: SteamRankingType[]; |
| 31 | } \ No newline at end of file | 31 | } |
diff --git a/frontend/src/types/Search.ts b/frontend/src/types/Search.ts index d218806..b258ee3 100644 --- a/frontend/src/types/Search.ts +++ b/frontend/src/types/Search.ts | |||
| @@ -3,11 +3,11 @@ import type { UserShort } from "@customTypes/Profile"; | |||
| 3 | export interface Search { | 3 | export interface Search { |
| 4 | players: UserShort[]; | 4 | players: UserShort[]; |
| 5 | maps: SearchMap[]; | 5 | maps: SearchMap[]; |
| 6 | }; | 6 | } |
| 7 | 7 | ||
| 8 | interface SearchMap { | 8 | interface SearchMap { |
| 9 | id: number; | 9 | id: number; |
| 10 | game: string; | 10 | game: string; |
| 11 | chapter: string; | 11 | chapter: string; |
| 12 | map: string; | 12 | map: string; |
| 13 | }; | 13 | } |
diff --git a/frontend/src/utils/Jwt.ts b/frontend/src/utils/Jwt.ts index ce351fb..dc6ec92 100644 --- a/frontend/src/utils/Jwt.ts +++ b/frontend/src/utils/Jwt.ts | |||
| @@ -1,44 +1,48 @@ | |||
| 1 | // llm ahh funcs | 1 | // llm ahh funcs |
| 2 | export function get_user_id_from_token(token: string | undefined): string | undefined { | 2 | export function get_user_id_from_token( |
| 3 | token: string | undefined | ||
| 4 | ): string | undefined { | ||
| 3 | if (!token) { | 5 | if (!token) { |
| 4 | return undefined; | 6 | return undefined; |
| 5 | } | 7 | } |
| 6 | const parts = token.split('.'); | 8 | const parts = token.split("."); |
| 7 | if (parts.length !== 3) { | 9 | if (parts.length !== 3) { |
| 8 | return undefined; | 10 | return undefined; |
| 9 | } | 11 | } |
| 10 | const base64Url = parts[1]; | 12 | const base64Url = parts[1]; |
| 11 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | 13 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); |
| 12 | 14 | ||
| 13 | const jsonPayload = decodeURIComponent( | 15 | const jsonPayload = decodeURIComponent( |
| 14 | atob(base64) | 16 | atob(base64) |
| 15 | .split('') | 17 | .split("") |
| 16 | .map(function (c) { | 18 | .map(function (c) { |
| 17 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | 19 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
| 18 | }) | 20 | }) |
| 19 | .join('') | 21 | .join("") |
| 20 | ); | 22 | ); |
| 21 | return JSON.parse(jsonPayload).sub; | 23 | return JSON.parse(jsonPayload).sub; |
| 22 | }; | 24 | } |
| 23 | 25 | ||
| 24 | export function get_user_mod_from_token(token: string | undefined): boolean | undefined { | 26 | export function get_user_mod_from_token( |
| 27 | token: string | undefined | ||
| 28 | ): boolean | undefined { | ||
| 25 | if (!token) { | 29 | if (!token) { |
| 26 | return undefined; | 30 | return undefined; |
| 27 | } | 31 | } |
| 28 | const parts = token.split('.'); | 32 | const parts = token.split("."); |
| 29 | if (parts.length !== 3) { | 33 | if (parts.length !== 3) { |
| 30 | return undefined; | 34 | return undefined; |
| 31 | } | 35 | } |
| 32 | const base64Url = parts[1]; | 36 | const base64Url = parts[1]; |
| 33 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); | 37 | const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); |
| 34 | 38 | ||
| 35 | const jsonPayload = decodeURIComponent( | 39 | const jsonPayload = decodeURIComponent( |
| 36 | atob(base64) | 40 | atob(base64) |
| 37 | .split('') | 41 | .split("") |
| 38 | .map(function (c) { | 42 | .map(function (c) { |
| 39 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); | 43 | return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); |
| 40 | }) | 44 | }) |
| 41 | .join('') | 45 | .join("") |
| 42 | ); | 46 | ); |
| 43 | return JSON.parse(jsonPayload).mod; | 47 | return JSON.parse(jsonPayload).mod; |
| 44 | }; | 48 | } |
diff --git a/frontend/src/utils/Time.ts b/frontend/src/utils/Time.ts index b83a7ed..8f2c03c 100644 --- a/frontend/src/utils/Time.ts +++ b/frontend/src/utils/Time.ts | |||
| @@ -1,42 +1,62 @@ | |||
| 1 | export function time_ago(date: any) { | 1 | export function time_ago(date: any) { |
| 2 | const now = new Date().getTime(); | 2 | const now = new Date().getTime(); |
| 3 | 3 | ||
| 4 | const localDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); | 4 | const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000); |
| 5 | const seconds = Math.floor((now - localDate.getTime()) / 1000); | 5 | const seconds = Math.floor((now - localDate.getTime()) / 1000); |
| 6 | 6 | ||
| 7 | let interval = Math.floor(seconds / 31536000); | 7 | let interval = Math.floor(seconds / 31536000); |
| 8 | if (interval === 1) {return interval + ' year ago';} | 8 | if (interval === 1) { |
| 9 | if (interval > 1) {return interval + ' years ago';} | 9 | return interval + " year ago"; |
| 10 | } | ||
| 11 | if (interval > 1) { | ||
| 12 | return interval + " years ago"; | ||
| 13 | } | ||
| 10 | 14 | ||
| 11 | interval = Math.floor(seconds / 2592000); | 15 | interval = Math.floor(seconds / 2592000); |
| 12 | if (interval === 1) {return interval + ' month ago';} | 16 | if (interval === 1) { |
| 13 | if (interval > 1) {return interval + ' months ago';} | 17 | return interval + " month ago"; |
| 18 | } | ||
| 19 | if (interval > 1) { | ||
| 20 | return interval + " months ago"; | ||
| 21 | } | ||
| 14 | 22 | ||
| 15 | interval = Math.floor(seconds / 86400); | 23 | interval = Math.floor(seconds / 86400); |
| 16 | if (interval === 1) {return interval + ' day ago';} | 24 | if (interval === 1) { |
| 17 | if (interval > 1) {return interval + ' days ago';} | 25 | return interval + " day ago"; |
| 26 | } | ||
| 27 | if (interval > 1) { | ||
| 28 | return interval + " days ago"; | ||
| 29 | } | ||
| 18 | 30 | ||
| 19 | interval = Math.floor(seconds / 3600); | 31 | interval = Math.floor(seconds / 3600); |
| 20 | if (interval === 1) {return interval + ' hour ago';} | 32 | if (interval === 1) { |
| 21 | if (interval > 1) {return interval + ' hours ago';} | 33 | return interval + " hour ago"; |
| 34 | } | ||
| 35 | if (interval > 1) { | ||
| 36 | return interval + " hours ago"; | ||
| 37 | } | ||
| 22 | 38 | ||
| 23 | interval = Math.floor(seconds / 60); | 39 | interval = Math.floor(seconds / 60); |
| 24 | if (interval === 1) {return interval + ' minute ago';} | 40 | if (interval === 1) { |
| 25 | if (interval > 1) {return interval + ' minutes ago';} | 41 | return interval + " minute ago"; |
| 42 | } | ||
| 43 | if (interval > 1) { | ||
| 44 | return interval + " minutes ago"; | ||
| 45 | } | ||
| 26 | 46 | ||
| 27 | if(seconds < 10) return 'just now'; | 47 | if (seconds < 10) return "just now"; |
| 28 | 48 | ||
| 29 | return Math.floor(seconds) + ' seconds ago'; | 49 | return Math.floor(seconds) + " seconds ago"; |
| 30 | }; | 50 | } |
| 31 | 51 | ||
| 32 | export function ticks_to_time(ticks: number) { | 52 | export function ticks_to_time(ticks: number) { |
| 33 | let seconds = Math.floor(ticks / 60) | 53 | let seconds = Math.floor(ticks / 60); |
| 34 | let minutes = Math.floor(seconds / 60) | 54 | let minutes = Math.floor(seconds / 60); |
| 35 | let hours = Math.floor(minutes / 60) | 55 | let hours = Math.floor(minutes / 60); |
| 36 | 56 | ||
| 37 | let milliseconds = Math.floor((ticks % 60) * 1000 / 60) | 57 | let milliseconds = Math.floor(((ticks % 60) * 1000) / 60); |
| 38 | seconds = seconds % 60; | 58 | seconds = seconds % 60; |
| 39 | minutes = minutes % 60; | 59 | minutes = minutes % 60; |
| 40 | 60 | ||
| 41 | return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')}`; | 61 | return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, "0") + ":" : minutes + ":"}${minutes > 0 ? seconds.toString().padStart(2, "0") : seconds}.${milliseconds.toString().padStart(3, "0")}`; |
| 42 | }; \ No newline at end of file | 62 | } |