aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/App.css1
-rw-r--r--frontend/src/App.tsx2
-rw-r--r--frontend/src/components/Sidebar.tsx288
-rw-r--r--frontend/src/components/Sidebar/Content.tsx133
-rw-r--r--frontend/src/components/Sidebar/Footer.tsx80
-rw-r--r--frontend/src/components/Sidebar/Header.tsx26
-rw-r--r--frontend/src/components/Sidebar/Sidebar.module.css23
-rw-r--r--frontend/src/components/Sidebar/Sidebar.tsx101
-rw-r--r--frontend/src/pages/Maplist/Components/Map.tsx60
-rw-r--r--frontend/src/pages/Maplist/Maplist.tsx81
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";
3import { Helmet } from "react-helmet"; 3import { Helmet } from "react-helmet";
4 4
5import { UserProfile } from "@customTypes/Profile"; 5import { UserProfile } from "@customTypes/Profile";
6import Sidebar from "./components/Sidebar"; 6import Sidebar from "./components/Sidebar/Sidebar";
7import "./App.css"; 7import "./App.css";
8 8
9import Profile from "@pages/Profile/Profile.tsx"; 9import 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 @@
1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from "react-router-dom";
3
4import {
5 BookIcon,
6 FlagIcon,
7 HelpIcon,
8 HomeIcon,
9 LogoIcon,
10 PortalIcon,
11 SearchIcon,
12 UploadIcon,
13} from "../images/Images";
14import Login from "@components/Login";
15import { UserProfile } from "@customTypes/Profile";
16import { Search } from "@customTypes/Search";
17import { API } from "@api/Api";
18
19interface 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
26function 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
32function 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
37const 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
288export 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 @@
1import React, { useRef } from "react";
2import { Link } from "react-router-dom";
3import { UserProfile } from "@customTypes/Profile";
4import { Search } from "@customTypes/Search";
5import { API } from "@api/Api";
6
7import styles from "./Sidebar.module.css";
8
9import {
10 FlagIcon,
11 HomeIcon,
12 PortalIcon,
13 SearchIcon,
14} from "../../images/Images";
15
16interface 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
24const 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
133export 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 @@
1import React, { useRef } from "react";
2import { Link } from "react-router-dom";
3
4import styles from "./Sidebar.module.css";
5
6import { UserProfile } from "@customTypes/Profile";
7import Login from "@components/Login";
8
9import {
10 UploadIcon,
11 BookIcon,
12 HelpIcon,
13} from "../../images/Images";
14
15interface 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
25const 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
80export 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 @@
1import React from "react";
2import { Link } from "react-router-dom";
3
4import {
5 LogoIcon,
6} from "../../images/Images";
7
8const 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
26export 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
20button>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 @@
1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from "react-router-dom";
3import { UserProfile } from "@customTypes/Profile";
4
5import Header from "./Header";
6import Footer from "./Footer";
7import Content from "./Content";
8
9interface 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
16const 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
101export 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 @@
1import React from "react";
2import { Link } from "react-router-dom";
3import type { Map } from "@customTypes/Map";
4
5interface MapProps {
6 map: Map;
7 catNum: number;
8};
9
10const 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
60export 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";
6import { Game } from "@customTypes/Game.ts"; 6import { Game } from "@customTypes/Game.ts";
7import { GameChapter, GamesChapters } from "@customTypes/Chapters.ts"; 7import { GameChapter, GamesChapters } from "@customTypes/Chapters.ts";
8 8
9import Map from "./Components/Map";
10
9const Maplist: React.FC = () => { 11const 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>