diff options
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/App.css | 229 | ||||
| -rw-r--r-- | frontend/src/api/Api.ts | 2 | ||||
| -rw-r--r-- | frontend/src/components/ConfirmDialog.tsx | 16 | ||||
| -rw-r--r-- | frontend/src/components/GameCategory.tsx | 15 | ||||
| -rw-r--r-- | frontend/src/components/GameEntry.tsx | 27 | ||||
| -rw-r--r-- | frontend/src/components/Leaderboards.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/Login.tsx | 53 | ||||
| -rw-r--r-- | frontend/src/components/ModMenu.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 386 | ||||
| -rw-r--r-- | frontend/src/components/Summary.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/images/Images.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/images/svgs/steam.tsx | 7 | ||||
| -rw-r--r-- | frontend/src/pages/About.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/pages/Games.tsx | 32 | ||||
| -rw-r--r-- | frontend/src/pages/Homepage.tsx | 12 | ||||
| -rw-r--r-- | frontend/src/pages/Maplist.tsx | 131 | ||||
| -rw-r--r-- | frontend/src/pages/Maps.tsx | 15 | ||||
| -rw-r--r-- | frontend/src/pages/Profile.tsx | 1 | ||||
| -rw-r--r-- | frontend/src/pages/Rules.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/pages/User.tsx | 1 |
21 files changed, 582 insertions, 366 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css index 14a9972..a4c058b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css | |||
| @@ -1,3 +1,54 @@ | |||
| 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 | @import "tailwindcss"; | ||
| 3 | |||
| 4 | @theme { | ||
| 5 | --color-rosewater: #f2d5cf; | ||
| 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; | ||
| 31 | |||
| 32 | --color-primary: var(--color-mauve); | ||
| 33 | --color-secondary: var(--color-blue); | ||
| 34 | --color-accent: var(--color-peach); | ||
| 35 | --color-background: var(--color-base); | ||
| 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); | ||
| 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'; | ||
| 50 | } | ||
| 51 | |||
| 1 | main { | 52 | main { |
| 2 | overflow: auto; | 53 | overflow: auto; |
| 3 | overflow-x: hidden; | 54 | overflow-x: hidden; |
| @@ -10,33 +61,32 @@ main { | |||
| 10 | padding-right: 30px; | 61 | padding-right: 30px; |
| 11 | 62 | ||
| 12 | font-size: 40px; | 63 | font-size: 40px; |
| 13 | font-family: BarlowSemiCondensed-Regular; | 64 | font-family: var(--font-barlow-semicondensed-regular); |
| 14 | color: #cdcfdf; | 65 | color: var(--color-text); |
| 15 | 66 | ||
| 16 | } | 67 | } |
| 17 | 68 | ||
| 18 | a { | 69 | a { |
| 19 | color: inherit; | 70 | color: inherit; |
| 20 | width: fit-content; | ||
| 21 | } | 71 | } |
| 22 | 72 | ||
| 23 | body { | 73 | body { |
| 24 | overflow: hidden; | 74 | overflow: hidden; |
| 25 | background-color: #141520; | 75 | background-color: var(--color-crust); |
| 26 | margin: 0; | 76 | margin: 0; |
| 27 | } | 77 | } |
| 28 | 78 | ||
| 29 | .loader { | 79 | .loader { |
| 30 | animation: loader 1.2s ease infinite; | 80 | animation: loader 1.2s ease infinite; |
| 31 | background-size: 400% 300%; | 81 | background-size: 400% 300%; |
| 32 | background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); | 82 | 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; | 83 | user-select: none; |
| 34 | } | 84 | } |
| 35 | 85 | ||
| 36 | .loader-text { | 86 | .loader-text { |
| 37 | animation: loader 1.2s ease infinite; | 87 | animation: loader 1.2s ease infinite; |
| 38 | background-size: 400% 300%; | 88 | background-size: 400% 300%; |
| 39 | background-image: linear-gradient(-90deg, #202232 0%, #202232 25%, #2a2c41 50%, #202232 75%, #202232 100%); | 89 | 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; | 90 | user-select: none; |
| 41 | color: #00000000; | 91 | color: #00000000; |
| 42 | border-radius: 1000px; | 92 | border-radius: 1000px; |
| @@ -76,6 +126,173 @@ body { | |||
| 76 | } | 126 | } |
| 77 | } | 127 | } |
| 78 | 128 | ||
| 129 | /* Custom Tailwind utilities for Catppuccin Frappe theme */ | ||
| 130 | @layer utilities { | ||
| 131 | .bg-primary { | ||
| 132 | background-color: var(--color-primary); | ||
| 133 | } | ||
| 134 | |||
| 135 | .bg-secondary { | ||
| 136 | background-color: var(--color-secondary); | ||
| 137 | } | ||
| 138 | |||
| 139 | .bg-accent { | ||
| 140 | background-color: var(--color-accent); | ||
| 141 | } | ||
| 142 | |||
| 143 | .bg-background { | ||
| 144 | background-color: var(--color-background); | ||
| 145 | } | ||
| 146 | |||
| 147 | .bg-surface { | ||
| 148 | background-color: var(--color-surface); | ||
| 149 | } | ||
| 150 | |||
| 151 | .bg-muted { | ||
| 152 | background-color: var(--color-muted); | ||
| 153 | } | ||
| 154 | |||
| 155 | .text-primary { | ||
| 156 | color: var(--color-primary); | ||
| 157 | } | ||
| 158 | |||
| 159 | .text-secondary { | ||
| 160 | color: var(--color-secondary); | ||
| 161 | } | ||
| 162 | |||
| 163 | .text-accent { | ||
| 164 | color: var(--color-accent); | ||
| 165 | } | ||
| 166 | |||
| 167 | .text-foreground { | ||
| 168 | color: var(--color-foreground); | ||
| 169 | } | ||
| 170 | |||
| 171 | .text-muted { | ||
| 172 | color: var(--color-muted); | ||
| 173 | } | ||
| 174 | |||
| 175 | .border-primary { | ||
| 176 | border-color: var(--color-primary); | ||
| 177 | } | ||
| 178 | |||
| 179 | .border-secondary { | ||
| 180 | border-color: var(--color-secondary); | ||
| 181 | } | ||
| 182 | |||
| 183 | .border-muted { | ||
| 184 | border-color: var(--color-border); | ||
| 185 | } | ||
| 186 | |||
| 187 | .hover\:bg-primary:hover { | ||
| 188 | background-color: var(--color-primary); | ||
| 189 | } | ||
| 190 | |||
| 191 | .hover\:bg-secondary:hover { | ||
| 192 | background-color: var(--color-secondary); | ||
| 193 | } | ||
| 194 | |||
| 195 | .hover\:bg-surface:hover { | ||
| 196 | background-color: var(--color-surface); | ||
| 197 | } | ||
| 198 | |||
| 199 | .hover\:text-primary:hover { | ||
| 200 | color: var(--color-primary); | ||
| 201 | } | ||
| 202 | |||
| 203 | .focus\:ring-primary:focus { | ||
| 204 | --tw-ring-color: var(--color-primary); | ||
| 205 | } | ||
| 206 | |||
| 207 | .triangle { | ||
| 208 | width: 0; | ||
| 209 | height: 0; | ||
| 210 | border-left: 5px solid transparent; | ||
| 211 | border-right: 5px solid transparent; | ||
| 212 | border-bottom: 8px solid var(--color-foreground); | ||
| 213 | display: inline-block; | ||
| 214 | } | ||
| 215 | |||
| 216 | .sidebar-button-selected { | ||
| 217 | background-color: var(--color-primary) !important; | ||
| 218 | color: var(--color-background) !important; | ||
| 219 | } | ||
| 220 | |||
| 221 | .sidebar-button-deselected { | ||
| 222 | background-color: var(--color-surface) !important; | ||
| 223 | color: var(--color-foreground) !important; | ||
| 224 | } | ||
| 225 | |||
| 226 | .profileboard-record { | ||
| 227 | background-color: var(--color-surface); | ||
| 228 | border: 1px solid var(--color-border); | ||
| 229 | border-radius: 0.5rem; | ||
| 230 | padding: 0.75rem; | ||
| 231 | margin-bottom: 0.5rem; | ||
| 232 | transition: all 0.2s ease; | ||
| 233 | } | ||
| 234 | |||
| 235 | .profileboard-record:hover { | ||
| 236 | background-color: var(--color-surface1); | ||
| 237 | } | ||
| 238 | |||
| 239 | .difficulty-rating { | ||
| 240 | width: 20px; | ||
| 241 | height: 20px; | ||
| 242 | background-color: var(--color-muted); | ||
| 243 | border-radius: 50%; | ||
| 244 | margin: 0 2px; | ||
| 245 | display: inline-block; | ||
| 246 | } | ||
| 247 | |||
| 248 | .nav-button { | ||
| 249 | background-color: var(--color-surface); | ||
| 250 | color: var(--color-foreground); | ||
| 251 | border: 1px solid var(--color-border); | ||
| 252 | border-radius: 0.5rem; | ||
| 253 | padding: 0.5rem 1rem; | ||
| 254 | transition: all 0.2s ease; | ||
| 255 | display: inline-flex; | ||
| 256 | align-items: center; | ||
| 257 | gap: 0.5rem; | ||
| 258 | text-decoration: none; | ||
| 259 | } | ||
| 260 | |||
| 261 | .nav-button:hover { | ||
| 262 | background-color: var(--color-surface1); | ||
| 263 | } | ||
| 264 | |||
| 265 | .record { | ||
| 266 | background-color: var(--color-surface); | ||
| 267 | border: 1px solid var(--color-border); | ||
| 268 | border-radius: 0.5rem; | ||
| 269 | padding: 0.5rem; | ||
| 270 | margin: 0.25rem; | ||
| 271 | cursor: pointer; | ||
| 272 | transition: all 0.2s ease; | ||
| 273 | } | ||
| 274 | |||
| 275 | .record:hover { | ||
| 276 | background-color: var(--color-surface1); | ||
| 277 | } | ||
| 278 | |||
| 279 | .portal-count { | ||
| 280 | font-size: 3rem; | ||
| 281 | font-weight: bold; | ||
| 282 | color: var(--color-primary); | ||
| 283 | } | ||
| 284 | |||
| 285 | .titles { | ||
| 286 | background-color: var(--color-accent); | ||
| 287 | color: var(--color-background); | ||
| 288 | padding: 0.25rem 0.5rem; | ||
| 289 | border-radius: 1rem; | ||
| 290 | font-size: 0.875rem; | ||
| 291 | margin-right: 0.5rem; | ||
| 292 | display: inline-block; | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 79 | @font-face { | 296 | @font-face { |
| 80 | font-family: 'BarlowCondensed-Bold'; | 297 | font-family: 'BarlowCondensed-Bold'; |
| 81 | src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); | 298 | src: local('BarlowCondensed-Bold'), url(./fonts/BarlowCondensed-Bold.ttf) format('truetype'); |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 0e1658c..b782d17 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -91,7 +91,7 @@ export const API = { | |||
| 91 | delete_map_summary(token, map_id, route_id), | 91 | delete_map_summary(token, map_id, route_id), |
| 92 | }; | 92 | }; |
| 93 | 93 | ||
| 94 | const BASE_API_URL: string = "/api/v1/"; | 94 | const BASE_API_URL: string = "https://lp.portal2.sr/api/v1/" |
| 95 | 95 | ||
| 96 | export function url(path: string): string { | 96 | export function url(path: string): string { |
| 97 | return BASE_API_URL + path; | 97 | return BASE_API_URL + path; |
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx index c89d9ea..8f2ce7a 100644 --- a/frontend/src/components/ConfirmDialog.tsx +++ b/frontend/src/components/ConfirmDialog.tsx | |||
| @@ -1,7 +1,5 @@ | |||
| 1 | import React from "react"; | 1 | import React from "react"; |
| 2 | 2 | ||
| 3 | import "@css/Dialog.css"; | ||
| 4 | |||
| 5 | interface ConfirmDialogProps { | 3 | interface ConfirmDialogProps { |
| 6 | title: string; | 4 | title: string; |
| 7 | subtitle: string; | 5 | subtitle: string; |
| @@ -16,17 +14,17 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ | |||
| 16 | onCancel, | 14 | onCancel, |
| 17 | }) => { | 15 | }) => { |
| 18 | return ( | 16 | return ( |
| 19 | <div className="dimmer"> | 17 | <div className="fixed w-[200%] h-full bg-black bg-opacity-50 z-[4]"> |
| 20 | <div className="dialog"> | 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 className="dialog-element dialog-header"> | 19 | <div className="p-2 text-2xl bg-mantle"> |
| 22 | <span>{title}</span> | 20 | <span>{title}</span> |
| 23 | </div> | 21 | </div> |
| 24 | <div className="dialog-element dialog-description"> | 22 | <div className="p-2"> |
| 25 | <span>{subtitle}</span> | 23 | <span>{subtitle}</span> |
| 26 | </div> | 24 | </div> |
| 27 | <div className="dialog-element dialog-btns-container"> | 25 | <div className="p-2 flex justify-end border-t-2 border-border bg-mantle"> |
| 28 | <button onClick={onCancel}>Cancel</button> | 26 | <button className="mr-2 px-4 py-2 bg-muted text-foreground rounded hover:bg-overlay1 transition-colors" onClick={onCancel}>Cancel</button> |
| 29 | <button onClick={onConfirm}>Confirm</button> | 27 | <button className="px-4 py-2 bg-primary text-background rounded hover:bg-mauve transition-colors" onClick={onConfirm}>Confirm</button> |
| 30 | </div> | 28 | </div> |
| 31 | </div> | 29 | </div> |
| 32 | </div> | 30 | </div> |
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx index 2bb6d42..b18c9d9 100644 --- a/frontend/src/components/GameCategory.tsx +++ b/frontend/src/components/GameCategory.tsx | |||
| @@ -2,7 +2,6 @@ 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; |
| @@ -12,18 +11,12 @@ interface GameCategoryProps { | |||
| 12 | const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { | 11 | const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { |
| 13 | return ( | 12 | return ( |
| 14 | <Link | 13 | <Link |
| 15 | className="games-page-item-body-item" | 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 | to={"/games/" + game.id + "?cat=" + cat.category.id} | 15 | to={"/games/" + game.id + "?cat=" + cat.category.id} |
| 17 | > | 16 | > |
| 18 | <div> | 17 | <p className="text-3xl font-semibold">{cat.category.name}</p> |
| 19 | <span className="games-page-item-body-item-title"> | 18 | <br /> |
| 20 | {cat.category.name} | 19 | <p className="font-bold text-4xl">{cat.portal_count}</p> |
| 21 | </span> | ||
| 22 | <br /> | ||
| 23 | <span className="games-page-item-body-item-num"> | ||
| 24 | {cat.portal_count} | ||
| 25 | </span> | ||
| 26 | </div> | ||
| 27 | </Link> | 20 | </Link> |
| 28 | ); | 21 | ); |
| 29 | }; | 22 | }; |
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx index 04c3483..f8fd179 100644 --- a/frontend/src/components/GameEntry.tsx +++ b/frontend/src/components/GameEntry.tsx | |||
| @@ -2,7 +2,6 @@ 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 | ||
| @@ -18,23 +17,25 @@ 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}> | 20 | <Link to={"/games/" + game.id} className="w-full"> |
| 22 | <div className="games-page-item"> | 21 | <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6"> |
| 23 | <div className="games-page-item-header"> | 22 | <div className="w-full h-1/2 bg-cover overflow-hidden relative"> |
| 24 | <div | 23 | <div |
| 25 | style={{ backgroundImage: `url(${game.image})` }} | 24 | style={{ backgroundImage: `url(${game.image})` }} |
| 26 | className="games-page-item-header-img" | 25 | className="w-full h-full backdrop-blur-sm blur-sm bg-cover" |
| 27 | ></div> | 26 | ></div> |
| 28 | <span> | 27 | <span className="absolute inset-0 flex justify-center items-center"> |
| 29 | <b>{game.name}</b> | 28 | <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b> |
| 30 | </span> | 29 | </span> |
| 31 | </div> | 30 | </div> |
| 32 | <div id={game.id as any as string} className="games-page-item-body"> | 31 | <div className="flex justify-center items-center h-1/2"> |
| 33 | {catInfo.map((cat, index) => { | 32 | <div className="flex flex-row justify-between w-full"> |
| 34 | return ( | 33 | {catInfo.map((cat, index) => { |
| 35 | <GameCategory cat={cat} game={game} key={index}></GameCategory> | 34 | return ( |
| 36 | ); | 35 | <GameCategory key={index} cat={cat} game={game} /> |
| 37 | })} | 36 | ); |
| 37 | })} | ||
| 38 | </div> | ||
| 38 | </div> | 39 | </div> |
| 39 | </div> | 40 | </div> |
| 40 | </Link> | 41 | </Link> |
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx index b388aba..99481a2 100644 --- a/frontend/src/components/Leaderboards.tsx +++ b/frontend/src/components/Leaderboards.tsx | |||
| @@ -36,7 +36,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 36 | return ( | 36 | return ( |
| 37 | <section id="section6" className="summary2"> | 37 | <section id="section6" className="summary2"> |
| 38 | <h1 style={{ textAlign: "center" }}> | 38 | <h1 style={{ textAlign: "center" }}> |
| 39 | Map is not available for competitive boards. | 39 | Loading... |
| 40 | </h1> | 40 | </h1> |
| 41 | </section> | 41 | </section> |
| 42 | ); | 42 | ); |
| @@ -195,6 +195,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 195 | filter: | 195 | filter: |
| 196 | "hue-rotate(160deg) contrast(60%) saturate(1000%)", | 196 | "hue-rotate(160deg) contrast(60%) saturate(1000%)", |
| 197 | }} | 197 | }} |
| 198 | className="w-6 h-6 mx-4" | ||
| 198 | /> | 199 | /> |
| 199 | </button> | 200 | </button> |
| 200 | <button | 201 | <button |
| @@ -209,6 +210,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 209 | filter: | 210 | filter: |
| 210 | "hue-rotate(300deg) contrast(60%) saturate(1000%)", | 211 | "hue-rotate(300deg) contrast(60%) saturate(1000%)", |
| 211 | }} | 212 | }} |
| 213 | className="w-6 h-6" | ||
| 212 | /> | 214 | /> |
| 213 | </button> | 215 | </button> |
| 214 | </span> | 216 | </span> |
| @@ -227,7 +229,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => { | |||
| 227 | (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`) | 229 | (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`) |
| 228 | } | 230 | } |
| 229 | > | 231 | > |
| 230 | <img src={DownloadIcon} alt="download" /> | 232 | <img src={DownloadIcon} alt="download" className="w-6 h-6 mr-4" /> |
| 231 | </button> | 233 | </button> |
| 232 | </span> | 234 | </span> |
| 233 | ) | 235 | ) |
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index 1858c48..ba85aeb 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx | |||
| @@ -1,18 +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>>; |
| 12 | isOpen: boolean; | ||
| 13 | } | 13 | } |
| 14 | 14 | ||
| 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | 15 | const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile, isOpen }) => { |
| 16 | const navigate = useNavigate(); | 16 | const navigate = useNavigate(); |
| 17 | 17 | ||
| 18 | const _login = () => { | 18 | const _login = () => { |
| @@ -32,16 +32,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | |||
| 32 | <> | 32 | <> |
| 33 | {profile.profile ? ( | 33 | {profile.profile ? ( |
| 34 | <> | 34 | <> |
| 35 | <Link to="/profile" tabIndex={-1} className="login"> | 35 | <Link to="/profile" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]"> |
| 36 | <button className="sidebar-button"> | 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"> |
| 37 | <img | 37 | <img |
| 38 | className="avatar-img" | 38 | className="rounded-[50px]" |
| 39 | src={profile.avatar_link} | 39 | src={profile.avatar_link} |
| 40 | alt="" | 40 | alt="" |
| 41 | /> | 41 | /> |
| 42 | <span>{profile.user_name}</span> | 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> |
| 43 | </button> | 43 | </button> |
| 44 | <button className="logout-button" onClick={_logout}> | 44 | <button className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent" onClick={_logout}> |
| 45 | <img src={ExitIcon} alt="" /> | 45 | <img src={ExitIcon} alt="" /> |
| 46 | <span /> | 46 | <span /> |
| 47 | </button> | 47 | </button> |
| @@ -49,16 +49,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | |||
| 49 | </> | 49 | </> |
| 50 | ) : ( | 50 | ) : ( |
| 51 | <> | 51 | <> |
| 52 | <Link to="/" tabIndex={-1} className="login"> | 52 | <Link to="/" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]"> |
| 53 | <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"> |
| 54 | <img | 54 | <img |
| 55 | className="avatar-img" | 55 | className="rounded-[50px]" |
| 56 | src={profile.avatar_link} | 56 | src={profile.avatar_link} |
| 57 | alt="" | 57 | alt="" |
| 58 | /> | 58 | /> |
| 59 | <span>Loading Profile...</span> | 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> |
| 60 | </button> | 60 | </button> |
| 61 | <button disabled className="logout-button" onClick={_logout}> | 61 | <button disabled className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent hidden" onClick={_logout}> |
| 62 | <img src={ExitIcon} alt="" /> | 62 | <img src={ExitIcon} alt="" /> |
| 63 | <span /> | 63 | <span /> |
| 64 | </button> | 64 | </button> |
| @@ -67,11 +67,28 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { | |||
| 67 | )} | 67 | )} |
| 68 | </> | 68 | </> |
| 69 | ) : ( | 69 | ) : ( |
| 70 | <Link to="/api/v1/login" tabIndex={-1} className="login"> | 70 | <Link to="/api/v1/login" tabIndex={-1}> |
| 71 | <button className="sidebar-button" onClick={_login}> | 71 | <button |
| 72 | <img className="avatar-img" src={UserIcon} alt="" /> | 72 | className={`${ |
| 73 | <span> | 73 | isOpen |
| 74 | <img src={LoginIcon} alt="Sign in through Steam" /> | 74 | ? "grid grid-cols-[50px_auto] place-items-start pl-[11px]" |
| 75 | : "flex items-center justify-center" | ||
| 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 | )} | ||
| 75 | </span> | 92 | </span> |
| 76 | </button> | 93 | </button> |
| 77 | </Link> | 94 | </Link> |
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx index 618d1a7..a0d7eb7 100644 --- a/frontend/src/components/ModMenu.tsx +++ b/frontend/src/components/ModMenu.tsx | |||
| @@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom"; | |||
| 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"; | ||
| 9 | import useConfirm from "@hooks/UseConfirm"; | 8 | import useConfirm from "@hooks/UseConfirm"; |
| 10 | 9 | ||
| 11 | interface ModMenuProps { | 10 | interface ModMenuProps { |
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b55d56b..88a5297 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | import React, { useCallback } 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 { | 4 | import { |
| @@ -10,12 +10,11 @@ import { | |||
| 10 | PortalIcon, | 10 | PortalIcon, |
| 11 | SearchIcon, | 11 | SearchIcon, |
| 12 | UploadIcon, | 12 | UploadIcon, |
| 13 | } from "@images/Images"; | 13 | } from "../images/Images"; |
| 14 | import Login from "@components/Login"; | 14 | import Login from "@components/Login"; |
| 15 | import { UserProfile } from "@customTypes/Profile"; | 15 | import { UserProfile } from "@customTypes/Profile"; |
| 16 | import { Search } from "@customTypes/Search"; | 16 | import { Search } from "@customTypes/Search"; |
| 17 | import { API } from "@api/Api"; | 17 | import { API } from "@api/Api"; |
| 18 | import "@css/Sidebar.css"; | ||
| 19 | 18 | ||
| 20 | interface SidebarProps { | 19 | interface SidebarProps { |
| 21 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | 20 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; |
| @@ -24,6 +23,17 @@ interface SidebarProps { | |||
| 24 | onUploadRun: () => void; | 23 | onUploadRun: () => void; |
| 25 | } | 24 | } |
| 26 | 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 | } | ||
| 31 | |||
| 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 | } | ||
| 36 | |||
| 27 | const Sidebar: React.FC<SidebarProps> = ({ | 37 | const Sidebar: React.FC<SidebarProps> = ({ |
| 28 | setToken, | 38 | setToken, |
| 29 | profile, | 39 | profile, |
| @@ -34,100 +44,38 @@ const Sidebar: React.FC<SidebarProps> = ({ | |||
| 34 | undefined | 44 | undefined |
| 35 | ); | 45 | ); |
| 36 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | 46 | const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); |
| 37 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); | 47 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false); |
| 48 | const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1); | ||
| 38 | 49 | ||
| 39 | const location = useLocation(); | 50 | const location = useLocation(); |
| 40 | const path = location.pathname; | 51 | const path = location.pathname; |
| 41 | 52 | ||
| 42 | const _handle_sidebar_hide = useCallback(() => { | 53 | const sidebarRef = useRef<HTMLDivElement>(null); |
| 43 | var btn = document.querySelectorAll( | 54 | const searchbarRef = useRef<HTMLInputElement>(null); |
| 44 | "button.sidebar-button" | 55 | const uploadRunRef = useRef<HTMLButtonElement>(null); |
| 45 | ) as NodeListOf<HTMLElement>; | 56 | const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); |
| 46 | const span = document.querySelectorAll( | 57 | |
| 47 | "button.sidebar-button>span" | 58 | const _handle_sidebar_toggle = useCallback(() => { |
| 48 | ) as NodeListOf<HTMLElement>; | 59 | if (!sidebarRef.current) return; |
| 49 | const side = document.querySelector("#sidebar-list") as HTMLElement; | ||
| 50 | const searchbar = document.querySelector("#searchbar") as HTMLInputElement; | ||
| 51 | const uploadRunBtn = document.querySelector( | ||
| 52 | "#upload-run" | ||
| 53 | ) as HTMLInputElement; | ||
| 54 | const uploadRunSpan = document.querySelector( | ||
| 55 | "#upload-run>span" | ||
| 56 | ) as HTMLInputElement; | ||
| 57 | 60 | ||
| 58 | if (isSidebarOpen) { | 61 | if (isSidebarOpen) { |
| 59 | if (profile) { | ||
| 60 | const login = document.querySelectorAll( | ||
| 61 | ".login>button" | ||
| 62 | )[1] as HTMLElement; | ||
| 63 | login.style.opacity = "1"; | ||
| 64 | uploadRunBtn.style.width = "310px"; | ||
| 65 | uploadRunBtn.style.padding = "0.4em 0 0 11px"; | ||
| 66 | uploadRunSpan.style.opacity = "0"; | ||
| 67 | setTimeout(() => { | ||
| 68 | uploadRunSpan.style.opacity = "1"; | ||
| 69 | }, 100); | ||
| 70 | } | ||
| 71 | setSidebarOpen(false); | 62 | setSidebarOpen(false); |
| 72 | side.style.width = "320px"; | ||
| 73 | btn.forEach((e, i) => { | ||
| 74 | e.style.width = "310px"; | ||
| 75 | e.style.padding = "0.4em 0 0 11px"; | ||
| 76 | setTimeout(() => { | ||
| 77 | span[i].style.opacity = "1"; | ||
| 78 | }, 100); | ||
| 79 | }); | ||
| 80 | side.style.zIndex = "2"; | ||
| 81 | } else { | 63 | } else { |
| 82 | if (profile) { | ||
| 83 | const login = document.querySelectorAll( | ||
| 84 | ".login>button" | ||
| 85 | )[1] as HTMLElement; | ||
| 86 | login.style.opacity = "0"; | ||
| 87 | uploadRunBtn.style.width = "40px"; | ||
| 88 | uploadRunBtn.style.padding = "0.4em 0 0 5px"; | ||
| 89 | uploadRunSpan.style.opacity = "0"; | ||
| 90 | } | ||
| 91 | setSidebarOpen(true); | 64 | setSidebarOpen(true); |
| 92 | side.style.width = "40px"; | 65 | searchbarRef.current?.focus(); |
| 93 | searchbar.focus(); | ||
| 94 | btn.forEach((e, i) => { | ||
| 95 | e.style.width = "40px"; | ||
| 96 | e.style.padding = "0.4em 0 0 5px"; | ||
| 97 | span[i].style.opacity = "0"; | ||
| 98 | }); | ||
| 99 | setTimeout(() => { | ||
| 100 | side.style.zIndex = "0"; | ||
| 101 | }, 300); | ||
| 102 | } | 66 | } |
| 103 | }, [isSidebarOpen, profile]); | 67 | }, [isSidebarOpen]); |
| 104 | 68 | ||
| 105 | const handle_sidebar_click = useCallback( | 69 | const handle_sidebar_click = useCallback( |
| 106 | (clicked_sidebar_idx: number) => { | 70 | (clicked_sidebar_idx: number) => { |
| 107 | const btn = document.querySelectorAll("button.sidebar-button"); | 71 | setSelectedButtonIndex(clicked_sidebar_idx); |
| 108 | if (isSidebarOpen) { | 72 | if (isSidebarOpen) { |
| 109 | setSidebarOpen(false); | 73 | setSidebarOpen(false); |
| 110 | _handle_sidebar_hide(); | ||
| 111 | } | 74 | } |
| 112 | // clusterfuck | ||
| 113 | btn.forEach((e, i) => { | ||
| 114 | btn[i].classList.remove("sidebar-button-selected"); | ||
| 115 | btn[i].classList.add("sidebar-button-deselected"); | ||
| 116 | }); | ||
| 117 | btn[clicked_sidebar_idx].classList.add("sidebar-button-selected"); | ||
| 118 | btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected"); | ||
| 119 | }, | 75 | }, |
| 120 | [isSidebarOpen, _handle_sidebar_hide] | 76 | [isSidebarOpen] |
| 121 | ); | 77 | ); |
| 122 | 78 | ||
| 123 | const _handle_sidebar_lock = () => { | ||
| 124 | if (!isSidebarLocked) { | ||
| 125 | _handle_sidebar_hide(); | ||
| 126 | setIsSidebarLocked(true); | ||
| 127 | setTimeout(() => setIsSidebarLocked(false), 300); | ||
| 128 | } | ||
| 129 | }; | ||
| 130 | |||
| 131 | const _handle_search_change = async (q: string) => { | 79 | const _handle_search_change = async (q: string) => { |
| 132 | const searchResponse = await API.get_search(q); | 80 | const searchResponse = await API.get_search(q); |
| 133 | setSearchData(searchResponse); | 81 | setSearchData(searchResponse); |
| @@ -135,149 +83,199 @@ const Sidebar: React.FC<SidebarProps> = ({ | |||
| 135 | 83 | ||
| 136 | React.useEffect(() => { | 84 | React.useEffect(() => { |
| 137 | if (path === "/") { | 85 | if (path === "/") { |
| 138 | handle_sidebar_click(1); | 86 | setSelectedButtonIndex(1); |
| 139 | } else if (path.includes("games")) { | 87 | } else if (path.includes("games")) { |
| 140 | handle_sidebar_click(2); | 88 | setSelectedButtonIndex(2); |
| 141 | } else if (path.includes("rankings")) { | 89 | } else if (path.includes("rankings")) { |
| 142 | handle_sidebar_click(3); | 90 | setSelectedButtonIndex(3); |
| 143 | } | 91 | } else if (path.includes("profile")) { |
| 144 | // else if (path.includes("news")) { handle_sidebar_click(4) } | 92 | setSelectedButtonIndex(4); |
| 145 | // else if (path.includes("scorelog")) { handle_sidebar_click(5) } | ||
| 146 | else if (path.includes("profile")) { | ||
| 147 | handle_sidebar_click(4); | ||
| 148 | } else if (path.includes("rules")) { | 93 | } else if (path.includes("rules")) { |
| 149 | handle_sidebar_click(5); | 94 | setSelectedButtonIndex(5); |
| 150 | } else if (path.includes("about")) { | 95 | } else if (path.includes("about")) { |
| 151 | handle_sidebar_click(6); | 96 | setSelectedButtonIndex(6); |
| 152 | } | 97 | } |
| 153 | }, [path, handle_sidebar_click]); | 98 | }, [path]); |
| 99 | |||
| 100 | const getButtonClasses = (buttonIndex: number) => { | ||
| 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"; | ||
| 102 | const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground"; | ||
| 103 | |||
| 104 | return `${baseClasses} ${selectedClasses}`; | ||
| 105 | }; | ||
| 106 | |||
| 107 | const iconClasses = "w-6 h-6 flex-shrink-0"; | ||
| 154 | 108 | ||
| 155 | return ( | 109 | return ( |
| 156 | <div id="sidebar"> | 110 | <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${ |
| 157 | <Link to="/" tabIndex={-1}> | 111 | isSidebarOpen ? 'w-80' : 'w-20' |
| 158 | <div id="logo"> | 112 | }`}> |
| 159 | {" "} | 113 | <div className="flex items-center h-20 px-4 border-b border-border"> |
| 160 | {/* logo */} | 114 | <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0"> |
| 161 | <img src={LogoIcon} alt="" height={"80px"} /> | 115 | <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" /> |
| 162 | <div id="logo-text"> | 116 | {isSidebarOpen && ( |
| 163 | <span> | 117 | <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden"> |
| 164 | <b>PORTAL 2</b> | 118 | <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate"> |
| 165 | </span> | 119 | PORTAL 2 |
| 166 | <br /> | 120 | </div> |
| 167 | <span>Least Portals Hub</span> | 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> | ||
| 136 | |||
| 137 | {/* Sidebar Content */} | ||
| 138 | <div | ||
| 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 | /> | ||
| 158 | |||
| 159 | {searchData && ( | ||
| 160 | <div className="mt-2 max-h-40 overflow-y-auto min-w-0"> | ||
| 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> | ||
| 168 | </div> | 187 | </div> |
| 188 | )} | ||
| 189 | |||
| 190 | <div className="flex-1 p-4 min-w-0"> | ||
| 191 | <nav className="space-y-2"> | ||
| 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> | ||
| 169 | </div> | 231 | </div> |
| 170 | </Link> | ||
| 171 | <div id="sidebar-list"> | ||
| 172 | {" "} | ||
| 173 | {/* List */} | ||
| 174 | <div id="sidebar-toplist"> | ||
| 175 | {" "} | ||
| 176 | {/* Top */} | ||
| 177 | <button | ||
| 178 | className="sidebar-button" | ||
| 179 | onClick={() => _handle_sidebar_lock()} | ||
| 180 | > | ||
| 181 | <img src={SearchIcon} alt="" /> | ||
| 182 | <span>Search</span> | ||
| 183 | </button> | ||
| 184 | <span></span> | ||
| 185 | <Link to="/" tabIndex={-1}> | ||
| 186 | <button className="sidebar-button"> | ||
| 187 | <img src={HomeIcon} alt="homepage" /> | ||
| 188 | <span>Home Page</span> | ||
| 189 | </button> | ||
| 190 | </Link> | ||
| 191 | <Link to="/games" tabIndex={-1}> | ||
| 192 | <button className="sidebar-button"> | ||
| 193 | <img src={PortalIcon} alt="games" /> | ||
| 194 | <span>Games</span> | ||
| 195 | </button> | ||
| 196 | </Link> | ||
| 197 | <Link to="/rankings" tabIndex={-1}> | ||
| 198 | <button className="sidebar-button"> | ||
| 199 | <img src={FlagIcon} alt="rankings" /> | ||
| 200 | <span>Rankings</span> | ||
| 201 | </button> | ||
| 202 | </Link> | ||
| 203 | {/* <Link to="/news" tabIndex={-1}> | ||
| 204 | <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> | ||
| 205 | </Link> */} | ||
| 206 | {/* <Link to="/scorelog" tabIndex={-1}> | ||
| 207 | <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score Logs</span></button> | ||
| 208 | </Link> */} | ||
| 209 | </div> | ||
| 210 | <div id="sidebar-bottomlist"> | ||
| 211 | <span></span> | ||
| 212 | 232 | ||
| 213 | {profile && profile.profile ? ( | 233 | {/* Bottom Section */} |
| 234 | <div className="p-4 border-t border-border space-y-2 min-w-0"> | ||
| 235 | {profile && profile.profile && ( | ||
| 214 | <button | 236 | <button |
| 237 | ref={uploadRunRef} | ||
| 215 | id="upload-run" | 238 | id="upload-run" |
| 216 | className="submit-run-button" | 239 | className={getButtonClasses(-1)} |
| 217 | onClick={() => onUploadRun()} | 240 | onClick={() => onUploadRun()} |
| 218 | > | 241 | > |
| 219 | <img src={UploadIcon} alt="upload" /> | 242 | <img src={UploadIcon} alt="Upload" className={iconClasses} /> |
| 220 | <span>Upload Record</span> | 243 | {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>} |
| 221 | </button> | 244 | </button> |
| 222 | ) : ( | ||
| 223 | <span></span> | ||
| 224 | )} | 245 | )} |
| 225 | 246 | ||
| 226 | <Login | 247 | <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}> |
| 227 | setToken={setToken} | 248 | <Login |
| 228 | profile={profile} | 249 | setToken={setToken} |
| 229 | setProfile={setProfile} | 250 | profile={profile} |
| 230 | /> | 251 | setProfile={setProfile} |
| 252 | isOpen={isSidebarOpen} | ||
| 253 | /> | ||
| 254 | </div> | ||
| 231 | 255 | ||
| 232 | <Link to="/rules" tabIndex={-1}> | 256 | <Link to="/rules" tabIndex={-1}> |
| 233 | <button className="sidebar-button"> | 257 | <button |
| 234 | <img src={BookIcon} alt="rules" /> | 258 | ref={el => sidebarButtonRefs.current[5] = el} |
| 235 | <span>Leaderboard Rules</span> | 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>} | ||
| 236 | </button> | 264 | </button> |
| 237 | </Link> | 265 | </Link> |
| 238 | 266 | ||
| 239 | <Link to="/about" tabIndex={-1}> | 267 | <Link to="/about" tabIndex={-1}> |
| 240 | <button className="sidebar-button"> | 268 | <button |
| 241 | <img src={HelpIcon} alt="about" /> | 269 | ref={el => sidebarButtonRefs.current[6] = el} |
| 242 | <span>About LPHUB</span> | 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>} | ||
| 243 | </button> | 275 | </button> |
| 244 | </Link> | 276 | </Link> |
| 245 | </div> | 277 | </div> |
| 246 | </div> | 278 | </div> |
| 247 | <div> | ||
| 248 | <input | ||
| 249 | type="text" | ||
| 250 | id="searchbar" | ||
| 251 | placeholder="Search for map or a player..." | ||
| 252 | onChange={e => _handle_search_change(e.target.value)} | ||
| 253 | /> | ||
| 254 | |||
| 255 | <div id="search-data"> | ||
| 256 | {searchData?.maps.map((q, index) => ( | ||
| 257 | <Link to={`/maps/${q.id}`} className="search-map" key={index}> | ||
| 258 | <span>{q.game}</span> | ||
| 259 | <span>{q.chapter}</span> | ||
| 260 | <span>{q.map}</span> | ||
| 261 | </Link> | ||
| 262 | ))} | ||
| 263 | {searchData?.players.map((q, index) => ( | ||
| 264 | <Link | ||
| 265 | to={ | ||
| 266 | profile && q.steam_id === profile.steam_id | ||
| 267 | ? `/profile` | ||
| 268 | : `/users/${q.steam_id}` | ||
| 269 | } | ||
| 270 | className="search-player" | ||
| 271 | key={index} | ||
| 272 | > | ||
| 273 | <img src={q.avatar_link} alt="pfp"></img> | ||
| 274 | <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}> | ||
| 275 | {q.user_name} | ||
| 276 | </span> | ||
| 277 | </Link> | ||
| 278 | ))} | ||
| 279 | </div> | ||
| 280 | </div> | ||
| 281 | </div> | 279 | </div> |
| 282 | ); | 280 | ); |
| 283 | }; | 281 | }; |
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx index 61e52d4..cdecf30 100644 --- a/frontend/src/components/Summary.tsx +++ b/frontend/src/components/Summary.tsx | |||
| @@ -2,7 +2,6 @@ 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; |
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index d5eabcd..0034019 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -2,7 +2,6 @@ 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"; | ||
| 6 | import { Game } from "@customTypes/Game"; | 5 | import { Game } from "@customTypes/Game"; |
| 7 | import { API } from "@api/Api"; | 6 | import { API } from "@api/Api"; |
| 8 | import { useNavigate } from "react-router-dom"; | 7 | import { useNavigate } from "react-router-dom"; |
diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx index eb12588..6b46893 100644 --- a/frontend/src/images/Images.tsx +++ b/frontend/src/images/Images.tsx | |||
| @@ -1,5 +1,5 @@ | |||
| 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"; |
| @@ -23,7 +23,7 @@ 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; |
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/pages/About.tsx b/frontend/src/pages/About.tsx index 5a69bfe..a5bb291 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx | |||
| @@ -2,8 +2,6 @@ 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 | 4 | ||
| 5 | import "@css/About.css"; | ||
| 6 | |||
| 7 | const About: React.FC = () => { | 5 | const About: React.FC = () => { |
| 8 | const [aboutText, setAboutText] = React.useState<string>(""); | 6 | const [aboutText, setAboutText] = React.useState<string>(""); |
| 9 | 7 | ||
| @@ -26,7 +24,7 @@ const About: React.FC = () => { | |||
| 26 | }, []); | 24 | }, []); |
| 27 | 25 | ||
| 28 | return ( | 26 | return ( |
| 29 | <div id="about"> | 27 | <div className="p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none"> |
| 30 | <Helmet> | 28 | <Helmet> |
| 31 | <title>LPHUB | About</title> | 29 | <title>LPHUB | About</title> |
| 32 | </Helmet> | 30 | </Helmet> |
diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx index d7dacde..1ef0f57 100644 --- a/frontend/src/pages/Games.tsx +++ b/frontend/src/pages/Games.tsx | |||
| @@ -3,41 +3,23 @@ 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 | const _page_load = () => { | ||
| 14 | const loaders = document.querySelectorAll(".loader"); | ||
| 15 | loaders.forEach(loader => { | ||
| 16 | (loader as HTMLElement).style.display = "none"; | ||
| 17 | }); | ||
| 18 | }; | ||
| 19 | |||
| 20 | React.useEffect(() => { | ||
| 21 | document | ||
| 22 | .querySelectorAll(".games-page-item-body") | ||
| 23 | .forEach((game, index) => { | ||
| 24 | game.innerHTML = ""; | ||
| 25 | }); | ||
| 26 | _page_load(); | ||
| 27 | }, []); | ||
| 28 | |||
| 29 | return ( | 12 | return ( |
| 30 | <div className="games-page"> | 13 | <div className="ml-10 min-h-screen w-[calc(100%-320px)] text-foreground font-[--font-barlow-semicondensed-regular] overflow-y-auto scrollbar-thin"> |
| 31 | <Helmet> | 14 | <Helmet> |
| 32 | <title>LPHUB | Games</title> | 15 | <title>LPHUB | Games</title> |
| 33 | </Helmet> | 16 | </Helmet> |
| 34 | <section> | 17 | <section className="py-12 px-12 w-full"> |
| 35 | <div className="games-page-content"> | 18 | <h1 className="text-3xl font-bold mb-8">Games</h1> |
| 36 | <div className="games-page-item-content"> | 19 | <div className="flex flex-col w-full"> |
| 37 | {games.map((game, index) => ( | 20 | {games.map((game, index) => ( |
| 38 | <GameEntry game={game} key={index} /> | 21 | <GameEntry game={game} key={index} /> |
| 39 | ))} | 22 | ))} |
| 40 | </div> | ||
| 41 | </div> | 23 | </div> |
| 42 | </section> | 24 | </section> |
| 43 | </div> | 25 | </div> |
diff --git a/frontend/src/pages/Homepage.tsx b/frontend/src/pages/Homepage.tsx index f0c5821..2d16b8d 100644 --- a/frontend/src/pages/Homepage.tsx +++ b/frontend/src/pages/Homepage.tsx | |||
| @@ -3,23 +3,23 @@ import { Helmet } from "react-helmet"; | |||
| 3 | 3 | ||
| 4 | const Homepage: React.FC = () => { | 4 | const Homepage: React.FC = () => { |
| 5 | return ( | 5 | return ( |
| 6 | <main> | 6 | <main className="text-foreground font-[--font-barlow-semicondensed-regular]"> |
| 7 | <Helmet> | 7 | <Helmet> |
| 8 | <title>LPHUB | Homepage</title> | 8 | <title>LPHUB | Homepage</title> |
| 9 | </Helmet> | 9 | </Helmet> |
| 10 | <section> | 10 | <section className="p-8"> |
| 11 | <p /> | 11 | <p /> |
| 12 | <h1>Welcome to Least Portals Hub!</h1> | 12 | <h1 className="text-5xl font-[--font-barlow-condensed-bold] mb-6 text-primary">Welcome to Least Portals Hub!</h1> |
| 13 | <p> | 13 | <p className="text-lg mb-4 leading-relaxed"> |
| 14 | At the moment, LPHUB is in beta state. This means that the site has | 14 | At the moment, LPHUB is in beta state. This means that the site has |
| 15 | only the core functionalities enabled for providing both collaborative | 15 | only the core functionalities enabled for providing both collaborative |
| 16 | information and competitive leaderboards. | 16 | information and competitive leaderboards. |
| 17 | </p> | 17 | </p> |
| 18 | <p> | 18 | <p className="text-lg mb-4 leading-relaxed"> |
| 19 | The website should feel intuitive to navigate around. For any type of | 19 | The website should feel intuitive to navigate around. For any type of |
| 20 | feedback, reach us at LPHUB Discord server. | 20 | feedback, reach us at LPHUB Discord server. |
| 21 | </p> | 21 | </p> |
| 22 | <p> | 22 | <p className="text-lg mb-4 leading-relaxed"> |
| 23 | By using LPHUB, you agree that you have read the 'Leaderboard Rules' | 23 | By using LPHUB, you agree that you have read the 'Leaderboard Rules' |
| 24 | and the 'About LPHUB' pages. | 24 | and the 'About LPHUB' pages. |
| 25 | </p> | 25 | </p> |
diff --git a/frontend/src/pages/Maplist.tsx b/frontend/src/pages/Maplist.tsx index a7242ef..8343129 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"; |
| @@ -92,44 +91,52 @@ const Maplist: React.FC = () => { | |||
| 92 | <Helmet> | 91 | <Helmet> |
| 93 | <title>LPHUB | Maplist</title> | 92 | <title>LPHUB | Maplist</title> |
| 94 | </Helmet> | 93 | </Helmet> |
| 95 | <section style={{ marginTop: "20px" }}> | 94 | |
| 95 | <section className="mt-5"> | ||
| 96 | <Link to="/games"> | 96 | <Link to="/games"> |
| 97 | <button className="nav-button" style={{ borderRadius: "20px" }}> | 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"> |
| 98 | <i className="triangle"></i> | 98 | <i className="triangle mr-2"></i> |
| 99 | <span>Games List</span> | 99 | <span className="px-2">Games List</span> |
| 100 | </button> | 100 | </button> |
| 101 | </Link> | 101 | </Link> |
| 102 | </section> | 102 | </section> |
| 103 | |||
| 103 | {load ? ( | 104 | {load ? ( |
| 104 | <div></div> | 105 | <div></div> |
| 105 | ) : ( | 106 | ) : ( |
| 106 | <section> | 107 | <section> |
| 107 | <h1>{game?.name}</h1> | 108 | <h1 className="font-[--font-barlow-condensed-bold] text-6xl my-0 text-foreground"> |
| 109 | {game?.name} | ||
| 110 | </h1> | ||
| 111 | |||
| 108 | <div | 112 | <div |
| 113 | className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative" | ||
| 109 | style={{ backgroundImage: `url(${game?.image})` }} | 114 | style={{ backgroundImage: `url(${game?.image})` }} |
| 110 | className="game-header" | ||
| 111 | > | 115 | > |
| 112 | <div className="blur"> | 116 | <div className="backdrop-blur-sm flex flex-col w-full"> |
| 113 | <div className="game-header-portal-count"> | 117 | <div className="h-full flex flex-col justify-center items-center"> |
| 114 | <h2 className="portal-count"> | 118 | <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-8xl text-foreground"> |
| 115 | { | 119 | { |
| 116 | game?.category_portals.find( | 120 | game?.category_portals.find( |
| 117 | obj => obj.category.id === catNum + 1 | 121 | obj => obj.category.id === catNum + 1 |
| 118 | )?.portal_count | 122 | )?.portal_count |
| 119 | } | 123 | } |
| 120 | </h2> | 124 | </h2> |
| 121 | <h3>portals</h3> | 125 | <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-4xl my-0 text-foreground"> |
| 126 | portals | ||
| 127 | </h3> | ||
| 122 | </div> | 128 | </div> |
| 123 | <div className="game-header-categories"> | 129 | |
| 130 | <div className="flex h-12 bg-surface gap-0.5"> | ||
| 124 | {game?.category_portals.map((cat, index) => ( | 131 | {game?.category_portals.map((cat, index) => ( |
| 125 | <button | 132 | <button |
| 126 | key={index} | 133 | key={index} |
| 127 | className={ | 134 | className={`border-0 text-foreground font-[--font-barlow-semicondensed-regular] text-xl cursor-pointer transition-all duration-100 w-full ${ |
| 128 | currentlySelected === cat.category.id || | 135 | currentlySelected === cat.category.id || |
| 129 | (cat.category.id - 1 === catNum && !hasClicked) | 136 | (cat.category.id - 1 === catNum && !hasClicked) |
| 130 | ? "game-cat-button selected" | 137 | ? "bg-surface" |
| 131 | : "game-cat-button" | 138 | : "bg-surface1 hover:bg-surface" |
| 132 | } | 139 | }`} |
| 133 | onClick={() => { | 140 | onClick={() => { |
| 134 | setCatNum(cat.category.id - 1); | 141 | setCatNum(cat.category.id - 1); |
| 135 | _update_currently_selected(cat.category.id); | 142 | _update_currently_selected(cat.category.id); |
| @@ -143,31 +150,32 @@ const Maplist: React.FC = () => { | |||
| 143 | </div> | 150 | </div> |
| 144 | 151 | ||
| 145 | <div> | 152 | <div> |
| 146 | <section className="chapter-select-container"> | 153 | <section> |
| 147 | <div> | 154 | <div> |
| 148 | <span | 155 | <span className="text-lg translate-y-1.5 block mt-2.5 text-foreground"> |
| 149 | style={{ | ||
| 150 | fontSize: "18px", | ||
| 151 | transform: "translateY(5px)", | ||
| 152 | display: "block", | ||
| 153 | marginTop: "10px", | ||
| 154 | }} | ||
| 155 | > | ||
| 156 | {curChapter?.chapter.name.split(" - ")[0]} | 156 | {curChapter?.chapter.name.split(" - ")[0]} |
| 157 | </span> | 157 | </span> |
| 158 | </div> | 158 | </div> |
| 159 | <div onClick={_handle_dropdown_click} className="dropdown"> | 159 | <div |
| 160 | <span>{curChapter?.chapter.name.split(" - ")[1]}</span> | 160 | onClick={_handle_dropdown_click} |
| 161 | <i className="triangle"></i> | 161 | className="cursor-pointer select-none flex w-fit items-center" |
| 162 | > | ||
| 163 | <span className="text-foreground text-2xl"> | ||
| 164 | {curChapter?.chapter.name.split(" - ")[1]} | ||
| 165 | </span> | ||
| 166 | <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i> | ||
| 162 | </div> | 167 | </div> |
| 168 | \ | ||
| 163 | <div | 169 | <div |
| 164 | className="dropdown-elements" | 170 | className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${ |
| 165 | style={{ display: dropdownActive }} | 171 | dropdownActive === "none" ? "hidden" : "block" |
| 172 | }`} | ||
| 166 | > | 173 | > |
| 167 | {gameChapters?.chapters.map((chapter, i) => { | 174 | {gameChapters?.chapters.map((chapter, i) => { |
| 168 | return ( | 175 | return ( |
| 169 | <div | 176 | <div |
| 170 | className="dropdown-element" | 177 | key={i} |
| 178 | className="cursor-pointer text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground" | ||
| 171 | onClick={() => { | 179 | onClick={() => { |
| 172 | _fetch_chapters(chapter.id.toString()); | 180 | _fetch_chapters(chapter.id.toString()); |
| 173 | _handle_dropdown_click(); | 181 | _handle_dropdown_click(); |
| @@ -179,49 +187,52 @@ const Maplist: React.FC = () => { | |||
| 179 | })} | 187 | })} |
| 180 | </div> | 188 | </div> |
| 181 | </section> | 189 | </section> |
| 182 | <section className="maplist"> | 190 | |
| 191 | <section className="grid grid-cols-4 gap-5 my-5"> | ||
| 183 | {curChapter?.maps.map((map, i) => { | 192 | {curChapter?.maps.map((map, i) => { |
| 184 | return ( | 193 | return ( |
| 185 | <div className="maplist-entry"> | 194 | <div key={i} className="bg-surface rounded-3xl overflow-hidden"> |
| 186 | <Link to={`/maps/${map.id}`}> | 195 | <Link to={`/maps/${map.id}`}> |
| 187 | <span>{map.name}</span> | 196 | <span className="text-center text-xl w-full block my-1.5 text-foreground"> |
| 197 | {map.name} | ||
| 198 | </span> | ||
| 188 | <div | 199 | <div |
| 189 | className="map-entry-image" | 200 | className="flex h-48 bg-cover relative" |
| 190 | style={{ backgroundImage: `url(${map.image})` }} | 201 | style={{ backgroundImage: `url(${map.image})` }} |
| 191 | > | 202 | > |
| 192 | <div className="blur map"> | 203 | <div className="backdrop-blur-sm w-full flex items-center justify-center"> |
| 193 | <span> | 204 | <span className="text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5"> |
| 194 | {map.is_disabled | 205 | {map.is_disabled |
| 195 | ? map.category_portals[0].portal_count | 206 | ? map.category_portals[0].portal_count |
| 196 | : map.category_portals.find( | 207 | : map.category_portals.find( |
| 197 | obj => obj.category.id === catNum + 1 | 208 | obj => obj.category.id === catNum + 1 |
| 198 | )?.portal_count} | 209 | )?.portal_count} |
| 199 | </span> | 210 | </span> |
| 200 | <span>portals</span> | 211 | <span className="text-4xl font-[--font-barlow-semicondensed-regular] text-white"> |
| 212 | portals | ||
| 213 | </span> | ||
| 201 | </div> | 214 | </div> |
| 202 | </div> | 215 | </div> |
| 203 | <div className="difficulty-bar"> | 216 | |
| 204 | {/* <span>Difficulty:</span> */} | 217 | {/* Difficulty rating */} |
| 205 | <div | 218 | <div className="flex mx-2.5 my-4"> |
| 206 | className={ | 219 | <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px"> |
| 207 | map.difficulty === 0 | 220 | {[1, 2, 3, 4, 5].map((point) => ( |
| 208 | ? "one" | 221 | <div |
| 209 | : map.difficulty === 1 | 222 | key={point} |
| 210 | ? "two" | 223 | className={`flex h-0.5 w-full rounded-3xl ${ |
| 211 | : map.difficulty === 2 | 224 | point <= (map.difficulty + 1) |
| 212 | ? "three" | 225 | ? map.difficulty === 0 |
| 213 | : map.difficulty === 3 | 226 | ? "bg-green-500" |
| 214 | ? "four" | 227 | : map.difficulty === 1 || map.difficulty === 2 |
| 215 | : map.difficulty === 4 | 228 | ? "bg-lime-500" |
| 216 | ? "five" | 229 | : map.difficulty === 3 |
| 217 | : "one" | 230 | ? "bg-red-400" |
| 218 | } | 231 | : "bg-red-600" |
| 219 | > | 232 | : "bg-surface1" |
| 220 | <div className="difficulty-point"></div> | 233 | }`} |
| 221 | <div className="difficulty-point"></div> | 234 | /> |
| 222 | <div className="difficulty-point"></div> | 235 | ))} |
| 223 | <div className="difficulty-point"></div> | ||
| 224 | <div className="difficulty-point"></div> | ||
| 225 | </div> | 236 | </div> |
| 226 | </div> | 237 | </div> |
| 227 | </Link> | 238 | </Link> |
diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx index fbdb8f3..75753ac 100644 --- a/frontend/src/pages/Maps.tsx +++ b/frontend/src/pages/Maps.tsx | |||
| @@ -2,14 +2,13 @@ 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; |
| @@ -82,15 +81,15 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 82 | 81 | ||
| 83 | <section id="section2" className="summary1"> | 82 | <section id="section2" className="summary1"> |
| 84 | <button className="nav-button"> | 83 | <button className="nav-button"> |
| 85 | <img src={PortalIcon} alt="" /> | 84 | <img src={PortalIcon} alt="" className="w-6 h-6" /> |
| 86 | <span>Summary</span> | 85 | <span>Summary</span> |
| 87 | </button> | 86 | </button> |
| 88 | <button className="nav-button"> | 87 | <button className="nav-button"> |
| 89 | <img src={FlagIcon} alt="" /> | 88 | <img src={FlagIcon} alt="" className="w-6 h-6" /> |
| 90 | <span>Leaderboards</span> | 89 | <span>Leaderboards</span> |
| 91 | </button> | 90 | </button> |
| 92 | <button className="nav-button"> | 91 | <button className="nav-button"> |
| 93 | <img src={ChatIcon} alt="" /> | 92 | <img src={ChatIcon} alt="" className="w-6 h-6" /> |
| 94 | <span>Discussions</span> | 93 | <span>Discussions</span> |
| 95 | </button> | 94 | </button> |
| 96 | </section> | 95 | </section> |
| @@ -151,15 +150,15 @@ const Maps: React.FC<MapProps> = ({ token, isModerator }) => { | |||
| 151 | 150 | ||
| 152 | <section id="section2" className="summary1"> | 151 | <section id="section2" className="summary1"> |
| 153 | <button className="nav-button" onClick={() => setNavState(0)}> | 152 | <button className="nav-button" onClick={() => setNavState(0)}> |
| 154 | <img src={PortalIcon} alt="" /> | 153 | <img src={PortalIcon} alt="" className="w-6 h-6" /> |
| 155 | <span>Summary</span> | 154 | <span>Summary</span> |
| 156 | </button> | 155 | </button> |
| 157 | <button className="nav-button" onClick={() => setNavState(1)}> | 156 | <button className="nav-button" onClick={() => setNavState(1)}> |
| 158 | <img src={FlagIcon} alt="" /> | 157 | <img src={FlagIcon} alt="" className="w-6 h-6" /> |
| 159 | <span>Leaderboards</span> | 158 | <span>Leaderboards</span> |
| 160 | </button> | 159 | </button> |
| 161 | <button className="nav-button" onClick={() => setNavState(2)}> | 160 | <button className="nav-button" onClick={() => setNavState(2)}> |
| 162 | <img src={ChatIcon} alt="" /> | 161 | <img src={ChatIcon} alt="" className="w-6 h-6" /> |
| 163 | <span>Discussions</span> | 162 | <span>Discussions</span> |
| 164 | </button> | 163 | </button> |
| 165 | </section> | 164 | </section> |
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index e2d6000..f44f587 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx | |||
| @@ -19,7 +19,6 @@ import { UserProfile } from "@customTypes/Profile"; | |||
| 19 | import { Game, GameChapters } from "@customTypes/Game"; | 19 | import { Game, GameChapters } from "@customTypes/Game"; |
| 20 | import { Map } from "@customTypes/Map"; | 20 | import { Map } from "@customTypes/Map"; |
| 21 | import { ticks_to_time } from "@utils/Time"; | 21 | import { ticks_to_time } from "@utils/Time"; |
| 22 | import "@css/Profile.css"; | ||
| 23 | import { API } from "@api/Api"; | 22 | import { API } from "@api/Api"; |
| 24 | import useConfirm from "@hooks/UseConfirm"; | 23 | import useConfirm from "@hooks/UseConfirm"; |
| 25 | import useMessage from "@hooks/UseMessage"; | 24 | import useMessage from "@hooks/UseMessage"; |
diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx index 91027a0..7cdc08b 100644 --- a/frontend/src/pages/Rules.tsx +++ b/frontend/src/pages/Rules.tsx | |||
| @@ -2,8 +2,6 @@ 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 | 4 | ||
| 5 | import "@css/Rules.css"; | ||
| 6 | |||
| 7 | const Rules: React.FC = () => { | 5 | const Rules: React.FC = () => { |
| 8 | const [rulesText, setRulesText] = React.useState<string>(""); | 6 | const [rulesText, setRulesText] = React.useState<string>(""); |
| 9 | 7 | ||
| @@ -27,7 +25,7 @@ const Rules: React.FC = () => { | |||
| 27 | }, []); | 25 | }, []); |
| 28 | 26 | ||
| 29 | return ( | 27 | return ( |
| 30 | <main> | 28 | <main className="p-8 text-foreground font-[--font-barlow-semicondensed-regular] prose prose-invert max-w-none"> |
| 31 | <Helmet> | 29 | <Helmet> |
| 32 | <title>LPHUB | Rules</title> | 30 | <title>LPHUB | Rules</title> |
| 33 | </Helmet> | 31 | </Helmet> |
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx index 0198034..4b8a456 100644 --- a/frontend/src/pages/User.tsx +++ b/frontend/src/pages/User.tsx | |||
| @@ -19,7 +19,6 @@ import { Game, GameChapters } from "@customTypes/Game"; | |||
| 19 | import { Map } from "@customTypes/Map"; | 19 | import { Map } from "@customTypes/Map"; |
| 20 | import { API } from "@api/Api"; | 20 | import { API } from "@api/Api"; |
| 21 | import { ticks_to_time } from "@utils/Time"; | 21 | import { ticks_to_time } from "@utils/Time"; |
| 22 | import "@css/Profile.css"; | ||
| 23 | import useMessage from "@hooks/UseMessage"; | 22 | import useMessage from "@hooks/UseMessage"; |
| 24 | 23 | ||
| 25 | interface UserProps { | 24 | interface UserProps { |