feat: enhance sidebar navigation with localStorage state management and section key support
This commit is contained in:
@@ -66,6 +66,7 @@ export function AppSidebar({ ...props }: ComponentProps<typeof Sidebar>) {
|
|||||||
<NavMain
|
<NavMain
|
||||||
key={section.titleKey}
|
key={section.titleKey}
|
||||||
icon={section.icon}
|
icon={section.icon}
|
||||||
|
sectionKey={section.titleKey}
|
||||||
title={t(section.titleKey)}
|
title={t(section.titleKey)}
|
||||||
items={section.items.map((item) => ({
|
items={section.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { useEffect, useState } from "react"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
dashboardNavSectionHasActiveItem,
|
dashboardNavSectionHasActiveItem,
|
||||||
|
getDashboardNavSectionStorageKey,
|
||||||
isDashboardNavItemActive,
|
isDashboardNavItemActive,
|
||||||
|
parseDashboardNavSectionOpenState,
|
||||||
} from "@/lib/navigation-active"
|
} from "@/lib/navigation-active"
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -26,10 +28,12 @@ import {
|
|||||||
|
|
||||||
export function NavMain({
|
export function NavMain({
|
||||||
icon,
|
icon,
|
||||||
|
sectionKey,
|
||||||
title,
|
title,
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
items: ReadonlyArray<{
|
items: ReadonlyArray<{
|
||||||
title: string
|
title: string
|
||||||
@@ -39,20 +43,38 @@ export function NavMain({
|
|||||||
}) {
|
}) {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const hasActiveItem = dashboardNavSectionHasActiveItem(items, pathname)
|
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(() => {
|
useEffect(() => {
|
||||||
|
const storedOpen = parseDashboardNavSectionOpenState(
|
||||||
|
window.localStorage.getItem(storageKey)
|
||||||
|
)
|
||||||
|
if (storedOpen !== undefined) {
|
||||||
|
setOpen(storedOpen)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (hasActiveItem) {
|
if (hasActiveItem) {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}
|
}
|
||||||
}, [hasActiveItem])
|
}, [hasActiveItem, storageKey])
|
||||||
|
|
||||||
|
const handleOpenChange = (nextOpen: boolean) => {
|
||||||
|
setOpen(nextOpen)
|
||||||
|
window.localStorage.setItem(storageKey, String(nextOpen))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="px-2 py-0 first:pt-2 last:pb-2">
|
<SidebarGroup className="px-2 py-0 first:pt-2 last:pb-2">
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={handleOpenChange}
|
||||||
className="group/collapsible"
|
className="group/collapsible"
|
||||||
render={<SidebarMenuItem />}
|
render={<SidebarMenuItem />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -63,3 +63,23 @@ describe("dashboardNavSectionHasActiveItem", () => {
|
|||||||
assert.equal(dashboardNavSectionHasActiveItem(items, "/dashboard/users"), false)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export type DashboardNavActiveItem = {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DASHBOARD_NAV_SECTION_STORAGE_PREFIX = "dashboard.sidebar.navSection"
|
||||||
|
|
||||||
function normalizePath(path: string) {
|
function normalizePath(path: string) {
|
||||||
return path !== "/" ? path.replace(/\/+$/, "") : path
|
return path !== "/" ? path.replace(/\/+$/, "") : path
|
||||||
}
|
}
|
||||||
@@ -22,3 +24,17 @@ export function dashboardNavSectionHasActiveItem(
|
|||||||
) {
|
) {
|
||||||
return items.some((item) => isDashboardNavItemActive(pathname, item.url))
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user