From b1fd0cdeff287e4cf0ec180ff9fee804cb2c8b8d Mon Sep 17 00:00:00 2001
From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com>
Date: Wed, 22 Oct 2025 01:52:05 +0400
Subject: feat/frontend: add general mobile support (#275)

---
 frontend/src/components/Sidebar.tsx | 174 ++++++++++++++++++++++--------------
 1 file changed, 106 insertions(+), 68 deletions(-)

(limited to 'frontend/src/components/Sidebar.tsx')

diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 67f7f3d..f972c6f 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -19,7 +19,9 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
 
   const [searchData, setSearchData] = React.useState<Search | undefined>(undefined);
   const [isSidebarLocked, setIsSidebarLocked] = React.useState<boolean>(false);
-  const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(true);
+  const [isSidebarOpen, setSidebarOpen] = React.useState<boolean>(false);
+  const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState<boolean>(false);
+  const [isMobileSearchOpen, setIsMobileSearchOpen] = React.useState<boolean>(false);
 
   const location = useLocation();
   const path = location.pathname;
@@ -88,18 +90,34 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
   };
 
   const _handle_sidebar_lock = () => {
-    if (!isSidebarLocked) {
-      _handle_sidebar_hide()
-      setIsSidebarLocked(true);
-      setTimeout(() => setIsSidebarLocked(false), 300);
+    if (window.innerWidth <= 768) {
+      setIsMobileSearchOpen(!isMobileSearchOpen);
+    } else {
+      if (!isSidebarLocked) {
+        _handle_sidebar_hide()
+        setIsSidebarLocked(true);
+        setTimeout(() => setIsSidebarLocked(false), 300);
+      }
     }
   };
 
+  const _close_mobile_search = () => {
+    setIsMobileSearchOpen(false);
+  };
+
   const _handle_search_change = async (q: string) => {
     const searchResponse = await API.get_search(q);
     setSearchData(searchResponse);
   };
 
+  const _toggle_mobile_menu = () => {
+    setIsMobileMenuOpen(!isMobileMenuOpen);
+  };
+
+  const _close_mobile_menu = () => {
+    setIsMobileMenuOpen(false);
+  };
+
   React.useEffect(() => {
     if (path === "/") { handle_sidebar_click(1) }
     else if (path.includes("games")) { handle_sidebar_click(2) }
@@ -112,90 +130,110 @@ const Sidebar: React.FC<SidebarProps> = ({ setToken, profile, setProfile, onUplo
   }, [path]);
 
   return (
-    <div id='sidebar'>
-      <Link to="/" tabIndex={-1}>
-        <div id='logo'> {/* logo */}
-          <img src={LogoIcon} alt="" height={"80px"} />
-          <div id='logo-text'>
-            <span><b>PORTAL 2</b></span><br />
-            <span>Least Portals Hub</span>
+    <>
+      <div id='mobile-topbar'>
+        <Link to="/" tabIndex={-1}>
+          <div id='mobile-logo'>
+            <img src={LogoIcon} alt="" height={"50px"} />
+            <div id='mobile-logo-text'>
+              <span><b>LPHUB</b></span>
+            </div>
           </div>
-        </div>
-      </Link>
-      <div id='sidebar-list'> {/* List */}
-        <div id='sidebar-toplist'> {/* Top */}
+        </Link>
+        <button id='hamburger-menu' onClick={_toggle_mobile_menu} className={isMobileMenuOpen ? 'open' : ''}>
+          <span></span>
+          <span></span>
+          <span></span>
+        </button>
+      </div>
+      <div id='sidebar' className={isMobileMenuOpen ? 'mobile-open' : ''}>
+        <Link to="/" tabIndex={-1}>
+          <div id='logo'> {/* logo */}
+            <img src={LogoIcon} alt="" height={"80px"} />
+            <div id='logo-text'>
+              <span><b>PORTAL 2</b></span><br />
+              <span>Least Portals Hub</span>
+            </div>
+          </div>
+        </Link>
+        <div id='sidebar-list'> {/* List */}
+          <div id='sidebar-toplist'> {/* Top */}
 
-          <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
+            <button className='sidebar-button' onClick={() => _handle_sidebar_lock()}><img src={SearchIcon} alt="" /><span>Search</span></button>
 
-          <span></span>
+            <span></span>
 
-          <Link to="/" tabIndex={-1}>
-            <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
-          </Link>
+            <Link to="/" tabIndex={-1} onClick={_close_mobile_menu}>
+              <button className='sidebar-button'><img src={HomeIcon} alt="homepage" /><span>Home&nbsp;Page</span></button>
+            </Link>
 
-          <Link to="/games" tabIndex={-1}>
-            <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button>
-          </Link>
+            <Link to="/games" tabIndex={-1} onClick={_close_mobile_menu}>
+              <button className='sidebar-button'><img src={PortalIcon} alt="games" /><span>Games</span></button>
+            </Link>
 
-          <Link to="/rankings" tabIndex={-1}>
-            <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button>
-          </Link>
+            <Link to="/rankings" tabIndex={-1} onClick={_close_mobile_menu}>
+              <button className='sidebar-button'><img src={FlagIcon} alt="rankings" /><span>Rankings</span></button>
+            </Link>
 
-          {/* <Link to="/news" tabIndex={-1}>
+            {/* <Link to="/news" tabIndex={-1}>
             <button className='sidebar-button'><img src={NewsIcon} alt="news" /><span>News</span></button>
           </Link> */}
 
-          {/* <Link to="/scorelog" tabIndex={-1}>
+            {/* <Link to="/scorelog" tabIndex={-1}>
             <button className='sidebar-button'><img src={TableIcon} alt="scorelogs" /><span>Score&nbsp;Logs</span></button>
           </Link> */}
-        </div>
-        <div id='sidebar-bottomlist'>
-          <span></span>
+          </div>
+          <div id='sidebar-bottomlist'>
+            <span></span>
 
-          {
-            profile && profile.profile ?
-              <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button>
-              : 
-              <span></span>
-          }
+            {
+              profile && profile.profile ?
+                <button id='upload-run' className='submit-run-button' onClick={() => onUploadRun()}><img src={UploadIcon} alt="upload" /><span>Upload&nbsp;Record</span></button>
+                :
+                <span></span>
+            }
 
-          <Login setToken={setToken} profile={profile} setProfile={setProfile} />
+            <Login setToken={setToken} profile={profile} setProfile={setProfile} />
 
-          <Link to="/rules" tabIndex={-1}>
-            <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button>
-          </Link>
+            <Link to="/rules" tabIndex={-1} onClick={_close_mobile_menu}>
+              <button className='sidebar-button'><img src={BookIcon} alt="rules" /><span>Leaderboard&nbsp;Rules</span></button>
+            </Link>
 
-          <Link to="/about" tabIndex={-1}>
-            <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button>
-          </Link>
+            <Link to="/about" tabIndex={-1} onClick={_close_mobile_menu}>
+              <button className='sidebar-button'><img src={HelpIcon} alt="about" /><span>About&nbsp;LPHUB</span></button>
+            </Link>
+          </div>
         </div>
-      </div>
-      <div>
-        <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
+        <div id='search-panel' className={isMobileSearchOpen ? 'mobile-search-open' : ''}>
+          <input type="text" id='searchbar' placeholder='Search for map or a player...' onChange={(e) => _handle_search_change(e.target.value)} />
+          <div className='mobile-search-header'>
+            <button className='mobile-search-close' onClick={_close_mobile_search}>✕</button>
+          </div>
 
-        <div id='search-data'>
-
-          {searchData?.maps.map((q, index) => (
-            <Link to={`/maps/${q.id}`} className='search-map' key={index}>
-              <span>{q.game}</span>
-              <span>{q.chapter}</span>
-              <span>{q.map}</span>
-            </Link>
-          ))}
-          {searchData?.players.map((q, index) =>
-          (
-            <Link to={
-              profile && q.steam_id === profile.steam_id ? `/profile` :
-                `/users/${q.steam_id}`
-            } className='search-player' key={index}>
-              <img src={q.avatar_link} alt='pfp'></img>
-              <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
-            </Link>
-          ))}
+          <div id='search-data'>
+
+            {searchData?.maps.map((q, index) => (
+              <Link to={`/maps/${q.id}`} className='search-map' key={index} onClick={_close_mobile_search}>
+                <span>{q.game}</span>
+                <span>{q.chapter}</span>
+                <span>{q.map}</span>
+              </Link>
+            ))}
+            {searchData?.players.map((q, index) =>
+            (
+              <Link to={
+                profile && q.steam_id === profile.steam_id ? `/profile` :
+                  `/users/${q.steam_id}`
+              } className='search-player' key={index} onClick={_close_mobile_search}>
+                <img src={q.avatar_link} alt='pfp'></img>
+                <span style={{ fontSize: `${36 - q.user_name.length * 0.8}px` }}>{q.user_name}</span>
+              </Link>
+            ))}
 
+          </div>
         </div>
       </div>
-    </div>
+    </>
   );
 };
 
-- 
cgit v1.2.3

