);
}
-
diff --git a/src/components/admin/admin-mobile-wrapper.tsx b/src/components/admin/admin-mobile-wrapper.tsx
index a695cfa2..d091414e 100644
--- a/src/components/admin/admin-mobile-wrapper.tsx
+++ b/src/components/admin/admin-mobile-wrapper.tsx
@@ -84,7 +84,7 @@ export function AdminMobileWrapper({
{/* Sidebar */}
pathname === p || pathname.startsWith(`${p}/`),
- )) {
+function itemIsActive(pathname: string, item: AdminNavItem): boolean {
+ if (
+ item.matchExcludePrefixes?.some(
+ (p) => pathname === p || pathname.startsWith(`${p}/`),
+ )
+ ) {
return false;
}
if (item.href === "/admin") return pathname === "/admin";
@@ -45,12 +44,23 @@ function readCollapsed(): Record
{
}
}
+function persistCollapsed(next: Record) {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
+ } catch {
+ /* ignore quota */
+ }
+}
+
export function AdminSidebarNav() {
const t = useTranslations("pages.admin.nav");
const pathname = usePathname() ?? "";
const [collapsed, setCollapsed] = useState>({});
const [hydrated, setHydrated] = useState(false);
+ const groups = useMemo(() => ADMIN_NAV_GROUPS, []);
+ const allKeys = useMemo(() => groups.map((g) => g.labelKey), [groups]);
+
useEffect(() => {
setCollapsed(readCollapsed());
setHydrated(true);
@@ -59,9 +69,9 @@ export function AdminSidebarNav() {
// 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);
+ const activeKeys = groups
+ .filter((g) => groupHasActive(pathname, g.items))
+ .map((g) => g.labelKey);
if (activeKeys.length === 0) return;
setCollapsed((prev) => {
let changed = false;
@@ -73,91 +83,156 @@ export function AdminSidebarNav() {
}
}
if (!changed) return prev;
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
- } catch {
- /* ignore quota */
- }
+ persistCollapsed(next);
return next;
});
- }, [pathname, hydrated]);
+ }, [pathname, hydrated, groups]);
const toggle = useCallback((labelKey: string) => {
setCollapsed((prev) => {
const next = { ...prev, [labelKey]: !prev[labelKey] };
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
- } catch {
- /* ignore quota */
- }
+ persistCollapsed(next);
return next;
});
}, []);
- const groups = useMemo(() => ADMIN_NAV_GROUPS, []);
+ const expandAll = useCallback(() => {
+ const next: Record = {};
+ for (const key of allKeys) next[key] = false;
+ persistCollapsed(next);
+ setCollapsed(next);
+ }, [allKeys]);
+
+ const collapseAll = useCallback(() => {
+ const next: Record = {};
+ for (const key of allKeys) next[key] = true;
+ // Keep the active route's group open so you never lose context.
+ for (const g of groups) {
+ if (groupHasActive(pathname, g.items)) next[g.labelKey] = false;
+ }
+ persistCollapsed(next);
+ setCollapsed(next);
+ }, [allKeys, groups, pathname]);
+
+ const allExpanded = hydrated && allKeys.every((k) => !collapsed[k]);
+ const canCollapseMore =
+ hydrated &&
+ allKeys.some((k) => {
+ const g = groups.find((x) => x.labelKey === k);
+ if (g && groupHasActive(pathname, g.items)) return false;
+ return !collapsed[k];
+ });
return (
-