aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/Sidebar.tsx
diff options
context:
space:
mode:
authorWolfboy248 <georgejvindkarlsen@gmail.com>2025-08-21 10:33:27 +0200
committerWolfboy248 <georgejvindkarlsen@gmail.com>2025-08-21 10:33:27 +0200
commitda1fd74f9387149b2b94d62853587a8afdb74ddd (patch)
tree57f13021890b6d27848a3379d0869790fd1d7c97 /frontend/src/components/Sidebar.tsx
parentorganised pages, started work on theme (diff)
downloadlphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.tar.gz
lphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.tar.bz2
lphub-da1fd74f9387149b2b94d62853587a8afdb74ddd.zip
Reorganised Maplist and Sidebar
Diffstat (limited to 'frontend/src/components/Sidebar.tsx')
-rw-r--r--frontend/src/components/Sidebar.tsx288
1 files changed, 0 insertions, 288 deletions
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;