From edb8355aad6d35d67859cbd0962c1659280e9ae0 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sat, 30 May 2026 20:06:34 +0800 Subject: [PATCH] feat: enhance sidebar navigation with localStorage state management and section key support --- web/components/app-sidebar.tsx | 1 + web/components/nav-main.tsx | 28 +++++++++++++++++++++++++--- web/lib/navigation-active.test.mjs | 20 ++++++++++++++++++++ web/lib/navigation-active.ts | 16 ++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/web/components/app-sidebar.tsx b/web/components/app-sidebar.tsx index e8dba5b..6fd81d4 100644 --- a/web/components/app-sidebar.tsx +++ b/web/components/app-sidebar.tsx @@ -66,6 +66,7 @@ export function AppSidebar({ ...props }: ComponentProps) { ({ ...item, diff --git a/web/components/nav-main.tsx b/web/components/nav-main.tsx index ea23f60..1aaf661 100644 --- a/web/components/nav-main.tsx +++ b/web/components/nav-main.tsx @@ -7,7 +7,9 @@ import { useEffect, useState } from "react" import { dashboardNavSectionHasActiveItem, + getDashboardNavSectionStorageKey, isDashboardNavItemActive, + parseDashboardNavSectionOpenState, } from "@/lib/navigation-active" import { Collapsible, @@ -26,10 +28,12 @@ import { export function NavMain({ icon, + sectionKey, title, items, }: { icon?: React.ReactNode + sectionKey: string title: string items: ReadonlyArray<{ title: string @@ -39,20 +43,38 @@ export function NavMain({ }) { const pathname = usePathname() const hasActiveItem = dashboardNavSectionHasActiveItem(items, pathname) - const [open, setOpen] = useState(true) + const storageKey = getDashboardNavSectionStorageKey(sectionKey) + const [open, setOpen] = useState(() => { + if (typeof window === "undefined") { + return true + } + return parseDashboardNavSectionOpenState(window.localStorage.getItem(storageKey)) ?? true + }) useEffect(() => { + const storedOpen = parseDashboardNavSectionOpenState( + window.localStorage.getItem(storageKey) + ) + if (storedOpen !== undefined) { + setOpen(storedOpen) + return + } if (hasActiveItem) { setOpen(true) } - }, [hasActiveItem]) + }, [hasActiveItem, storageKey]) + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen) + window.localStorage.setItem(storageKey, String(nextOpen)) + } return ( } > diff --git a/web/lib/navigation-active.test.mjs b/web/lib/navigation-active.test.mjs index 56efa9c..342ce08 100644 --- a/web/lib/navigation-active.test.mjs +++ b/web/lib/navigation-active.test.mjs @@ -63,3 +63,23 @@ describe("dashboardNavSectionHasActiveItem", () => { assert.equal(dashboardNavSectionHasActiveItem(items, "/dashboard/users"), false) }) }) + +describe("dashboard nav section storage helpers", () => { + it("builds stable localStorage keys from section identifiers", async () => { + const { getDashboardNavSectionStorageKey } = await loadModule() + + assert.equal( + getDashboardNavSectionStorageKey("nav.receptionCenter"), + "dashboard.sidebar.navSection.nav.receptionCenter.open" + ) + }) + + it("parses stored open states and ignores unknown values", async () => { + const { parseDashboardNavSectionOpenState } = await loadModule() + + assert.equal(parseDashboardNavSectionOpenState("true"), true) + assert.equal(parseDashboardNavSectionOpenState("false"), false) + assert.equal(parseDashboardNavSectionOpenState(null), undefined) + assert.equal(parseDashboardNavSectionOpenState("bad"), undefined) + }) +}) diff --git a/web/lib/navigation-active.ts b/web/lib/navigation-active.ts index bbd12fb..ac4d724 100644 --- a/web/lib/navigation-active.ts +++ b/web/lib/navigation-active.ts @@ -2,6 +2,8 @@ export type DashboardNavActiveItem = { url: string } +const DASHBOARD_NAV_SECTION_STORAGE_PREFIX = "dashboard.sidebar.navSection" + function normalizePath(path: string) { return path !== "/" ? path.replace(/\/+$/, "") : path } @@ -22,3 +24,17 @@ export function dashboardNavSectionHasActiveItem( ) { return items.some((item) => isDashboardNavItemActive(pathname, item.url)) } + +export function getDashboardNavSectionStorageKey(sectionKey: string) { + return `${DASHBOARD_NAV_SECTION_STORAGE_PREFIX}.${sectionKey}.open` +} + +export function parseDashboardNavSectionOpenState(value: string | null) { + if (value === "true") { + return true + } + if (value === "false") { + return false + } + return undefined +}