diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 9508fc45..926fa3b2 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -5,12 +5,11 @@ import { getTranslations } from "next-intl/server";
import type { ReactNode } from "react";
import { AdminHubChrome } from "@/components/admin/admin-hub-chrome";
import { AdminMobileWrapper } from "@/components/admin/admin-mobile-wrapper";
-import { AdminNavLink } from "@/components/admin/admin-nav-link";
+import { AdminSidebarNav } from "@/components/admin/admin-sidebar-nav";
import { AdminTopbar } from "@/components/admin/admin-topbar";
import { LanguageSwitcher } from "@/components/language-switcher";
import { ThemeSwitcher } from "@/components/theme-switcher";
import { requireStaff } from "@/lib/admin/guard";
-import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav";
import { setCsrfCookie } from "@/lib/foundation/security";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
@@ -94,40 +93,7 @@ async function Sidebar({
-
+
pathname === p || pathname.startsWith(`${p}/`),
+ )) {
+ return false;
+ }
+ if (item.href === "/admin") return pathname === "/admin";
+ if (item.matchPrefixes?.length) {
+ return item.matchPrefixes.some(
+ (p) => pathname === p || pathname.startsWith(`${p}/`),
+ );
+ }
+ return pathname === item.href || pathname.startsWith(`${item.href}/`);
+}
+
+function groupHasActive(pathname: string, items: AdminNavItem[]): boolean {
+ return items.some((item) => itemIsActive(pathname, item));
+}
+
+function readCollapsed(): Record
{
+ if (typeof window === "undefined") return {};
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (!raw) return {};
+ const parsed = JSON.parse(raw) as unknown;
+ if (!parsed || typeof parsed !== "object") return {};
+ return parsed as Record;
+ } catch {
+ return {};
+ }
+}
+
+export function AdminSidebarNav() {
+ const t = useTranslations("pages.admin.nav");
+ const pathname = usePathname() ?? "";
+ const [collapsed, setCollapsed] = useState>({});
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ setCollapsed(readCollapsed());
+ setHydrated(true);
+ }, []);
+
+ // Keep the group that contains the current page open.
+ useEffect(() => {
+ if (!hydrated) return;
+ const activeKeys = ADMIN_NAV_GROUPS.filter((g) =>
+ groupHasActive(pathname, g.items),
+ ).map((g) => g.labelKey);
+ if (activeKeys.length === 0) return;
+ setCollapsed((prev) => {
+ let changed = false;
+ const next = { ...prev };
+ for (const key of activeKeys) {
+ if (next[key]) {
+ next[key] = false;
+ changed = true;
+ }
+ }
+ if (!changed) return prev;
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
+ } catch {
+ /* ignore quota */
+ }
+ return next;
+ });
+ }, [pathname, hydrated]);
+
+ const toggle = useCallback((labelKey: string) => {
+ setCollapsed((prev) => {
+ const next = { ...prev, [labelKey]: !prev[labelKey] };
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
+ } catch {
+ /* ignore quota */
+ }
+ return next;
+ });
+ }, []);
+
+ const groups = useMemo(() => ADMIN_NAV_GROUPS, []);
+
+ return (
+
+ );
+}