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.tsx381
1 files changed, 231 insertions, 150 deletions
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..88a5297 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,99 +1,80 @@
1import React 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 { BookIcon, FlagIcon, HelpIcon, HomeIcon, LogoIcon, PortalIcon, SearchIcon, UploadIcon } from '@images/Images'; 4import {
5import Login from '@components/Login'; 5 BookIcon,
6import { UserProfile } from '@customTypes/Profile'; 6 FlagIcon,
7import { Search } from '@customTypes/Search'; 7 HelpIcon,
8import { API } from '@api/Api'; 8 HomeIcon,
9import "@css/Sidebar.css"; 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";
10 18
11interface SidebarProps { 19interface SidebarProps {
12 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 20 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
13 profile?: UserProfile; 21 profile?: UserProfile;
14 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 22 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
15 onUploadRun: () => void; 23 onUploadRun: () => void;
16}; 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}
17 31
18const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUploadRun }) => { 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}
19 36
20 const [searchData, setSearchData] = React.useState<Search | undefined>(undefined); 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 );
21 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false); 46 const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
22 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true); 47 const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
48 const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
23 49
24 const location = useLocation(); 50 const location = useLocation();
25 const path = location.pathname; 51 const path = location.pathname;
26 52
27 const handle_sidebar_click = (clicked_sidebar_idx: number) => { 53 const sidebarRef = useRef<HTMLDivElement>(null);
28 const btn = document.querySelectorAll("button.sidebar-button"); 54 const searchbarRef = useRef<HTMLInputElement>(null);
29 if (isSidebarOpen) { setSidebarOpen(false); _handle_sidebar_hide() } 55 const uploadRunRef = useRef<HTMLButtonElement>(null);
30 // clusterfuck 56 const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
31 btn.forEach((e, i) => {
32 btn[i].classList.remove("sidebar-button-selected")
33 btn[i].classList.add("sidebar-button-deselected")
34 })
35 btn[clicked_sidebar_idx].classList.add("sidebar-button-selected")
36 btn[clicked_sidebar_idx].classList.remove("sidebar-button-deselected")
37 };
38 57
39 const _handle_sidebar_hide = () => { 58 const _handle_sidebar_toggle = useCallback(() => {
40 var btn = document.querySelectorAll("button.sidebar-button") as NodeListOf<HTMLElement> 59 if (!sidebarRef.current) return;
41 const span = document.querySelectorAll("button.sidebar-button>span") as NodeListOf<HTMLElement>
42 const side = document.querySelector("#sidebar-list") as HTMLElement;
43 const searchbar = document.querySelector("#searchbar") as HTMLInputElement;
44 const uploadRunBtn = document.querySelector("#upload-run") as HTMLInputElement;
45 const uploadRunSpan = document.querySelector("#upload-run>span") as HTMLInputElement;
46 60
47 if (isSidebarOpen) { 61 if (isSidebarOpen) {
48 if (profile) {
49 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
50 login.style.opacity = "1"
51 uploadRunBtn.style.width = "310px"
52 uploadRunBtn.style.padding = "0.4em 0 0 11px"
53 uploadRunSpan.style.opacity = "0"
54 setTimeout(() => {
55 uploadRunSpan.style.opacity = "1"
56 }, 100)
57 }
58 setSidebarOpen(false); 62 setSidebarOpen(false);
59 side.style.width = "320px"
60 btn.forEach((e, i) => {
61 e.style.width = "310px"
62 e.style.padding = "0.4em 0 0 11px"
63 setTimeout(() => {
64 span[i].style.opacity = "1"
65 }, 100)
66 });
67 side.style.zIndex = "2"
68 } else { 63 } else {
69 if (profile) {
70 const login = document.querySelectorAll(".login>button")[1] as HTMLElement;
71 login.style.opacity = "0"
72 uploadRunBtn.style.width = "40px"
73 uploadRunBtn.style.padding = "0.4em 0 0 5px"
74 uploadRunSpan.style.opacity = "0"
75 }
76 setSidebarOpen(true); 64 setSidebarOpen(true);
77 side.style.width = "40px"; 65 searchbarRef.current?.focus();
78 searchbar.focus();
79 btn.forEach((e, i) => {
80 e.style.width = "40px"
81 e.style.padding = "0.4em 0 0 5px"
82 span[i].style.opacity = "0"
83 })
84 setTimeout(() => {
85 side.style.zIndex = "0"
86 }, 300);
87 } 66 }
88 }; 67 }, [isSidebarOpen]);
89 68
90 const _handle_sidebar_lock = () => { 69 const handle_sidebar_click = useCallback(
91 if (!isSidebarLocked) { 70 (clicked_sidebar_idx: number) => {
92 _handle_sidebar_hide() 71 setSelectedButtonIndex(clicked_sidebar_idx);
93 setIsSidebarLocked(true); 72 if (isSidebarOpen) {
94 setTimeout(() => setIsSidebarLocked(false), 300); 73 setSidebarOpen(false);
95 } 74 }
96 }; 75 },
76 [isSidebarOpen]
77 );
97 78
98 const _handle_search_change = async (q: string) => { 79 const _handle_search_change = async (q: string) => {
99 const searchResponse = await API.get_search(q); 80 const searchResponse = await API.get_search(q);
@@ -101,100 +82,200 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
101 }; 82 };
102 83
103 React.useEffect(() => { 84 React.useEffect(() => {
104 if (path === "/") { handle_sidebar_click(1) } 85 if (path === "/") {
105 else if (path.includes("games")) { handle_sidebar_click(2) } 86 setSelectedButtonIndex(1);
106 else if (path.includes("rankings")) { handle_sidebar_click(3) } 87 } else if (path.includes("games")) {
107 // else if (path.includes("news")) { handle_sidebar_click(4) } 88 setSelectedButtonIndex(2);
108 // else if (path.includes("scorelog")) { handle_sidebar_click(5) } 89 } else if (path.includes("rankings")) {
109 else if (path.includes("profile")) { handle_sidebar_click(4) } 90 setSelectedButtonIndex(3);
110 else if (path.includes("rules")) { handle_sidebar_click(5) } 91 } else if (path.includes("profile")) {
111 else if (path.includes("about")) { handle_sidebar_click(6) } 92 setSelectedButtonIndex(4);
93 } else if (path.includes("rules")) {
94 setSelectedButtonIndex(5);
95 } else if (path.includes("about")) {
96 setSelectedButtonIndex(6);
97 }
112 }, [path]); 98 }, [path]);
113 99
114 return ( 100 const getButtonClasses = (buttonIndex: number) => {
115 <div id='sidebar'> 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";
116 <Link to="/" tabIndex={-1}> 102 const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
117 <div id='logo'> {/* logo */} 103
118 <img src={LogoIcon} alt="" height={"80px"} /> 104 return `${baseClasses} ${selectedClasses}`;
119 <div id='logo-text'> 105 };
120 <span><b>PORTAL 2</b></span><br />
121 <span>Least Portals Hub</span>
122 </div>
123 </div>
124 </Link>
125 <div id='sidebar-list'> {/* List */}
126 <div id='sidebar-toplist'> {/* Top */}
127
128 <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
129
130 <span></span>
131 106
132 <Link to="/" tabIndex={-1}> 107 const iconClasses = "w-6 h-6 flex-shrink-0";
133 <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
134 </Link>
135 108
136 <Link to="/games" tabIndex={-1}> 109 return (
137 <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button> 110 <div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${
138 </Link> 111 isSidebarOpen ? 'w-80' : 'w-20'
112 }`}>
113 <div className="flex items-center h-20 px-4 border-b border-border">
114 <Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
115 <img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
116 {isSidebarOpen && (
117 <div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden">
118 <div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate">
119 PORTAL 2
120 </div>
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>
139 136
140 <Link to="/rankings" tabIndex={-1}> 137 {/* Sidebar Content */}
141 <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button> 138 <div
142 </Link> 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 />
143 158
144 {/* <Link to="/news" tabIndex={-1}> 159 {searchData && (
145 <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button> 160 <div className="mt-2 max-h-40 overflow-y-auto min-w-0">
146 </Link> */} 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>
187 </div>
188 )}
147 189
148 {/* <Link to="/scorelog" tabIndex={-1}> 190 <div className="flex-1 p-4 min-w-0">
149 <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button> 191 <nav className="space-y-2">
150 </Link> */} 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>
151 </div> 231 </div>
152 <div id='sidebar-bottomlist'>
153 <span></span>
154 232
155 { 233 {/* Bottom Section */}
156 profile && profile.profile ? 234 <div className="p-4 border-t border-border space-y-2 min-w-0">
157 <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button> 235 {profile && profile.profile && (
158 : 236 <button
159 <span></span> 237 ref={uploadRunRef}
160 } 238 id="upload-run"
239 className={getButtonClasses(-1)}
240 onClick={() => onUploadRun()}
241 >
242 <img src={UploadIcon} alt="Upload" className={iconClasses} />
243 {isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>}
244 </button>
245 )}
161 246
162 <Login setToken={setToken} profile={profile} setProfile={setProfile} /> 247 <div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}>
248 <Login
249 setToken={setToken}
250 profile={profile}
251 setProfile={setProfile}
252 isOpen={isSidebarOpen}
253 />
254 </div>
163 255
164 <Link to="/rules" tabIndex={-1}> 256 <Link to="/rules" tabIndex={-1}>
165 <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button> 257 <button
258 ref={el => sidebarButtonRefs.current[5] = el}
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>}
264 </button>
166 </Link> 265 </Link>
167 266
168 <Link to="/about" tabIndex={-1}> 267 <Link to="/about" tabIndex={-1}>
169 <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button> 268 <button
269 ref={el => sidebarButtonRefs.current[6] = el}
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>}
275 </button>
170 </Link> 276 </Link>
171 </div> 277 </div>
172 </div> 278 </div>
173 <div>
174 <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
175
176 <div id='search-data'>
177
178 {searchData?.maps.map((q, index) => (
179 <Link to={`/maps/${q.id}`} className='search-map' key={index}>
180 <span>{q.game}</span>
181 <span>{q.chapter}</span>
182 <span>{q.map}</span>
183 </Link>
184 ))}
185 {searchData?.players.map((q, index) =>
186 (
187 <Link to={
188 profile && q.steam_id === profile.steam_id ? `/profile` :
189 `/users/${q.steam_id}`
190 } className='search-player' key={index}>
191 <img src={q.avatar_link} alt='pfp'></img>
192 <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
193 </Link>
194 ))}
195
196 </div>
197 </div>
198 </div> 279 </div>
199 ); 280 );
200}; 281};