diff options
| author | Wolfboy248 <georgejvindkarlsen@gmail.com> | 2025-08-21 10:33:27 +0200 |
|---|---|---|
| committer | Wolfboy248 <georgejvindkarlsen@gmail.com> | 2025-08-21 10:33:27 +0200 |
| commit | da1fd74f9387149b2b94d62853587a8afdb74ddd (patch) | |
| tree | 57f13021890b6d27848a3379d0869790fd1d7c97 | |
| parent | organised pages, started work on theme (diff) | |
| download | lphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.tar.gz lphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.tar.bz2 lphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.zip | |
Reorganised Maplist and Sidebar
| -rw-r--r-- | frontend/src/App.css | 1 | ||||
| -rw-r--r-- | frontend/src/App.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 288 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Content.tsx | 133 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Footer.tsx | 80 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Header.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Sidebar.module.css | 23 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Sidebar.tsx | 101 | ||||
| -rw-r--r-- | frontend/src/pages/Maplist/Components/Map.tsx | 60 | ||||
| -rw-r--r-- | frontend/src/pages/Maplist/Maplist.tsx | 81 |
10 files changed, 445 insertions, 350 deletions
diff --git a/frontend/src/App.css b/frontend/src/App.css index a39dcf1..3e0d813 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | --color-main: #141520; | 6 | --color-main: #141520; |
| 7 | --color-panel: #202232; | 7 | --color-panel: #202232; |
| 8 | --color-block: #2b2e46; | 8 | --color-block: #2b2e46; |
| 9 | --color-bright: #333753; | ||
| 9 | 10 | ||
| 10 | --color-white: #cdcfdf; | 11 | --color-white: #cdcfdf; |
| 11 | 12 | ||
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a95e77..5d0c8eb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx | |||
| @@ -3,7 +3,7 @@ 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/Sidebar"; |
| 7 | import "./App.css"; | 7 | import "./App.css"; |
| 8 | 8 | ||
| 9 | import Profile from "@pages/Profile/Profile.tsx"; | 9 | import Profile from "@pages/Profile/Profile.tsx"; |
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx deleted file mode 100644 index 0083a3e..0000000 --- a/frontend/src/components/Sidebar.tsx +++ /dev/null | |||
| @@ -1,288 +0,0 @@ | |||
| 1 | import React, { useCallback, useRef } from "react"; | ||
| 2 | import { Link, useLocation } from "react-router-dom"; | ||
| 3 | |||
| 4 | import { | ||
| 5 | BookIcon, | ||
| 6 | FlagIcon, | ||
| 7 | HelpIcon, | ||
| 8 | HomeIcon, | ||
| 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"; | ||
| 18 | |||
| 19 | interface SidebarProps { | ||
| 20 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| 21 | profile?: UserProfile; | ||
| 22 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | ||
| 23 | onUploadRun: () => void; | ||
| 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 | } | ||
| 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 | |||
| 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 | ); | ||
| 46 | // const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | ||
| 47 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false); | ||
| 48 | const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1); | ||
| 49 | |||
| 50 | const location = useLocation(); | ||
| 51 | const path = location.pathname; | ||
| 52 | |||
| 53 | const sidebarRef = useRef<HTMLDivElement>(null); | ||
| 54 | const searchbarRef = useRef<HTMLInputElement>(null); | ||
| 55 | const uploadRunRef = useRef<HTMLButtonElement>(null); | ||
| 56 | const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); | ||
| 57 | |||
| 58 | const _handle_sidebar_toggle = useCallback(() => { | ||
| 59 | if (!sidebarRef.current) return; | ||
| 60 | |||
| 61 | if (isSidebarOpen) { | ||
| 62 | setSidebarOpen(false); | ||
| 63 | } else { | ||
| 64 | setSidebarOpen(true); | ||
| 65 | searchbarRef.current?.focus(); | ||
| 66 | } | ||
| 67 | }, [isSidebarOpen]); | ||
| 68 | |||
| 69 | const handle_sidebar_click = useCallback( | ||
| 70 | (clicked_sidebar_idx: number) => { | ||
| 71 | setSelectedButtonIndex(clicked_sidebar_idx); | ||
| 72 | if (isSidebarOpen) { | ||
| 73 | setSidebarOpen(false); | ||
| 74 | } | ||
| 75 | }, | ||
| 76 | [isSidebarOpen] | ||
| 77 | ); | ||
| 78 | |||
| 79 | const _handle_search_change = async (q: string) => { | ||
| 80 | const searchResponse = await API.get_search(q); | ||
| 81 | setSearchData(searchResponse); | ||
| 82 | }; | ||
| 83 | |||
| 84 | React.useEffect(() => { | ||
| 85 | if (path === "/") { | ||
| 86 | setSelectedButtonIndex(1); | ||
| 87 | } else if (path.includes("games")) { | ||
| 88 | setSelectedButtonIndex(2); | ||
| 89 | } else if (path.includes("rankings")) { | ||
| 90 | setSelectedButtonIndex(3); | ||
| 91 | } else if (path.includes("profile")) { | ||
| 92 | setSelectedButtonIndex(4); | ||
| 93 | } else if (path.includes("rules")) { | ||
| 94 | setSelectedButtonIndex(5); | ||
| 95 | } else if (path.includes("about")) { | ||
| 96 | setSelectedButtonIndex(6); | ||
| 97 | } | ||
| 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"; | ||
| 108 | |||
| 109 | return ( | ||
| 110 | <div className={`w-80 not-md:w-full text-white bg-block | ||
| 111 | }`}> | ||
| 112 | <div className="flex items-center px-4 border-b border-border"> | ||
| 113 | <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0"> | ||
| 114 | <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" /> | ||
| 115 | {isSidebarOpen && ( | ||
| 116 | <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden"> | ||
| 117 | <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate"> | ||
| 118 | PORTAL 2 | ||
| 119 | </div> | ||
| 120 | <div className="text-sm leading-4 truncate"> | ||
| 121 | Least Portals Hub | ||
| 122 | </div> | ||
| 123 | </div> | ||
| 124 | )} | ||
| 125 | </Link> | ||
| 126 | |||
| 127 | <button | ||
| 128 | onClick={_handle_sidebar_toggle} | ||
| 129 | className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground" | ||
| 130 | title={isSidebarOpen ? "Close sidebar" : "Open sidebar"} | ||
| 131 | > | ||
| 132 | {isSidebarOpen ? <ClosedSidebarIcon /> : <OpenSidebarIcon />} | ||
| 133 | </button> | ||
| 134 | </div> | ||
| 135 | |||
| 136 | {/* Sidebar Content */} | ||
| 137 | <div | ||
| 138 | ref={sidebarRef} | ||
| 139 | className="flex flex-col overflow-y-auto overflow-x-hidden" | ||
| 140 | > | ||
| 141 | {isSidebarOpen && ( | ||
| 142 | <div className="p-4 border-b border-border min-w-0"> | ||
| 143 | <div className="flex items-center gap-3 mb-3"> | ||
| 144 | <img src={SearchIcon} alt="Search" className={iconClasses} /> | ||
| 145 | <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span> | ||
| 146 | </div> | ||
| 147 | |||
| 148 | <div className="min-w-0"> | ||
| 149 | <input | ||
| 150 | ref={searchbarRef} | ||
| 151 | type="text" | ||
| 152 | id="searchbar" | ||
| 153 | placeholder="Search for map or a player..." | ||
| 154 | onChange={e => _handle_search_change(e.target.value)} | ||
| 155 | className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0" | ||
| 156 | /> | ||
| 157 | |||
| 158 | {searchData && ( | ||
| 159 | <div className="mt-2 max-h-40 overflow-y-auto min-w-0"> | ||
| 160 | {searchData?.maps.map((q, index) => ( | ||
| 161 | <Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}> | ||
| 162 | <span className="block text-xs text-subtext1 truncate">{q.game}</span> | ||
| 163 | <span className="block text-xs text-subtext1 truncate">{q.chapter}</span> | ||
| 164 | <span className="block text-sm text-foreground truncate">{q.map}</span> | ||
| 165 | </Link> | ||
| 166 | ))} | ||
| 167 | {searchData?.players.map((q, index) => ( | ||
| 168 | <Link | ||
| 169 | to={ | ||
| 170 | profile && q.steam_id === profile.steam_id | ||
| 171 | ? `/profile` | ||
| 172 | : `/users/${q.steam_id}` | ||
| 173 | } | ||
| 174 | className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" | ||
| 175 | key={index} | ||
| 176 | > | ||
| 177 | <img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" /> | ||
| 178 | <span className="text-sm text-foreground truncate"> | ||
| 179 | {q.user_name} | ||
| 180 | </span> | ||
| 181 | </Link> | ||
| 182 | ))} | ||
| 183 | </div> | ||
| 184 | )} | ||
| 185 | </div> | ||
| 186 | </div> | ||
| 187 | )} | ||
| 188 | |||
| 189 | <div className="flex-1 p-4 min-w-0"> | ||
| 190 | <nav className="space-y-2"> | ||
| 191 | {[ | ||
| 192 | { | ||
| 193 | to: "/", | ||
| 194 | refIndex: 1, | ||
| 195 | icon: HomeIcon, | ||
| 196 | alt: "Home", | ||
| 197 | label: "Home Page", | ||
| 198 | }, | ||
| 199 | { | ||
| 200 | to: "/games", | ||
| 201 | refIndex: 2, | ||
| 202 | icon: PortalIcon, | ||
| 203 | alt: "Games", | ||
| 204 | label: "Games", | ||
| 205 | }, | ||
| 206 | { | ||
| 207 | to: "/rankings", | ||
| 208 | refIndex: 3, | ||
| 209 | icon: FlagIcon, | ||
| 210 | alt: "Rankings", | ||
| 211 | label: "Rankings", | ||
| 212 | }, | ||
| 213 | ].map(({ to, refIndex, icon, alt, label }) => ( | ||
| 214 | <Link to={to} tabIndex={-1} key={refIndex}> | ||
| 215 | <button | ||
| 216 | ref={el => { | ||
| 217 | sidebarButtonRefs.current[refIndex] = el | ||
| 218 | }} | ||
| 219 | className={getButtonClasses(refIndex)} | ||
| 220 | onClick={() => handle_sidebar_click(refIndex)} | ||
| 221 | > | ||
| 222 | <img src={icon} alt={alt} className={iconClasses} /> | ||
| 223 | {isSidebarOpen && ( | ||
| 224 | <span className="text-white font-[--font-barlow-semicondensed-regular] truncate"> | ||
| 225 | {label} | ||
| 226 | </span> | ||
| 227 | )} | ||
| 228 | </button> | ||
| 229 | </Link> | ||
| 230 | ))} | ||
| 231 | </nav> | ||
| 232 | </div> | ||
| 233 | |||
| 234 | {/* Bottom Section */} | ||
| 235 | <div className="p-4 border-t border-border space-y-2 min-w-0"> | ||
| 236 | {profile && profile.profile && ( | ||
| 237 | <button | ||
| 238 | ref={uploadRunRef} | ||
| 239 | id="upload-run" | ||
| 240 | className={getButtonClasses(-1)} | ||
| 241 | onClick={() => onUploadRun()} | ||
| 242 | > | ||
| 243 | <img src={UploadIcon} alt="Upload" className={iconClasses} /> | ||
| 244 | {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>} | ||
| 245 | </button> | ||
| 246 | )} | ||
| 247 | |||
| 248 | <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}> | ||
| 249 | <Login | ||
| 250 | setToken={setToken} | ||
| 251 | profile={profile} | ||
| 252 | setProfile={setProfile} | ||
| 253 | isOpen={isSidebarOpen} | ||
| 254 | /> | ||
| 255 | </div> | ||
| 256 | |||
| 257 | <Link to="/rules" tabIndex={-1}> | ||
| 258 | <button | ||
| 259 | ref={el => { | ||
| 260 | sidebarButtonRefs.current[5] = el | ||
| 261 | }} | ||
| 262 | className={getButtonClasses(5)} | ||
| 263 | onClick={() => handle_sidebar_click(5)} | ||
| 264 | > | ||
| 265 | <img src={BookIcon} alt="Rules" className={iconClasses} /> | ||
| 266 | {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>} | ||
| 267 | </button> | ||
| 268 | </Link> | ||
| 269 | |||
| 270 | <Link to="/about" tabIndex={-1}> | ||
| 271 | <button | ||
| 272 | ref={el => { | ||
| 273 | sidebarButtonRefs.current[6] = el | ||
| 274 | }} | ||
| 275 | className={getButtonClasses(6)} | ||
| 276 | onClick={() => handle_sidebar_click(6)} | ||
| 277 | > | ||
| 278 | <img src={HelpIcon} alt="About" className={iconClasses} /> | ||
| 279 | {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>} | ||
| 280 | </button> | ||
| 281 | </Link> | ||
| 282 | </div> | ||
| 283 | </div> | ||
| 284 | </div> | ||
| 285 | ); | ||
| 286 | }; | ||
| 287 | |||
| 288 | export default Sidebar; | ||
diff --git a/frontend/src/components/Sidebar/Content.tsx b/frontend/src/components/Sidebar/Content.tsx new file mode 100644 index 0000000..4051b08 --- /dev/null +++ b/frontend/src/components/Sidebar/Content.tsx | |||
| @@ -0,0 +1,133 @@ | |||
| 1 | import React, { useRef } from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | import { UserProfile } from "@customTypes/Profile"; | ||
| 4 | import { Search } from "@customTypes/Search"; | ||
| 5 | import { API } from "@api/Api"; | ||
| 6 | |||
| 7 | import styles from "./Sidebar.module.css"; | ||
| 8 | |||
| 9 | import { | ||
| 10 | FlagIcon, | ||
| 11 | HomeIcon, | ||
| 12 | PortalIcon, | ||
| 13 | SearchIcon, | ||
| 14 | } from "../../images/Images"; | ||
| 15 | |||
| 16 | interface ContentProps { | ||
| 17 | profile?: UserProfile; | ||
| 18 | isSidebarOpen: boolean; | ||
| 19 | sidebarButtonRefs: React.RefObject<(HTMLButtonElement | null)[]>; | ||
| 20 | getButtonClasses: (buttonIndex: number) => string; | ||
| 21 | handle_sidebar_click: (clicked_sidebar_idx: number) => void; | ||
| 22 | }; | ||
| 23 | |||
| 24 | const Content: React.FC<ContentProps> = ({ profile, isSidebarOpen, sidebarButtonRefs, getButtonClasses, handle_sidebar_click }) => { | ||
| 25 | const [searchData, setSearchData] = React.useState<Search | undefined>( | ||
| 26 | undefined | ||
| 27 | ); | ||
| 28 | |||
| 29 | const searchbarRef = useRef<HTMLInputElement>(null); | ||
| 30 | |||
| 31 | const _handle_search_change = async (q: string) => { | ||
| 32 | const searchResponse = await API.get_search(q); | ||
| 33 | setSearchData(searchResponse); | ||
| 34 | }; | ||
| 35 | |||
| 36 | const iconClasses = ""; | ||
| 37 | |||
| 38 | return ( | ||
| 39 | <div className="h-full"> | ||
| 40 | |||
| 41 | <div className="px-2"> | ||
| 42 | <div className={`${styles.button}`}> | ||
| 43 | <img src={SearchIcon} alt="Search" className={iconClasses} /> | ||
| 44 | <span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span> | ||
| 45 | </div> | ||
| 46 | |||
| 47 | <div className="min-w-0"> | ||
| 48 | <input | ||
| 49 | ref={searchbarRef} | ||
| 50 | type="text" | ||
| 51 | id="searchbar" | ||
| 52 | placeholder="Search for map or a player..." | ||
| 53 | onChange={e => _handle_search_change(e.target.value)} | ||
| 54 | className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0" | ||
| 55 | /> | ||
| 56 | |||
| 57 | {searchData && ( | ||
| 58 | <div className="mt-2 max-h-40 overflow-y-auto min-w-0"> | ||
| 59 | {searchData?.maps.map((q, index) => ( | ||
| 60 | <Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}> | ||
| 61 | <span className="block text-xs text-subtext1 truncate">{q.game}</span> | ||
| 62 | <span className="block text-xs text-subtext1 truncate">{q.chapter}</span> | ||
| 63 | <span className="block text-sm text-foreground truncate">{q.map}</span> | ||
| 64 | </Link> | ||
| 65 | ))} | ||
| 66 | {searchData?.players.map((q, index) => ( | ||
| 67 | <Link | ||
| 68 | to={ | ||
| 69 | profile && q.steam_id === profile.steam_id | ||
| 70 | ? `/profile` | ||
| 71 | : `/users/${q.steam_id}` | ||
| 72 | } | ||
| 73 | className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" | ||
| 74 | key={index} | ||
| 75 | > | ||
| 76 | <img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" /> | ||
| 77 | <span className="text-sm text-foreground truncate"> | ||
| 78 | {q.user_name} | ||
| 79 | </span> | ||
| 80 | </Link> | ||
| 81 | ))} | ||
| 82 | </div> | ||
| 83 | )} | ||
| 84 | </div> | ||
| 85 | </div> | ||
| 86 | |||
| 87 | <div className="flex-1 min-w-0"> | ||
| 88 | <nav className="px-2 flex flex-col gap-2"> | ||
| 89 | {[ | ||
| 90 | { | ||
| 91 | to: "/", | ||
| 92 | refIndex: 1, | ||
| 93 | icon: HomeIcon, | ||
| 94 | alt: "Home", | ||
| 95 | label: "Home Page", | ||
| 96 | }, | ||
| 97 | { | ||
| 98 | to: "/games", | ||
| 99 | refIndex: 2, | ||
| 100 | icon: PortalIcon, | ||
| 101 | alt: "Games", | ||
| 102 | label: "Games", | ||
| 103 | }, | ||
| 104 | { | ||
| 105 | to: "/rankings", | ||
| 106 | refIndex: 3, | ||
| 107 | icon: FlagIcon, | ||
| 108 | alt: "Rankings", | ||
| 109 | label: "Rankings", | ||
| 110 | }, | ||
| 111 | ].map(({ to, refIndex, icon, alt, label }) => ( | ||
| 112 | <Link to={to} tabIndex={-1} key={refIndex}> | ||
| 113 | <button | ||
| 114 | ref={el => { | ||
| 115 | sidebarButtonRefs.current[refIndex] = el | ||
| 116 | }} | ||
| 117 | className={`${styles.button}`} | ||
| 118 | onClick={() => handle_sidebar_click(refIndex)} | ||
| 119 | > | ||
| 120 | <img src={icon} alt={alt} className={iconClasses} /> | ||
| 121 | <span className=""> | ||
| 122 | {label} | ||
| 123 | </span> | ||
| 124 | </button> | ||
| 125 | </Link> | ||
| 126 | ))} | ||
| 127 | </nav> | ||
| 128 | </div> | ||
| 129 | </div> | ||
| 130 | ); | ||
| 131 | } | ||
| 132 | |||
| 133 | export default Content; | ||
diff --git a/frontend/src/components/Sidebar/Footer.tsx b/frontend/src/components/Sidebar/Footer.tsx new file mode 100644 index 0000000..070301a --- /dev/null +++ b/frontend/src/components/Sidebar/Footer.tsx | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | import React, { useRef } from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import styles from "./Sidebar.module.css"; | ||
| 5 | |||
| 6 | import { UserProfile } from "@customTypes/Profile"; | ||
| 7 | import Login from "@components/Login"; | ||
| 8 | |||
| 9 | import { | ||
| 10 | UploadIcon, | ||
| 11 | BookIcon, | ||
| 12 | HelpIcon, | ||
| 13 | } from "../../images/Images"; | ||
| 14 | |||
| 15 | interface FooterProps { | ||
| 16 | profile?: UserProfile; | ||
| 17 | onUploadRun: () => void; | ||
| 18 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | ||
| 19 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| 20 | sidebarButtonRefs: React.RefObject<(HTMLButtonElement | null)[]>; | ||
| 21 | getButtonClasses: (buttonIndex: number) => string; | ||
| 22 | handle_sidebar_click: (clicked_sidebar_idx: number) => void; | ||
| 23 | }; | ||
| 24 | |||
| 25 | const Footer: React.FC<FooterProps> = ({ profile, onUploadRun, setToken, setProfile, sidebarButtonRefs, getButtonClasses, handle_sidebar_click }) => { | ||
| 26 | const uploadRunRef = useRef<HTMLButtonElement>(null); | ||
| 27 | |||
| 28 | return ( | ||
| 29 | <div className=""> | ||
| 30 | {profile && profile.profile && ( | ||
| 31 | <button | ||
| 32 | ref={uploadRunRef} | ||
| 33 | id="upload-run" | ||
| 34 | className={getButtonClasses(-1)} | ||
| 35 | onClick={() => onUploadRun()} | ||
| 36 | > | ||
| 37 | <img src={UploadIcon} alt="Upload" className={``} /> | ||
| 38 | {true && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>} | ||
| 39 | </button> | ||
| 40 | )} | ||
| 41 | |||
| 42 | <div className={true ? 'min-w-0' : 'flex justify-center'}> | ||
| 43 | <Login | ||
| 44 | setToken={setToken} | ||
| 45 | profile={profile} | ||
| 46 | setProfile={setProfile} | ||
| 47 | isOpen={true} | ||
| 48 | /> | ||
| 49 | </div> | ||
| 50 | |||
| 51 | <Link to="/rules" tabIndex={-1}> | ||
| 52 | <button | ||
| 53 | ref={el => { | ||
| 54 | sidebarButtonRefs.current[5] = el | ||
| 55 | }} | ||
| 56 | className={`${styles.button}`} | ||
| 57 | onClick={() => handle_sidebar_click(5)} | ||
| 58 | > | ||
| 59 | <img src={BookIcon} alt="Rules" /> | ||
| 60 | {true && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>} | ||
| 61 | </button> | ||
| 62 | </Link> | ||
| 63 | |||
| 64 | <Link to="/about" tabIndex={-1}> | ||
| 65 | <button | ||
| 66 | ref={el => { | ||
| 67 | sidebarButtonRefs.current[6] = el | ||
| 68 | }} | ||
| 69 | className={`${styles.button}`} | ||
| 70 | onClick={() => handle_sidebar_click(6)} | ||
| 71 | > | ||
| 72 | <img src={HelpIcon} alt="About" /> | ||
| 73 | {true && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>} | ||
| 74 | </button> | ||
| 75 | </Link> | ||
| 76 | </div> | ||
| 77 | ); | ||
| 78 | } | ||
| 79 | |||
| 80 | export default Footer; | ||
diff --git a/frontend/src/components/Sidebar/Header.tsx b/frontend/src/components/Sidebar/Header.tsx new file mode 100644 index 0000000..e990060 --- /dev/null +++ b/frontend/src/components/Sidebar/Header.tsx | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | import React from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import { | ||
| 5 | LogoIcon, | ||
| 6 | } from "../../images/Images"; | ||
| 7 | |||
| 8 | const Header: React.FC = () => { | ||
| 9 | return ( | ||
| 10 | <div className="flex justify-center px-4 py-3 bg-gradient-to-t from-block to-bright"> | ||
| 11 | <Link to="/" tabIndex={-1} className="flex gap-4"> | ||
| 12 | <img src={LogoIcon} alt="Logo" className="h-18 translate-y-0.5" /> | ||
| 13 | <div className="text-[#fff] flex flex-col justify-center not-md:hidden"> | ||
| 14 | <div className="font-barlow-condensed-bold text-5xl truncate leading-10"> | ||
| 15 | PORTAL 2 | ||
| 16 | </div> | ||
| 17 | <div className="font-barlow-condensed-regular text-3xl leading-7"> | ||
| 18 | Least Portals Hub | ||
| 19 | </div> | ||
| 20 | </div> | ||
| 21 | </Link> | ||
| 22 | </div> | ||
| 23 | ) | ||
| 24 | } | ||
| 25 | |||
| 26 | export default Header; | ||
diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css new file mode 100644 index 0000000..8079676 --- /dev/null +++ b/frontend/src/components/Sidebar/Sidebar.module.css | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | .button { | ||
| 2 | display: flex; | ||
| 3 | width: 100%; | ||
| 4 | cursor: pointer; | ||
| 5 | align-items: center; | ||
| 6 | border-radius: 2000px; | ||
| 7 | transition: all 0.2s ease; | ||
| 8 | padding: 4px 0px; | ||
| 9 | } | ||
| 10 | |||
| 11 | .button:hover { | ||
| 12 | background-color: var(--color-panel); | ||
| 13 | } | ||
| 14 | |||
| 15 | .button>img { | ||
| 16 | padding: 0px 8px; | ||
| 17 | height: 28px; | ||
| 18 | } | ||
| 19 | |||
| 20 | button>span { | ||
| 21 | font-size: 22px; | ||
| 22 | font-family: var(--font-barlow-semicondensed-regular); | ||
| 23 | } \ No newline at end of file | ||
diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..2dafa2b --- /dev/null +++ b/frontend/src/components/Sidebar/Sidebar.tsx | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | import React, { useCallback, useRef } from "react"; | ||
| 2 | import { Link, useLocation } from "react-router-dom"; | ||
| 3 | import { UserProfile } from "@customTypes/Profile"; | ||
| 4 | |||
| 5 | import Header from "./Header"; | ||
| 6 | import Footer from "./Footer"; | ||
| 7 | import Content from "./Content"; | ||
| 8 | |||
| 9 | interface SidebarProps { | ||
| 10 | setToken: React.Dispatch<React.SetStateAction<string | undefined>>; | ||
| 11 | profile?: UserProfile; | ||
| 12 | setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; | ||
| 13 | onUploadRun: () => void; | ||
| 14 | } | ||
| 15 | |||
| 16 | const Sidebar: React.FC<SidebarProps> = ({ | ||
| 17 | setToken, | ||
| 18 | profile, | ||
| 19 | setProfile, | ||
| 20 | onUploadRun, | ||
| 21 | }) => { | ||
| 22 | // const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); | ||
| 23 | const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false); | ||
| 24 | const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1); | ||
| 25 | |||
| 26 | const location = useLocation(); | ||
| 27 | const path = location.pathname; | ||
| 28 | |||
| 29 | const sidebarRef = useRef<HTMLDivElement>(null); | ||
| 30 | const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); | ||
| 31 | |||
| 32 | // const _handle_sidebar_toggle = useCallback(() => { | ||
| 33 | // if (!sidebarRef.current) return; | ||
| 34 | |||
| 35 | // if (isSidebarOpen) { | ||
| 36 | // setSidebarOpen(false); | ||
| 37 | // } else { | ||
| 38 | // setSidebarOpen(true); | ||
| 39 | // searchbarRef.current?.focus(); | ||
| 40 | // } | ||
| 41 | // }, [isSidebarOpen]); | ||
| 42 | |||
| 43 | const handle_sidebar_click = useCallback( | ||
| 44 | (clicked_sidebar_idx: number) => { | ||
| 45 | setSelectedButtonIndex(clicked_sidebar_idx); | ||
| 46 | if (isSidebarOpen) { | ||
| 47 | setSidebarOpen(false); | ||
| 48 | } | ||
| 49 | }, | ||
| 50 | [isSidebarOpen] | ||
| 51 | ); | ||
| 52 | |||
| 53 | React.useEffect(() => { | ||
| 54 | if (path === "/") { | ||
| 55 | setSelectedButtonIndex(1); | ||
| 56 | } else if (path.includes("games")) { | ||
| 57 | setSelectedButtonIndex(2); | ||
| 58 | } else if (path.includes("rankings")) { | ||
| 59 | setSelectedButtonIndex(3); | ||
| 60 | } else if (path.includes("profile")) { | ||
| 61 | setSelectedButtonIndex(4); | ||
| 62 | } else if (path.includes("rules")) { | ||
| 63 | setSelectedButtonIndex(5); | ||
| 64 | } else if (path.includes("about")) { | ||
| 65 | setSelectedButtonIndex(6); | ||
| 66 | } | ||
| 67 | }, [path]); | ||
| 68 | |||
| 69 | const getButtonClasses = (buttonIndex: number) => { | ||
| 70 | const baseClasses = "flex items-center gap-3 w-full text-left bg-inherit cursor-pointer border-none rounded-[2000px] py-3 px-3 transition-all duration-300 hover:bg-panel"; | ||
| 71 | const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground"; | ||
| 72 | |||
| 73 | return `${baseClasses} ${selectedClasses}`; | ||
| 74 | }; | ||
| 75 | |||
| 76 | return ( | ||
| 77 | <div className={`w-80 not-md:w-full text-white bg-block flex flex-col not-md:flex-row | ||
| 78 | }`}> | ||
| 79 | |||
| 80 | {/* Header */} | ||
| 81 | <Header /> | ||
| 82 | |||
| 83 | <div className="flex h-full w-full"> | ||
| 84 | <div className="flex flex-col"> | ||
| 85 | {/* Sidebar Content */} | ||
| 86 | <Content profile={profile} isSidebarOpen={isSidebarOpen} sidebarButtonRefs={sidebarButtonRefs} getButtonClasses={getButtonClasses} handle_sidebar_click={handle_sidebar_click} /> | ||
| 87 | |||
| 88 | {/* Bottom Section */} | ||
| 89 | <Footer profile={profile} onUploadRun={onUploadRun} setToken={setToken} setProfile={setProfile} sidebarButtonRefs={sidebarButtonRefs} getButtonClasses={getButtonClasses} handle_sidebar_click={handle_sidebar_click} /> | ||
| 90 | </div> | ||
| 91 | |||
| 92 | <div className="w-20"> | ||
| 93 | |||
| 94 | </div> | ||
| 95 | |||
| 96 | </div> | ||
| 97 | </div> | ||
| 98 | ); | ||
| 99 | }; | ||
| 100 | |||
| 101 | export default Sidebar; | ||
diff --git a/frontend/src/pages/Maplist/Components/Map.tsx b/frontend/src/pages/Maplist/Components/Map.tsx new file mode 100644 index 0000000..5451830 --- /dev/null +++ b/frontend/src/pages/Maplist/Components/Map.tsx | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | import React from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | import type { Map } from "@customTypes/Map"; | ||
| 4 | |||
| 5 | interface MapProps { | ||
| 6 | map: Map; | ||
| 7 | catNum: number; | ||
| 8 | }; | ||
| 9 | |||
| 10 | const Map: React.FC<MapProps> = ({ map, catNum }) => { | ||
| 11 | return ( | ||
| 12 | |||
| 13 | <div className="bg-panel rounded-3xl overflow-hidden"> | ||
| 14 | <Link to={`/maps/${map.id}`}> | ||
| 15 | <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate"> | ||
| 16 | {map.name} | ||
| 17 | </span> | ||
| 18 | <div | ||
| 19 | className="flex h-40 sm:h-48 bg-cover relative" | ||
| 20 | style={{ backgroundImage: `url(${map.image})` }} | ||
| 21 | > | ||
| 22 | <div className="backdrop-blur-sm w-full flex items-center justify-center"> | ||
| 23 | <span className="text-3xl sm:text-5xl font-barlow-semicondensed-semibold text-white mr-1.5"> | ||
| 24 | {map.is_disabled | ||
| 25 | ? map.category_portals[0].portal_count | ||
| 26 | : map.category_portals.find( | ||
| 27 | obj => obj.category.id === catNum + 1 | ||
| 28 | )?.portal_count} | ||
| 29 | </span> | ||
| 30 | <span className="text-2xl sm:text-4xl font-barlow-semicondensed-regular text-white"> | ||
| 31 | portals | ||
| 32 | </span> | ||
| 33 | </div> | ||
| 34 | </div> | ||
| 35 | |||
| 36 | <div className="flex mx-5 my-4"> | ||
| 37 | <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px"> | ||
| 38 | {[1, 2, 3, 4, 5].map((point) => ( | ||
| 39 | <div | ||
| 40 | key={point} | ||
| 41 | className={`flex h-[3px] w-full rounded-3xl ${point <= (map.difficulty + 1) | ||
| 42 | ? map.difficulty === 0 | ||
| 43 | ? "bg-green-500" | ||
| 44 | : map.difficulty === 1 || map.difficulty === 2 | ||
| 45 | ? "bg-lime-500" | ||
| 46 | : map.difficulty === 3 | ||
| 47 | ? "bg-red-400" | ||
| 48 | : "bg-red-600" | ||
| 49 | : "bg-block" | ||
| 50 | }`} | ||
| 51 | /> | ||
| 52 | ))} | ||
| 53 | </div> | ||
| 54 | </div> | ||
| 55 | </Link> | ||
| 56 | </div> | ||
| 57 | ); | ||
| 58 | }; | ||
| 59 | |||
| 60 | export default Map; | ||
diff --git a/frontend/src/pages/Maplist/Maplist.tsx b/frontend/src/pages/Maplist/Maplist.tsx index 8d9c14a..a5649db 100644 --- a/frontend/src/pages/Maplist/Maplist.tsx +++ b/frontend/src/pages/Maplist/Maplist.tsx | |||
| @@ -6,6 +6,8 @@ import { API } from "@api/Api.ts"; | |||
| 6 | import { Game } from "@customTypes/Game.ts"; | 6 | import { Game } from "@customTypes/Game.ts"; |
| 7 | import { GameChapter, GamesChapters } from "@customTypes/Chapters.ts"; | 7 | import { GameChapter, GamesChapters } from "@customTypes/Chapters.ts"; |
| 8 | 8 | ||
| 9 | import Map from "./Components/Map"; | ||
| 10 | |||
| 9 | const Maplist: React.FC = () => { | 11 | const Maplist: React.FC = () => { |
| 10 | const [game, setGame] = React.useState<Game | null>(null); | 12 | const [game, setGame] = React.useState<Game | null>(null); |
| 11 | const [catNum, setCatNum] = React.useState(0); | 13 | const [catNum, setCatNum] = React.useState(0); |
| @@ -87,14 +89,14 @@ const Maplist: React.FC = () => { | |||
| 87 | }, [gameChapters, location.search]); | 89 | }, [gameChapters, location.search]); |
| 88 | 90 | ||
| 89 | return ( | 91 | return ( |
| 90 | <div> | 92 | <div className="px-12"> |
| 91 | <Helmet> | 93 | <Helmet> |
| 92 | <title>LPHUB | Maplist</title> | 94 | <title>LPHUB | Maplist</title> |
| 93 | </Helmet> | 95 | </Helmet> |
| 94 | 96 | ||
| 95 | <section className="mt-5"> | 97 | <section className="my-5"> |
| 96 | <Link to="/games"> | 98 | <Link to="/games"> |
| 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"> | 99 | <button className="nav-button rounded-[20px] h-10 bg-surface border-0 text-foreground text-lg font-barlow-semicondensed-regular transition-colors duration-100 hover:bg-surface2 flex items-center px-2"> |
| 98 | <i className="triangle mr-2"></i> | 100 | <i className="triangle mr-2"></i> |
| 99 | <span className="px-2">Games List</span> | 101 | <span className="px-2">Games List</span> |
| 100 | </button> | 102 | </button> |
| @@ -105,36 +107,36 @@ const Maplist: React.FC = () => { | |||
| 105 | <div></div> | 107 | <div></div> |
| 106 | ) : ( | 108 | ) : ( |
| 107 | <section> | 109 | <section> |
| 108 | <h1 className="font-[--font-barlow-condensed-bold] text-3xl sm:text-6xl my-0 text-foreground"> | 110 | <h1 className="text-3xl sm:text-6xl my-0"> |
| 109 | {game?.name} | 111 | {game?.name} |
| 110 | </h1> | 112 | </h1> |
| 111 | 113 | ||
| 112 | <div | 114 | <div |
| 113 | className="text-center rounded-3xl overflow-hidden bg-cover bg-[25%] mt-3 relative" | 115 | className="text-center rounded-3xl overflow-hidden bg-panel bg-[25%] mt-3 relative" |
| 114 | style={{ backgroundImage: `url(${game?.image})` }} | 116 | style={{ backgroundImage: `url(${game?.image})` }} |
| 115 | > | 117 | > |
| 116 | <div className="backdrop-blur-sm flex flex-col w-full"> | 118 | <div className="backdrop-blur-sm flex flex-col w-full"> |
| 117 | <div className="h-full flex flex-col justify-center items-center py-6"> | 119 | <div className="h-full flex justify-center items-center py-6"> |
| 118 | <h2 className="my-5 font-[--font-barlow-semicondensed-semibold] text-4xl sm:text-8xl text-foreground"> | 120 | <span className="font-barlow-semicondensed-semibold text-8xl"> |
| 119 | { | 121 | { |
| 120 | game?.category_portals.find( | 122 | game?.category_portals.find( |
| 121 | obj => obj.category.id === catNum + 1 | 123 | obj => obj.category.id === catNum + 1 |
| 122 | )?.portal_count | 124 | )?.portal_count |
| 123 | } | 125 | } |
| 124 | </h2> | 126 | </span> |
| 125 | <h3 className="font-[--font-barlow-semicondensed-regular] mx-2.5 text-2xl sm:text-4xl my-0 text-foreground"> | 127 | <span className="font-barlow-semicondensed-regular mx-2.5 text-2xl sm:text-4xl my-0 text-foreground"> |
| 126 | portals | 128 | portals |
| 127 | </h3> | 129 | </span> |
| 128 | </div> | 130 | </div> |
| 129 | 131 | ||
| 130 | <div className="flex h-12 bg-surface gap-0.5"> | 132 | <div className="flex h-12 bg-panel gap-0.5"> |
| 131 | {game?.category_portals.map((cat, index) => ( | 133 | {game?.category_portals.map((cat, index) => ( |
| 132 | <button | 134 | <button |
| 133 | key={index} | 135 | 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 ${currentlySelected === cat.category.id || | 136 | className={`border-0 text-foreground font-barlow-semicondensed-regular text-sm sm:text-xl cursor-pointer transition-all duration-100 w-full ${currentlySelected === cat.category.id || |
| 135 | (cat.category.id - 1 === catNum && !hasClicked) | 137 | (cat.category.id - 1 === catNum && !hasClicked) |
| 136 | ? "bg-surface" | 138 | ? "bg-panel" |
| 137 | : "bg-surface1 hover:bg-surface" | 139 | : "bg-block hover:bg-block" |
| 138 | }`} | 140 | }`} |
| 139 | onClick={() => { | 141 | onClick={() => { |
| 140 | setCatNum(cat.category.id - 1); | 142 | setCatNum(cat.category.id - 1); |
| @@ -164,16 +166,16 @@ const Maplist: React.FC = () => { | |||
| 164 | </span> | 166 | </span> |
| 165 | <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i> | 167 | <i className="triangle translate-x-1.5 translate-y-2 -rotate-90"></i> |
| 166 | </div> | 168 | </div> |
| 167 | \ | 169 | |
| 168 | <div | 170 | <div |
| 169 | className={`absolute z-[1000] bg-surface1 rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${dropdownActive === "none" ? "hidden" : "block" | 171 | className={`absolute z-[1000] bg-panel rounded-2xl overflow-hidden p-1 animate-in fade-in duration-100 ${dropdownActive === "none" ? "hidden" : "block" |
| 170 | }`} | 172 | }`} |
| 171 | > | 173 | > |
| 172 | {gameChapters?.chapters.map((chapter, i) => { | 174 | {gameChapters?.chapters.map((chapter, i) => { |
| 173 | return ( | 175 | return ( |
| 174 | <div | 176 | <div |
| 175 | key={i} | 177 | key={i} |
| 176 | className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-surface text-foreground" | 178 | className="cursor-pointer text-base sm:text-xl rounded-[2000px] p-1 hover:bg-block text-foreground" |
| 177 | onClick={() => { | 179 | onClick={() => { |
| 178 | _fetch_chapters(chapter.id.toString()); | 180 | _fetch_chapters(chapter.id.toString()); |
| 179 | _handle_dropdown_click(); | 181 | _handle_dropdown_click(); |
| @@ -189,50 +191,7 @@ const Maplist: React.FC = () => { | |||
| 189 | <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5"> | 191 | <section className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 my-5"> |
| 190 | {curChapter?.maps.map((map, i) => { | 192 | {curChapter?.maps.map((map, i) => { |
| 191 | return ( | 193 | return ( |
| 192 | <div key={i} className="bg-surface rounded-3xl overflow-hidden"> | 194 | <Map key={i} map={map} catNum={catNum} /> |
| 193 | <Link to={`/maps/${map.id}`}> | ||
| 194 | <span className="text-center text-base sm:text-xl w-full block my-1.5 text-foreground truncate"> | ||
| 195 | {map.name} | ||
| 196 | </span> | ||
| 197 | <div | ||
| 198 | className="flex h-40 sm:h-48 bg-cover relative" | ||
| 199 | style={{ backgroundImage: `url(${map.image})` }} | ||
| 200 | > | ||
| 201 | <div className="backdrop-blur-sm w-full flex items-center justify-center"> | ||
| 202 | <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-semibold] text-white mr-1.5"> | ||
| 203 | {map.is_disabled | ||
| 204 | ? map.category_portals[0].portal_count | ||
| 205 | : map.category_portals.find( | ||
| 206 | obj => obj.category.id === catNum + 1 | ||
| 207 | )?.portal_count} | ||
| 208 | </span> | ||
| 209 | <span className="text-2xl sm:text-4xl font-[--font-barlow-semicondensed-regular] text-white"> | ||
| 210 | portals | ||
| 211 | </span> | ||
| 212 | </div> | ||
| 213 | </div> | ||
| 214 | |||
| 215 | <div className="flex mx-2.5 my-4"> | ||
| 216 | <div className="flex w-full items-center justify-center gap-1.5 rounded-[2000px] ml-0.5 translate-y-px"> | ||
| 217 | {[1, 2, 3, 4, 5].map((point) => ( | ||
| 218 | <div | ||
| 219 | key={point} | ||
| 220 | className={`flex h-0.5 w-full rounded-3xl ${point <= (map.difficulty + 1) | ||
| 221 | ? map.difficulty === 0 | ||
| 222 | ? "bg-green-500" | ||
| 223 | : map.difficulty === 1 || map.difficulty === 2 | ||
| 224 | ? "bg-lime-500" | ||
| 225 | : map.difficulty === 3 | ||
| 226 | ? "bg-red-400" | ||
| 227 | : "bg-red-600" | ||
| 228 | : "bg-surface1" | ||
| 229 | }`} | ||
| 230 | /> | ||
| 231 | ))} | ||
| 232 | </div> | ||
| 233 | </div> | ||
| 234 | </Link> | ||
| 235 | </div> | ||
| 236 | ); | 195 | ); |
| 237 | })} | 196 | })} |
| 238 | </section> | 197 | </section> |