aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorFifthWit <fifthwitbusiness@gmail.com>2025-08-14 14:01:01 -0500
committerFifthWit <fifthwitbusiness@gmail.com>2025-08-14 14:01:01 -0500
commit6a8b909afbe1560be95f7ad0a3e19cfe4717aec6 (patch)
tree83cdbe3b5b7e5b83d5f0d08964634cc942264072 /frontend/src/components
parentSwitched to Vite as build tool (diff)
downloadlphub-6a8b909afbe1560be95f7ad0a3e19cfe4717aec6.tar.gz
lphub-6a8b909afbe1560be95f7ad0a3e19cfe4717aec6.tar.bz2
lphub-6a8b909afbe1560be95f7ad0a3e19cfe4717aec6.zip
Switched to tailwind/vite
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ConfirmDialog.tsx16
-rw-r--r--frontend/src/components/GameCategory.tsx15
-rw-r--r--frontend/src/components/GameEntry.tsx27
-rw-r--r--frontend/src/components/Leaderboards.tsx6
-rw-r--r--frontend/src/components/Login.tsx53
-rw-r--r--frontend/src/components/ModMenu.tsx1
-rw-r--r--frontend/src/components/Sidebar.tsx386
-rw-r--r--frontend/src/components/Summary.tsx1
-rw-r--r--frontend/src/components/UploadRunDialog.tsx1
9 files changed, 256 insertions, 250 deletions
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
index c89d9ea..8f2ce7a 100644
--- a/frontend/src/components/ConfirmDialog.tsx
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -1,7 +1,5 @@
1import React from "react"; 1import React from "react";
2 2
3import "@css/Dialog.css";
4
5interface ConfirmDialogProps { 3interface ConfirmDialogProps {
6 title: string; 4 title: string;
7 subtitle: string; 5 subtitle: string;
@@ -16,17 +14,17 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
16 onCancel, 14 onCancel,
17}) => { 15}) => {
18 return ( 16 return (
19 <div className="dimmer"> 17 <div className="fixed w-[200%] h-full bg-black bg-opacity-50 z-[4]">
20 <div className="dialog"> 18 <div className="fixed z-[4] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-surface rounded-3xl overflow-hidden min-w-[350px] border border-border animate-[dialog_in_0.2s_cubic-bezier(0.075,0.82,0.165,1.1)] text-foreground font-[--font-barlow-semicondensed-regular]">
21 <div className="dialog-element dialog-header"> 19 <div className="p-2 text-2xl bg-mantle">
22 <span>{title}</span> 20 <span>{title}</span>
23 </div> 21 </div>
24 <div className="dialog-element dialog-description"> 22 <div className="p-2">
25 <span>{subtitle}</span> 23 <span>{subtitle}</span>
26 </div> 24 </div>
27 <div className="dialog-element dialog-btns-container"> 25 <div className="p-2 flex justify-end border-t-2 border-border bg-mantle">
28 <button onClick={onCancel}>Cancel</button> 26 <button className="mr-2 px-4 py-2 bg-muted text-foreground rounded hover:bg-overlay1 transition-colors" onClick={onCancel}>Cancel</button>
29 <button onClick={onConfirm}>Confirm</button> 27 <button className="px-4 py-2 bg-primary text-background rounded hover:bg-mauve transition-colors" onClick={onConfirm}>Confirm</button>
30 </div> 28 </div>
31 </div> 29 </div>
32 </div> 30 </div>
diff --git a/frontend/src/components/GameCategory.tsx b/frontend/src/components/GameCategory.tsx
index 2bb6d42..b18c9d9 100644
--- a/frontend/src/components/GameCategory.tsx
+++ b/frontend/src/components/GameCategory.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from "@customTypes/Game"; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css";
6 5
7interface GameCategoryProps { 6interface GameCategoryProps {
8 game: Game; 7 game: Game;
@@ -12,18 +11,12 @@ interface GameCategoryProps {
12const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => { 11const GameCategory: React.FC<GameCategoryProps> = ({ cat, game }) => {
13 return ( 12 return (
14 <Link 13 <Link
15 className="games-page-item-body-item" 14 className="bg-surface text-center w-full h-[100px] rounded-3xl text-foreground m-3 hover:bg-surface1 transition-colors flex flex-col justify-between p-4"
16 to={"/games/" + game.id + "?cat=" + cat.category.id} 15 to={"/games/" + game.id + "?cat=" + cat.category.id}
17 > 16 >
18 <div> 17 <p className="text-3xl font-semibold">{cat.category.name}</p>
19 <span className="games-page-item-body-item-title"> 18 <br />
20 {cat.category.name} 19 <p className="font-bold text-4xl">{cat.portal_count}</p>
21 </span>
22 <br />
23 <span className="games-page-item-body-item-num">
24 {cat.portal_count}
25 </span>
26 </div>
27 </Link> 20 </Link>
28 ); 21 );
29}; 22};
diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx
index 04c3483..f8fd179 100644
--- a/frontend/src/components/GameEntry.tsx
+++ b/frontend/src/components/GameEntry.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { Link } from "react-router-dom"; 2import { Link } from "react-router-dom";
3 3
4import { Game, GameCategoryPortals } from "@customTypes/Game"; 4import { Game, GameCategoryPortals } from "@customTypes/Game";
5import "@css/Games.css";
6 5
7import GameCategory from "@components/GameCategory"; 6import GameCategory from "@components/GameCategory";
8 7
@@ -18,23 +17,25 @@ const GameEntry: React.FC<GameEntryProps> = ({ game }) => {
18 }, [game.category_portals]); 17 }, [game.category_portals]);
19 18
20 return ( 19 return (
21 <Link to={"/games/" + game.id}> 20 <Link to={"/games/" + game.id} className="w-full">
22 <div className="games-page-item"> 21 <div className="w-full h-64 bg-mantle rounded-3xl overflow-hidden my-6">
23 <div className="games-page-item-header"> 22 <div className="w-full h-1/2 bg-cover overflow-hidden relative">
24 <div 23 <div
25 style={{ backgroundImage: `url(${game.image})` }} 24 style={{ backgroundImage: `url(${game.image})` }}
26 className="games-page-item-header-img" 25 className="w-full h-full backdrop-blur-sm blur-sm bg-cover"
27 ></div> 26 ></div>
28 <span> 27 <span className="absolute inset-0 flex justify-center items-center">
29 <b>{game.name}</b> 28 <b className="text-[56px] font-[--font-barlow-condensed-bold] text-white">{game.name}</b>
30 </span> 29 </span>
31 </div> 30 </div>
32 <div id={game.id as any as string} className="games-page-item-body"> 31 <div className="flex justify-center items-center h-1/2">
33 {catInfo.map((cat, index) => { 32 <div className="flex flex-row justify-between w-full">
34 return ( 33 {catInfo.map((cat, index) => {
35 <GameCategory cat={cat} game={game} key={index}></GameCategory> 34 return (
36 ); 35 <GameCategory key={index} cat={cat} game={game} />
37 })} 36 );
37 })}
38 </div>
38 </div> 39 </div>
39 </div> 40 </div>
40 </Link> 41 </Link>
diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx
index b388aba..99481a2 100644
--- a/frontend/src/components/Leaderboards.tsx
+++ b/frontend/src/components/Leaderboards.tsx
@@ -36,7 +36,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
36 return ( 36 return (
37 <section id="section6" className="summary2"> 37 <section id="section6" className="summary2">
38 <h1 style={{ textAlign: "center" }}> 38 <h1 style={{ textAlign: "center" }}>
39 Map is not available for competitive boards. 39 Loading...
40 </h1> 40 </h1>
41 </section> 41 </section>
42 ); 42 );
@@ -195,6 +195,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
195 filter: 195 filter:
196 "hue-rotate(160deg) contrast(60%) saturate(1000%)", 196 "hue-rotate(160deg) contrast(60%) saturate(1000%)",
197 }} 197 }}
198 className="w-6 h-6 mx-4"
198 /> 199 />
199 </button> 200 </button>
200 <button 201 <button
@@ -209,6 +210,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
209 filter: 210 filter:
210 "hue-rotate(300deg) contrast(60%) saturate(1000%)", 211 "hue-rotate(300deg) contrast(60%) saturate(1000%)",
211 }} 212 }}
213 className="w-6 h-6"
212 /> 214 />
213 </button> 215 </button>
214 </span> 216 </span>
@@ -227,7 +229,7 @@ const Leaderboards: React.FC<LeaderboardsProps> = ({ mapID }) => {
227 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`) 229 (window.location.href = `/api/v1/demos?uuid=${r.demo_id}`)
228 } 230 }
229 > 231 >
230 <img src={DownloadIcon} alt="download" /> 232 <img src={DownloadIcon} alt="download" className="w-6 h-6 mr-4" />
231 </button> 233 </button>
232 </span> 234 </span>
233 ) 235 )
diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx
index 1858c48..ba85aeb 100644
--- a/frontend/src/components/Login.tsx
+++ b/frontend/src/components/Login.tsx
@@ -1,18 +1,18 @@
1import React from "react"; 1import React from "react";
2import { Link, useNavigate } from "react-router-dom"; 2import { Link, useNavigate } from "react-router-dom";
3 3
4import { ExitIcon, UserIcon, LoginIcon } from "@images/Images"; 4import { ExitIcon, UserIcon, LoginIcon } from "../images/Images";
5import { UserProfile } from "@customTypes/Profile"; 5import { UserProfile } from "@customTypes/Profile";
6import { API } from "@api/Api"; 6import { API } from "@api/Api";
7import "@css/Login.css";
8 7
9interface LoginProps { 8interface LoginProps {
10 setToken: React.Dispatch<React.SetStateAction<string | undefined>>; 9 setToken: React.Dispatch<React.SetStateAction<string | undefined>>;
11 profile?: UserProfile; 10 profile?: UserProfile;
12 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>; 11 setProfile: React.Dispatch<React.SetStateAction<UserProfile | undefined>>;
12 isOpen: boolean;
13} 13}
14 14
15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => { 15const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile, isOpen }) => {
16 const navigate = useNavigate(); 16 const navigate = useNavigate();
17 17
18 const _login = () => { 18 const _login = () => {
@@ -32,16 +32,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
32 <> 32 <>
33 {profile.profile ? ( 33 {profile.profile ? (
34 <> 34 <>
35 <Link to="/profile" tabIndex={-1} className="login"> 35 <Link to="/profile" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
36 <button className="sidebar-button"> 36 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
37 <img 37 <img
38 className="avatar-img" 38 className="rounded-[50px]"
39 src={profile.avatar_link} 39 src={profile.avatar_link}
40 alt="" 40 alt=""
41 /> 41 />
42 <span>{profile.user_name}</span> 42 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">{profile.user_name}</span>
43 </button> 43 </button>
44 <button className="logout-button" onClick={_logout}> 44 <button className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent" onClick={_logout}>
45 <img src={ExitIcon} alt="" /> 45 <img src={ExitIcon} alt="" />
46 <span /> 46 <span />
47 </button> 47 </button>
@@ -49,16 +49,16 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
49 </> 49 </>
50 ) : ( 50 ) : (
51 <> 51 <>
52 <Link to="/" tabIndex={-1} className="login"> 52 <Link to="/" tabIndex={-1} className="grid grid-cols-[50px_auto_200px]">
53 <button className="sidebar-button"> 53 <button className="grid grid-cols-[50px_auto] place-items-start text-left bg-inherit cursor-pointer border-none w-[310px] h-10 rounded-[20px] py-[0.3em] px-0 pl-[11px] transition-all duration-300">
54 <img 54 <img
55 className="avatar-img" 55 className="rounded-[50px]"
56 src={profile.avatar_link} 56 src={profile.avatar_link}
57 alt="" 57 alt=""
58 /> 58 />
59 <span>Loading Profile...</span> 59 <span className="font-[--font-barlow-semicondensed-regular] text-lg text-foreground h-8 leading-7 transition-opacity duration-100 max-w-[22ch] overflow-hidden">Loading Profile...</span>
60 </button> 60 </button>
61 <button disabled className="logout-button" onClick={_logout}> 61 <button disabled className="relative left-[210px] w-[50px] !pl-[10px] !bg-transparent hidden" onClick={_logout}>
62 <img src={ExitIcon} alt="" /> 62 <img src={ExitIcon} alt="" />
63 <span /> 63 <span />
64 </button> 64 </button>
@@ -67,11 +67,28 @@ const Login: React.FC<LoginProps> = ({ setToken, profile, setProfile }) => {
67 )} 67 )}
68 </> 68 </>
69 ) : ( 69 ) : (
70 <Link to="/api/v1/login" tabIndex={-1} className="login"> 70 <Link to="/api/v1/login" tabIndex={-1}>
71 <button className="sidebar-button" onClick={_login}> 71 <button
72 <img className="avatar-img" src={UserIcon} alt="" /> 72 className={`${
73 <span> 73 isOpen
74 <img src={LoginIcon} alt="Sign in through Steam" /> 74 ? "grid grid-cols-[50px_auto] place-items-start pl-[11px]"
75 : "flex items-center justify-center"
76 } text-left bg-inherit cursor-pointer border-none w-[310px] h-16 rounded-[20px] py-[0.3em] px-0 transition-all duration-300 ${isOpen ? "text-white" : "text-gray-400"}`}
77 onClick={_login}
78 >
79 <span className={`font-[--font-barlow-semicondensed-regular] text-lg h-12 leading-7 transition-opacity duration-100 ${isOpen ? " overflow-hidden" : ""}`}>
80 {isOpen ? (
81 <div className="bg-neutral-800 p-2 rounded-lg w-64 flex flex-row items-center justifyt-start gap-2 font-semibold">
82 <LoginIcon />
83 <span>
84 Login with Steam
85 </span>
86 </div>
87 ) : (
88 <div className="bg-neutral-800 p-2 rounded-lg w-">
89 <LoginIcon />
90 </div>
91 )}
75 </span> 92 </span>
76 </button> 93 </button>
77 </Link> 94 </Link>
diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx
index 618d1a7..a0d7eb7 100644
--- a/frontend/src/components/ModMenu.tsx
+++ b/frontend/src/components/ModMenu.tsx
@@ -5,7 +5,6 @@ import { useNavigate } from "react-router-dom";
5import { MapSummary } from "@customTypes/Map"; 5import { MapSummary } from "@customTypes/Map";
6import { ModMenuContent } from "@customTypes/Content"; 6import { ModMenuContent } from "@customTypes/Content";
7import { API } from "@api/Api"; 7import { API } from "@api/Api";
8import "@css/ModMenu.css";
9import useConfirm from "@hooks/UseConfirm"; 8import useConfirm from "@hooks/UseConfirm";
10 9
11interface ModMenuProps { 10interface ModMenuProps {
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};
diff --git a/frontend/src/components/Summary.tsx b/frontend/src/components/Summary.tsx
index 61e52d4..cdecf30 100644
--- a/frontend/src/components/Summary.tsx
+++ b/frontend/src/components/Summary.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import ReactMarkdown from "react-markdown"; 2import ReactMarkdown from "react-markdown";
3 3
4import { MapSummary } from "@customTypes/Map"; 4import { MapSummary } from "@customTypes/Map";
5import "@css/Maps.css";
6 5
7interface SummaryProps { 6interface SummaryProps {
8 selectedRun: number; 7 selectedRun: number;
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx
index d5eabcd..0034019 100644
--- a/frontend/src/components/UploadRunDialog.tsx
+++ b/frontend/src/components/UploadRunDialog.tsx
@@ -2,7 +2,6 @@ import React from "react";
2import { UploadRunContent } from "@customTypes/Content"; 2import { UploadRunContent } from "@customTypes/Content";
3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp"; 3import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from "@nekz/sdp";
4 4
5import "@css/UploadRunDialog.css";
6import { Game } from "@customTypes/Game"; 5import { Game } from "@customTypes/Game";
7import { API } from "@api/Api"; 6import { API } from "@api/Api";
8import { useNavigate } from "react-router-dom"; 7import { useNavigate } from "react-router-dom";