aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/Sidebar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/Sidebar.tsx')
-rw-r--r--frontend/src/components/Sidebar.tsx386
1 files changed, 192 insertions, 194 deletions
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 @@
1import React, { useCallback } from "react"; 1import React, { useCallback, useRef } from "react";
2import { Link, useLocation } from "react-router-dom"; 2import { Link, useLocation } from "react-router-dom";
3 3
4import { 4import {
@@ -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";
14import Login from "@components/Login"; 14import Login from "@components/Login";
15import { UserProfile } from "@customTypes/Profile"; 15import { UserProfile } from "@customTypes/Profile";
16import { Search } from "@customTypes/Search"; 16import { Search } from "@customTypes/Search";
17import { API } from "@api/Api"; 17import { API } from "@api/Api";
18import "@css/Sidebar.css";
19 18
20interface SidebarProps { 19interface 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
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
27const Sidebar: React.FC<SidebarProps> = ({ 37const 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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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};