diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 21b27e7..9b12ad3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import { configureNotifications, notify } from "./lib/notifications"; import { setHintsEnabled } from "./lib/hints"; import Login from "./components/Login"; import Header from "./components/Header"; +import NavSidebar from "./components/NavSidebar"; import Sidebar from "./components/Sidebar"; import Feed from "./components/Feed"; import Channels, { type ChannelStatusFilter } from "./components/Channels"; @@ -192,32 +193,37 @@ export default function App() { ); return ( -
-
+ setSettingsOpen(true)} onOpenAbout={() => setAboutOpen(true)} - onChangeLanguage={changeLanguage} - onGoToFullHistory={() => { - setChannelFilter("needs_full"); - setPage("channels"); - }} /> - openReleaseNotes(CURRENT_VERSION)} /> -
- {page === "feed" && ( - - )} -
+
+
{ + setChannelFilter("needs_full"); + setPage("channels"); + }} + /> + openReleaseNotes(CURRENT_VERSION)} /> +
+ {page === "feed" && ( + + )} +
{page === "channels" ? ( setWizardOpen(true)} /> )} -
+
+
{settingsOpen && ( void; page: Page; - setPage: (p: Page) => void; - onOpenSettings: () => void; - onOpenAbout: () => void; onChangeLanguage: (code: LangCode) => void; onGoToFullHistory: () => void; }) { const { t, i18n } = useTranslation(); - async function logout() { - await fetch("/auth/logout", { method: "POST", credentials: "include" }); - location.reload(); - } - return (
- - {page === "feed" && ( @@ -101,130 +83,7 @@ export default function Header({
-
- -
); } - -function Avatar({ me, className = "" }: { me: Me; className?: string }) { - return ( - - ); -} - -function AccountMenu({ - me, - logout, - page, - setPage, - onOpenSettings, - onOpenAbout, -}: { - me: Me; - logout: () => void; - page: Page; - setPage: (p: Page) => void; - onOpenSettings: () => void; - onOpenAbout: () => void; -}) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const closeTimer = useRef | null>(null); - - function openNow() { - if (closeTimer.current) clearTimeout(closeTimer.current); - setOpen(true); - } - function closeSoon() { - closeTimer.current = setTimeout(() => setOpen(false), 220); - } - - const itemClass = - "w-full flex items-center gap-2 text-sm px-2 py-2 rounded-lg text-muted hover:text-fg hover:bg-card transition"; - - return ( -
- - - {open && ( -
-
- -
-
- {me.display_name ?? me.email.split("@")[0]} -
-
{me.email}
-
-
- - {me.role === "admin" && ( -
- - {t("header.account.admin")} -
- )} - -
- {page !== "feed" && ( - - )} - {page !== "channels" && ( - - )} - {page !== "playlists" && ( - - )} - {me.role === "admin" && page !== "stats" && ( - - )} - - - -
-
- )} -
- ); -} diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx new file mode 100644 index 0000000..1ea7c09 --- /dev/null +++ b/frontend/src/components/NavSidebar.tsx @@ -0,0 +1,187 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + BarChart3, + ChevronLeft, + ChevronRight, + Home, + Info, + ListVideo, + LogOut, + Settings, + Shield, + Tv, +} from "lucide-react"; +import type { Me } from "../lib/api"; +import type { Page } from "../lib/urlState"; +import AvatarImg from "./Avatar"; + +// Primary app navigation: a collapsible left rail with icon+label entries (Design C). The +// modules used to hide under the avatar dropdown; they now live here. Collapsed, it becomes +// a thin icon-only rail. Account actions sit at the bottom in a popover. +export default function NavSidebar({ + me, + page, + setPage, + onOpenSettings, + onOpenAbout, +}: { + me: Me; + page: Page; + setPage: (p: Page) => void; + onOpenSettings: () => void; + onOpenAbout: () => void; +}) { + const { t } = useTranslation(); + const [collapsed, setCollapsed] = useState( + () => localStorage.getItem("siftlode.navCollapsed") === "1" + ); + const [acctOpen, setAcctOpen] = useState(false); + + function toggleCollapsed() { + setCollapsed((c) => { + const next = !c; + localStorage.setItem("siftlode.navCollapsed", next ? "1" : "0"); + return next; + }); + } + + async function logout() { + await fetch("/auth/logout", { method: "POST", credentials: "include" }); + location.reload(); + } + + const items: { page: Page; icon: typeof Home; label: string }[] = [ + { page: "feed", icon: Home, label: t("header.account.feed") }, + { page: "channels", icon: Tv, label: t("header.account.channels") }, + { page: "playlists", icon: ListVideo, label: t("header.account.playlists") }, + ]; + if (me.role === "admin") + items.push({ page: "stats", icon: BarChart3, label: t("header.account.stats") }); + + const rowBase = + "w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition"; + const name = me.display_name ?? me.email.split("@")[0]; + + return ( + + ); +} diff --git a/frontend/src/i18n/locales/de/nav.json b/frontend/src/i18n/locales/de/nav.json new file mode 100644 index 0000000..5892e3f --- /dev/null +++ b/frontend/src/i18n/locales/de/nav.json @@ -0,0 +1,5 @@ +{ + "primary": "Hauptnavigation", + "collapse": "Seitenleiste einklappen", + "expand": "Seitenleiste ausklappen" +} diff --git a/frontend/src/i18n/locales/en/nav.json b/frontend/src/i18n/locales/en/nav.json new file mode 100644 index 0000000..621159b --- /dev/null +++ b/frontend/src/i18n/locales/en/nav.json @@ -0,0 +1,5 @@ +{ + "primary": "Primary navigation", + "collapse": "Collapse sidebar", + "expand": "Expand sidebar" +} diff --git a/frontend/src/i18n/locales/hu/nav.json b/frontend/src/i18n/locales/hu/nav.json new file mode 100644 index 0000000..5c18135 --- /dev/null +++ b/frontend/src/i18n/locales/hu/nav.json @@ -0,0 +1,5 @@ +{ + "primary": "Fő navigáció", + "collapse": "Oldalsáv összecsukása", + "expand": "Oldalsáv kinyitása" +}