feat: rebuild Layout with collapsible sidebar topbar toggles and Supabase link
Browse filesThe shell now mirrors YouTube more closely with a sectioned sidebar, a search field, language and theme switches in the topbar, and a hamburger that collapses the sidebar on desktop or opens it as a drawer on mobile. Adds a Database entry that points to the Supabase editor so the team can jump straight to the data.
- frontend/src/App.tsx +17 -11
- frontend/src/components/Layout.tsx +163 -15
frontend/src/App.tsx
CHANGED
|
@@ -1,22 +1,28 @@
|
|
| 1 |
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
| 2 |
import { Layout } from "./components/Layout";
|
| 3 |
import { AppProvider } from "./context/AppContext";
|
|
|
|
|
|
|
| 4 |
import { HubPage } from "./pages/HubPage";
|
| 5 |
import { SettingsPage } from "./pages/SettingsPage";
|
| 6 |
import { WatchPage } from "./pages/WatchPage";
|
| 7 |
|
| 8 |
export default function App() {
|
| 9 |
return (
|
| 10 |
-
<
|
| 11 |
-
<
|
| 12 |
-
<
|
| 13 |
-
<
|
| 14 |
-
<
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
);
|
| 22 |
}
|
|
|
|
| 1 |
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
| 2 |
import { Layout } from "./components/Layout";
|
| 3 |
import { AppProvider } from "./context/AppContext";
|
| 4 |
+
import { ThemeProvider } from "./context/ThemeContext";
|
| 5 |
+
import { I18nProvider } from "./i18n/I18nContext";
|
| 6 |
import { HubPage } from "./pages/HubPage";
|
| 7 |
import { SettingsPage } from "./pages/SettingsPage";
|
| 8 |
import { WatchPage } from "./pages/WatchPage";
|
| 9 |
|
| 10 |
export default function App() {
|
| 11 |
return (
|
| 12 |
+
<ThemeProvider>
|
| 13 |
+
<I18nProvider>
|
| 14 |
+
<AppProvider>
|
| 15 |
+
<BrowserRouter>
|
| 16 |
+
<Routes>
|
| 17 |
+
<Route element={<Layout />}>
|
| 18 |
+
<Route index element={<WatchPage />} />
|
| 19 |
+
<Route path="hub" element={<HubPage />} />
|
| 20 |
+
<Route path="settings" element={<SettingsPage />} />
|
| 21 |
+
</Route>
|
| 22 |
+
</Routes>
|
| 23 |
+
</BrowserRouter>
|
| 24 |
+
</AppProvider>
|
| 25 |
+
</I18nProvider>
|
| 26 |
+
</ThemeProvider>
|
| 27 |
);
|
| 28 |
}
|
frontend/src/components/Layout.tsx
CHANGED
|
@@ -1,25 +1,173 @@
|
|
|
|
|
| 1 |
import { NavLink, Outlet } from "react-router-dom";
|
| 2 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
export function Layout() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
return (
|
| 6 |
-
<div className="app-shell">
|
| 7 |
-
<nav className=
|
| 8 |
-
<
|
| 9 |
-
<img src="/signalmod_logo.png" alt="SignalMod" className="
|
| 10 |
-
</div>
|
| 11 |
-
<NavLink to="/" end className={({ isActive }) => (isActive ? "nav active" : "nav")}>
|
| 12 |
-
Watch
|
| 13 |
-
</NavLink>
|
| 14 |
-
<NavLink to="/hub" className={({ isActive }) => (isActive ? "nav active" : "nav")}>
|
| 15 |
-
Moderator Hub
|
| 16 |
-
</NavLink>
|
| 17 |
-
<NavLink to="/settings" className={({ isActive }) => (isActive ? "nav active" : "nav")}>
|
| 18 |
-
Settings
|
| 19 |
</NavLink>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
<main className="main-content">
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
<Outlet />
|
| 24 |
</main>
|
| 25 |
</div>
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
import { NavLink, Outlet } from "react-router-dom";
|
| 3 |
+
import { useI18n } from "../i18n/I18nContext";
|
| 4 |
+
import { useTheme } from "../context/ThemeContext";
|
| 5 |
+
import {
|
| 6 |
+
DatabaseIcon,
|
| 7 |
+
GlobeIcon,
|
| 8 |
+
HistoryIcon,
|
| 9 |
+
HomeIcon,
|
| 10 |
+
HubIcon,
|
| 11 |
+
LibraryIcon,
|
| 12 |
+
MenuIcon,
|
| 13 |
+
MoonIcon,
|
| 14 |
+
SearchIcon,
|
| 15 |
+
SettingsIcon,
|
| 16 |
+
ShieldIcon,
|
| 17 |
+
ShortsIcon,
|
| 18 |
+
SubsIcon,
|
| 19 |
+
SunIcon,
|
| 20 |
+
WatchIcon,
|
| 21 |
+
YourVideosIcon,
|
| 22 |
+
} from "./icons";
|
| 23 |
+
|
| 24 |
+
const DATABASE_URL =
|
| 25 |
+
"https://supabase.com/dashboard/project/kxzxqlhdzkpcanotdbgl/editor/17547?schema=public&sort=created_at%3Adesc";
|
| 26 |
|
| 27 |
export function Layout() {
|
| 28 |
+
const { t, toggleLocale } = useI18n();
|
| 29 |
+
const { theme, toggleTheme } = useTheme();
|
| 30 |
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
| 31 |
+
const [collapsed, setCollapsed] = useState(false);
|
| 32 |
+
|
| 33 |
+
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
| 34 |
+
isActive ? "nav-item active" : "nav-item";
|
| 35 |
+
|
| 36 |
+
const closeMobile = () => setSidebarOpen(false);
|
| 37 |
+
|
| 38 |
+
const sidebarClass = [
|
| 39 |
+
"sidebar",
|
| 40 |
+
sidebarOpen ? "open" : "",
|
| 41 |
+
collapsed ? "collapsed" : "",
|
| 42 |
+
]
|
| 43 |
+
.filter(Boolean)
|
| 44 |
+
.join(" ");
|
| 45 |
+
|
| 46 |
return (
|
| 47 |
+
<div className={collapsed ? "app-shell collapsed-shell" : "app-shell"}>
|
| 48 |
+
<nav className={sidebarClass}>
|
| 49 |
+
<NavLink to="/" className="brand" onClick={closeMobile} aria-label="SignalMod">
|
| 50 |
+
<img src="/signalmod_logo.png" alt="SignalMod" className="brand-logo" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
</NavLink>
|
| 52 |
+
|
| 53 |
+
<div className="sidebar-section">
|
| 54 |
+
<NavLink to="/" end className={navLinkClass} onClick={closeMobile} title={t.nav.home}>
|
| 55 |
+
<span className="nav-icon"><HomeIcon /></span>
|
| 56 |
+
<span className="nav-label">{t.nav.home}</span>
|
| 57 |
+
</NavLink>
|
| 58 |
+
<button className="nav-item" type="button" disabled title={t.nav.shorts}>
|
| 59 |
+
<span className="nav-icon"><ShortsIcon /></span>
|
| 60 |
+
<span className="nav-label">{t.nav.shorts}</span>
|
| 61 |
+
</button>
|
| 62 |
+
<button className="nav-item" type="button" disabled title={t.nav.subscriptions}>
|
| 63 |
+
<span className="nav-icon"><SubsIcon /></span>
|
| 64 |
+
<span className="nav-label">{t.nav.subscriptions}</span>
|
| 65 |
+
</button>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="sidebar-section">
|
| 69 |
+
<button className="nav-item" type="button" title={t.nav.library}>
|
| 70 |
+
<span className="nav-icon"><LibraryIcon /></span>
|
| 71 |
+
<span className="nav-label">{t.nav.library}</span>
|
| 72 |
+
</button>
|
| 73 |
+
<button className="nav-item" type="button" title={t.nav.history}>
|
| 74 |
+
<span className="nav-icon"><HistoryIcon /></span>
|
| 75 |
+
<span className="nav-label">{t.nav.history}</span>
|
| 76 |
+
</button>
|
| 77 |
+
<button className="nav-item" type="button" title={t.nav.yourVideos}>
|
| 78 |
+
<span className="nav-icon"><YourVideosIcon /></span>
|
| 79 |
+
<span className="nav-label">{t.nav.yourVideos}</span>
|
| 80 |
+
</button>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div className="sidebar-section">
|
| 84 |
+
<h3 className="sidebar-heading">
|
| 85 |
+
<span className="nav-icon"><ShieldIcon /></span>
|
| 86 |
+
<span className="nav-label">{t.nav.moderation}</span>
|
| 87 |
+
</h3>
|
| 88 |
+
<NavLink to="/" end className={navLinkClass} onClick={closeMobile} title={t.nav.watch}>
|
| 89 |
+
<span className="nav-icon"><WatchIcon /></span>
|
| 90 |
+
<span className="nav-label">{t.nav.watch}</span>
|
| 91 |
+
</NavLink>
|
| 92 |
+
<NavLink to="/hub" className={navLinkClass} onClick={closeMobile} title={t.nav.hub}>
|
| 93 |
+
<span className="nav-icon"><HubIcon /></span>
|
| 94 |
+
<span className="nav-label">{t.nav.hub}</span>
|
| 95 |
+
</NavLink>
|
| 96 |
+
<NavLink to="/settings" className={navLinkClass} onClick={closeMobile} title={t.nav.settings}>
|
| 97 |
+
<span className="nav-icon"><SettingsIcon /></span>
|
| 98 |
+
<span className="nav-label">{t.nav.settings}</span>
|
| 99 |
+
</NavLink>
|
| 100 |
+
<a
|
| 101 |
+
className="nav-item"
|
| 102 |
+
href={DATABASE_URL}
|
| 103 |
+
target="_blank"
|
| 104 |
+
rel="noopener noreferrer"
|
| 105 |
+
onClick={closeMobile}
|
| 106 |
+
title={t.nav.database}
|
| 107 |
+
>
|
| 108 |
+
<span className="nav-icon"><DatabaseIcon /></span>
|
| 109 |
+
<span className="nav-label">{t.nav.database}</span>
|
| 110 |
+
</a>
|
| 111 |
+
</div>
|
| 112 |
</nav>
|
| 113 |
+
|
| 114 |
+
{sidebarOpen && (
|
| 115 |
+
<div className="sidebar-backdrop" onClick={closeMobile} />
|
| 116 |
+
)}
|
| 117 |
+
|
| 118 |
<main className="main-content">
|
| 119 |
+
<header className="topbar">
|
| 120 |
+
<div className="topbar-left">
|
| 121 |
+
<button
|
| 122 |
+
className="icon-btn"
|
| 123 |
+
type="button"
|
| 124 |
+
onClick={() => {
|
| 125 |
+
if (window.matchMedia("(max-width: 820px)").matches) {
|
| 126 |
+
setSidebarOpen((v) => !v);
|
| 127 |
+
} else {
|
| 128 |
+
setCollapsed((v) => !v);
|
| 129 |
+
}
|
| 130 |
+
}}
|
| 131 |
+
aria-label={t.topbar.toggleSidebar}
|
| 132 |
+
title={t.topbar.toggleSidebar}
|
| 133 |
+
>
|
| 134 |
+
<MenuIcon />
|
| 135 |
+
</button>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div className="topbar-search">
|
| 139 |
+
<SearchIcon />
|
| 140 |
+
<input
|
| 141 |
+
placeholder={t.topbar.searchPlaceholder}
|
| 142 |
+
aria-label={t.topbar.searchPlaceholder}
|
| 143 |
+
/>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<div className="topbar-right">
|
| 147 |
+
<button
|
| 148 |
+
className="lang-toggle"
|
| 149 |
+
type="button"
|
| 150 |
+
onClick={toggleLocale}
|
| 151 |
+
title={t.topbar.toggleLanguage}
|
| 152 |
+
aria-label={t.topbar.toggleLanguage}
|
| 153 |
+
>
|
| 154 |
+
<GlobeIcon /> {t.topbar.languageLabel}
|
| 155 |
+
</button>
|
| 156 |
+
<button
|
| 157 |
+
className="theme-toggle"
|
| 158 |
+
type="button"
|
| 159 |
+
onClick={toggleTheme}
|
| 160 |
+
title={t.topbar.toggleTheme}
|
| 161 |
+
aria-label={t.topbar.toggleTheme}
|
| 162 |
+
>
|
| 163 |
+
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
|
| 164 |
+
</button>
|
| 165 |
+
<button className="sign-in" type="button">
|
| 166 |
+
{t.topbar.signIn}
|
| 167 |
+
</button>
|
| 168 |
+
</div>
|
| 169 |
+
</header>
|
| 170 |
+
|
| 171 |
<Outlet />
|
| 172 |
</main>
|
| 173 |
</div>
|