aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/Sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/Sidebar')
-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
5 files changed, 363 insertions, 0 deletions
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;