1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
import React, { useCallback, useRef } from "react";
import { Link, useLocation } from "react-router-dom";
import {
BookIcon,
FlagIcon,
HelpIcon,
HomeIcon,
LogoIcon,
PortalIcon,
SearchIcon,
UploadIcon,
} from "../images/Images";
import Login from "@components/Login";
import { UserProfile } from "@customTypes/Profile";
import { Search } from "@customTypes/Search";
import { API } from "@api/Api";
interface SidebarProps {
setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
profile?: UserProfile;
setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
onUploadRun: () => void;
}
function OpenSidebarIcon(){
return (
<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>
)
}
function ClosedSidebarIcon(){
return (
<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> )
}
const Sidebar: React.FC<SidebarProps> = ({
setToken,
profile,
setProfile,
onUploadRun,
}) => {
const [searchData, setSearchData] = React.useState<Search | undefined>(
undefined
);
const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
const [selectedButtonIndex, setSelectedButtonIndex] = React.useState<number>(1);
const location = useLocation();
const path = location.pathname;
const sidebarRef = useRef<HTMLDivElement>(null);
const searchbarRef = useRef<HTMLInputElement>(null);
const uploadRunRef = useRef<HTMLButtonElement>(null);
const sidebarButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
const _handle_sidebar_toggle = useCallback(() => {
if (!sidebarRef.current) return;
if (isSidebarOpen) {
setSidebarOpen(false);
} else {
setSidebarOpen(true);
searchbarRef.current?.focus();
}
}, [isSidebarOpen]);
const handle_sidebar_click = useCallback(
(clicked_sidebar_idx: number) => {
setSelectedButtonIndex(clicked_sidebar_idx);
if (isSidebarOpen) {
setSidebarOpen(false);
}
},
[isSidebarOpen]
);
const _handle_search_change = async (q: string) => {
const searchResponse = await API.get_search(q);
setSearchData(searchResponse);
};
React.useEffect(() => {
if (path === "/") {
setSelectedButtonIndex(1);
} else if (path.includes("games")) {
setSelectedButtonIndex(2);
} else if (path.includes("rankings")) {
setSelectedButtonIndex(3);
} else if (path.includes("profile")) {
setSelectedButtonIndex(4);
} else if (path.includes("rules")) {
setSelectedButtonIndex(5);
} else if (path.includes("about")) {
setSelectedButtonIndex(6);
}
}, [path]);
const getButtonClasses = (buttonIndex: number) => {
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";
const selectedClasses = selectedButtonIndex === buttonIndex ? "bg-primary text-background" : "bg-transparent text-foreground";
return `${baseClasses} ${selectedClasses}`;
};
const iconClasses = "w-6 h-6 flex-shrink-0";
return (
<div className={`fixed top-0 left-0 h-screen bg-surface border-r border-border transition-all duration-300 z-10 overflow-hidden ${
isSidebarOpen ? 'w-80' : 'w-20'
}`}>
<div className="flex items-center h-20 px-4 border-b border-border">
<Link to="/" tabIndex={-1} className="flex items-center flex-1 cursor-pointer select-none min-w-0">
<img src={LogoIcon} alt="Logo" className="w-12 h-12 flex-shrink-0" />
{isSidebarOpen && (
<div className="ml-3 font-[--font-barlow-condensed-regular] text-white min-w-0 overflow-hidden">
<div className="font-[--font-barlow-condensed-bold] text-2xl leading-6 truncate">
PORTAL 2
</div>
<div className="text-sm leading-4 truncate">
Least Portals Hub
</div>
</div>
)}
</Link>
<button
onClick={_handle_sidebar_toggle}
className="ml-2 p-2 rounded-lg hover:bg-surface1 transition-colors text-foreground"
title={isSidebarOpen ? "Close sidebar" : "Open sidebar"}
>
{isSidebarOpen ? <ClosedSidebarIcon /> : <OpenSidebarIcon />}
</button>
</div>
{/* Sidebar Content */}
<div
ref={sidebarRef}
className="flex flex-col h-[calc(100vh-80px)] overflow-y-auto overflow-x-hidden"
>
{isSidebarOpen && (
<div className="p-4 border-b border-border min-w-0">
<div className="flex items-center gap-3 mb-3">
<img src={SearchIcon} alt="Search" className={iconClasses} />
<span className="text-white font-[--font-barlow-semicondensed-regular] truncate">Search</span>
</div>
<div className="min-w-0">
<input
ref={searchbarRef}
type="text"
id="searchbar"
placeholder="Search for map or a player..."
onChange={e => _handle_search_change(e.target.value)}
className="w-full p-2 bg-input text-foreground border border-border rounded-lg text-sm min-w-0"
/>
{searchData && (
<div className="mt-2 max-h-40 overflow-y-auto min-w-0">
{searchData?.maps.map((q, index) => (
<Link to={`/maps/${q.id}`} className="block p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0" key={index}>
<span className="block text-xs text-subtext1 truncate">{q.game}</span>
<span className="block text-xs text-subtext1 truncate">{q.chapter}</span>
<span className="block text-sm text-foreground truncate">{q.map}</span>
</Link>
))}
{searchData?.players.map((q, index) => (
<Link
to={
profile && q.steam_id === profile.steam_id
? `/profile`
: `/users/${q.steam_id}`
}
className="flex items-center p-2 mb-1 bg-surface1 rounded hover:bg-surface2 transition-colors min-w-0"
key={index}
>
<img src={q.avatar_link} alt="pfp" className="w-6 h-6 rounded-full mr-2 flex-shrink-0" />
<span className="text-sm text-foreground truncate">
{q.user_name}
</span>
</Link>
))}
</div>
)}
</div>
</div>
)}
<div className="flex-1 p-4 min-w-0">
<nav className="space-y-2">
{[
{
to: "/",
refIndex: 1,
icon: HomeIcon,
alt: "Home",
label: "Home Page",
},
{
to: "/games",
refIndex: 2,
icon: PortalIcon,
alt: "Games",
label: "Games",
},
{
to: "/rankings",
refIndex: 3,
icon: FlagIcon,
alt: "Rankings",
label: "Rankings",
},
].map(({ to, refIndex, icon, alt, label }) => (
<Link to={to} tabIndex={-1} key={refIndex}>
<button
ref={el => sidebarButtonRefs.current[refIndex] = el}
className={getButtonClasses(refIndex)}
onClick={() => handle_sidebar_click(refIndex)}
>
<img src={icon} alt={alt} className={iconClasses} />
{isSidebarOpen && (
<span className="text-white font-[--font-barlow-semicondensed-regular] truncate">
{label}
</span>
)}
</button>
</Link>
))}
</nav>
</div>
{/* Bottom Section */}
<div className="p-4 border-t border-border space-y-2 min-w-0">
{profile && profile.profile && (
<button
ref={uploadRunRef}
id="upload-run"
className={getButtonClasses(-1)}
onClick={() => onUploadRun()}
>
<img src={UploadIcon} alt="Upload" className={iconClasses} />
{isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Upload Record</span>}
</button>
)}
<div className={isSidebarOpen ? 'min-w-0' : 'flex justify-center'}>
<Login
setToken={setToken}
profile={profile}
setProfile={setProfile}
isOpen={isSidebarOpen}
/>
</div>
<Link to="/rules" tabIndex={-1}>
<button
ref={el => sidebarButtonRefs.current[5] = el}
className={getButtonClasses(5)}
onClick={() => handle_sidebar_click(5)}
>
<img src={BookIcon} alt="Rules" className={iconClasses} />
{isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">Leaderboard Rules</span>}
</button>
</Link>
<Link to="/about" tabIndex={-1}>
<button
ref={el => sidebarButtonRefs.current[6] = el}
className={getButtonClasses(6)}
onClick={() => handle_sidebar_click(6)}
>
<img src={HelpIcon} alt="About" className={iconClasses} />
{isSidebarOpen && <span className="font-[--font-barlow-semicondensed-regular] truncate">About LPHUB</span>}
</button>
</Link>
</div>
</div>
</div>
);
};
export default Sidebar;
|