diff options
Diffstat (limited to 'frontend/src/components/Sidebar')
| -rw-r--r-- | frontend/src/components/Sidebar/Content.tsx | 133 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Footer.tsx | 80 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Header.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Sidebar.module.css | 23 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar/Sidebar.tsx | 101 |
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 @@ | |||
| 1 | import React, { useRef } from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | import { UserProfile } from "@customTypes/Profile"; | ||
| 4 | import { Search } from "@customTypes/Search"; | ||
| 5 | import { API } from "@api/Api"; | ||
| 6 | |||
| 7 | import styles from "./Sidebar.module.css"; | ||
| 8 | |||
| 9 | import { | ||
| 10 | FlagIcon, | ||
| 11 | HomeIcon, | ||
| 12 | PortalIcon, | ||
| 13 | SearchIcon, | ||
| 14 | } from "../../images/Images"; | ||
| 15 | |||
| 16 | interface 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 | |||
| 24 | const 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 | |||
| 133 | export 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 @@ | |||
| 1 | import React, { useRef } from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import styles from "./Sidebar.module.css"; | ||
| 5 | |||
| 6 | import { UserProfile } from "@customTypes/Profile"; | ||
| 7 | import Login from "@components/Login"; | ||
| 8 | |||
| 9 | import { | ||
| 10 | UploadIcon, | ||
| 11 | BookIcon, | ||
| 12 | HelpIcon, | ||
| 13 | } from "../../images/Images"; | ||
| 14 | |||
| 15 | interface 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 | |||
| 25 | const 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 | |||
| 80 | export 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 @@ | |||
| 1 | import React from "react"; | ||
| 2 | import { Link } from "react-router-dom"; | ||
| 3 | |||
| 4 | import { | ||
| 5 | LogoIcon, | ||
| 6 | } from "../../images/Images"; | ||
| 7 | |||
| 8 | const 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 | |||
| 26 | export 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 | |||
| 20 | button>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 @@ | |||
| 1 | import React, { useCallback, useRef } from "react"; | ||
| 2 | import { Link, useLocation } from "react-router-dom"; | ||
| 3 | import { UserProfile } from "@customTypes/Profile"; | ||
| 4 | |||
| 5 | import Header from "./Header"; | ||
| 6 | import Footer from "./Footer"; | ||
| 7 | import Content from "./Content"; | ||
| 8 | |||
| 9 | interface 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 | |||
| 16 | const 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 | |||
| 101 | export default Sidebar; | ||