"use client"
// react
import { type ReactNode, useState, useSyncExternalStore } from "react"
// next
import Link from "next/link"
import { usePathname } from "next/navigation"
// shadcn
import { Button } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
// third-party
import { useTheme } from "next-themes"
// project-imports
import branding from "@/branding.json"
import {
hasActiveDescendant,
isNavItemActive,
menuGroups,
type NavItem,
} from "@/components/uiable/blocks/layout/dashboard-layout/data/dashboard-3.data"
// import { LayoutSwitcher } from "@/components/uiable/layout/layout-preference"
import NotificationDropdown from "@/components/uiable/layout/notification-dropdown"
import SearchBar from "@/components/uiable/layout/search-bar"
import Logo from "@/components/uiable/layout/shared/logo"
import UserProfile from "@/components/uiable/layout/user-profile"
// assets
import {
ChevronRight,
LifeBuoy,
Moon,
PanelLeft,
Pin,
PinOff,
Settings,
Sun,
} from "lucide-react"
// assets
const brandFavLogo = "https://cdn.uiable.com/brand/favicon.svg"
// Map each menu group to a rail item: the group icon appears in the icon rail,
// and the group's items populate the secondary panel when selected.
const railItems: NavItem[] = menuGroups.map((group) => ({
id: group.id,
title: group.label,
icon: group.icon,
children: group.items,
}))
const railItemClass = "size-11 rounded-lg"
const railItemActiveClass =
"bg-primary/10 text-primary hover:bg-primary/10 hover:text-primary"
const railItemInactiveClass = "text-muted-foreground hover:text-foreground"
const panelLinkClass =
"justify-start rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors"
const panelLinkActiveClass = "bg-primary/10 text-primary"
const panelLinkInactiveClass =
"text-muted-foreground hover:bg-muted hover:text-foreground"
// ------------------------------ | ICON RAIL | ------------------------------ //
function IconRail({
activeId,
onSelect,
panelOpen,
onTogglePanel,
}: {
activeId: string
onSelect: (item: NavItem) => void
panelOpen: boolean
onTogglePanel: () => void
}) {
return (
<aside className="flex w-16 shrink-0 flex-col items-center border-r border-border/50 bg-background/80">
{/* Below xl the full logo lives in the secondary panel, so show the
compact mark here. At xl+ that space becomes the panel toggle. */}
<div className="flex h-16 w-full shrink-0 items-center justify-center border-b border-border/50">
<img
src={brandFavLogo}
alt={`${branding.brandName} Logo`}
className="size-8 xl:hidden"
/>
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
onClick={onTogglePanel}
aria-pressed={panelOpen}
className={`hidden cursor-pointer ${railItemClass} ${railItemInactiveClass} xl:inline-flex`}
/>
}
>
{panelOpen ? (
<Pin className="size-5.5" />
) : (
<PinOff className="size-5.5" />
)}
<span className="sr-only">
{panelOpen ? "Collapse menu" : "Expand menu"}
</span>
</TooltipTrigger>
<TooltipContent side="right">
<p>{panelOpen ? "Collapse menu" : "Expand menu"}</p>
</TooltipContent>
</Tooltip>
</div>
<nav className="flex flex-1 flex-col items-center gap-1 py-4">
{railItems.map((item) => {
const Icon = item.icon
const isActive = item.id === activeId
return (
<Tooltip key={item.id}>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
onClick={() => onSelect(item)}
className={`${railItemClass} ${
isActive ? railItemActiveClass : railItemInactiveClass
}`}
/>
}
>
{Icon ? <Icon className="size-5.5" /> : null}
</TooltipTrigger>
<TooltipContent side="right">
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
)
})}
</nav>
<div className="flex flex-col items-center gap-2 pb-4">
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
className={`${railItemClass} ${railItemInactiveClass}`}
/>
}
>
<LifeBuoy className="size-5.5" />
</TooltipTrigger>
<TooltipContent side="right">
<p>Support</p>
</TooltipContent>
</Tooltip>
<Button
variant="ghost"
size="icon"
className={`${railItemClass} ${railItemInactiveClass}`}
>
<Settings className="size-5.5" />
</Button>
</div>
</aside>
)
}
// ------------------------------ | PANEL NAV NODE | ------------------------------ //
// Renders a single nav entry in the secondary panel. Leaf items become links;
// parent items (e.g. "Menu levels") become an inline collapsible section.
function PanelNavNode({
node,
activeLinkId,
onSelect,
}: {
node: NavItem
activeLinkId?: string
onSelect: (link: NavItem) => void
}) {
const pathname = usePathname()
if (!node.children?.length) {
const isActive =
node.id === activeLinkId || isNavItemActive(pathname, node.href)
return (
<Button
variant="ghost"
nativeButton={false}
onClick={() => onSelect(node)}
className={`${panelLinkClass} ${
isActive ? panelLinkActiveClass : panelLinkInactiveClass
}`}
render={
<Link
href={node.href ?? "#"}
target={node.target}
rel={node.target === "_blank" ? "noopener noreferrer" : undefined}
/>
}
>
{node.title}
</Button>
)
}
return (
<Collapsible
className="group/collapsible"
defaultOpen={hasActiveDescendant(node, pathname)}
>
<CollapsibleTrigger
render={
<Button
variant="ghost"
className={`w-full ${panelLinkClass} ${
hasActiveDescendant(node, pathname)
? panelLinkActiveClass
: panelLinkInactiveClass
}`}
/>
}
>
<span className="flex-1">{node.title}</span>
<ChevronRight className="ml-auto size-4 transition-transform group-data-open/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-300 ease-out data-ending-style:h-0 data-starting-style:h-0">
<div className="ml-3 flex flex-col gap-0.5 border-l border-border/50 pt-0.5 pl-2">
{node.children.map((child) => (
<PanelNavNode
key={child.id}
node={child}
activeLinkId={activeLinkId}
onSelect={onSelect}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
)
}
// ------------------------------ | PANEL NAV | ------------------------------ //
function PanelNav({
item,
activeLinkId,
onSelect,
}: {
item: NavItem
activeLinkId?: string
onSelect: (link: NavItem) => void
}) {
return (
<nav className="flex flex-col gap-1">
{item.children?.map((node) => (
<PanelNavNode
key={node.id}
node={node}
activeLinkId={activeLinkId}
onSelect={onSelect}
/>
))}
</nav>
)
}
// ------------------------------ | SECONDARY PANEL | ------------------------------ //
function SecondaryPanel({
item,
activeLinkId,
onSelect,
}: {
item: NavItem
activeLinkId?: string
onSelect: (link: NavItem) => void
}) {
return (
<aside className="hidden w-56 shrink-0 flex-col border-r border-border/50 bg-background/40 xl:flex">
<div className="flex h-16 shrink-0 items-center border-b border-border/50 px-4">
<Logo link={false} />
</div>
<div className="flex flex-1 flex-col overflow-y-auto p-3">
<h6 className="mb-3 px-2 pt-2 capitalize">{item.title}</h6>
<PanelNav item={item} activeLinkId={activeLinkId} onSelect={onSelect} />
</div>
</aside>
)
}
// ------------------------------ | MOBILE PANEL | ------------------------------ //
// The secondary panel is hidden on small screens, so expose its links through a
// slide-in sheet triggered from the header.
function MobilePanel({
item,
activeLinkId,
onSelect,
}: {
item: NavItem
activeLinkId?: string
onSelect: (link: NavItem) => void
}) {
const [open, setOpen] = useState(false)
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-primary xl:hidden">
<PanelLeft className="size-5" />
<span className="sr-only">Toggle section menu</span>
</SheetTrigger>
{/* Mirror the desktop secondary panel: brand logo header, then the
active section's heading and links. */}
<SheetContent
side="left"
showCloseButton={false}
className="gap-0 overflow-y-auto p-0 data-[side=left]:w-64"
>
<SheetHeader className="flex h-16 shrink-0 flex-row items-center border-b border-border/50 px-4 py-0">
<SheetTitle className="sr-only">{item.title}</SheetTitle>
<Logo link={false} />
</SheetHeader>
<div className="flex flex-col p-3">
<h6 className="mb-3 px-2 pt-2 capitalize">{item.title}</h6>
<PanelNav
item={item}
activeLinkId={activeLinkId}
onSelect={(link) => {
onSelect(link)
setOpen(false)
}}
/>
</div>
</SheetContent>
</Sheet>
)
}
// ------------------------------ | DASHBOARD HEADER | ------------------------------ //
function DashboardHeader({
item,
activeLinkId,
onSelect,
}: {
item: NavItem
activeLinkId?: string
onSelect: (link: NavItem) => void
}) {
const { resolvedTheme, setTheme } = useTheme()
const mounted = useSyncExternalStore(
() => () => {},
() => true,
() => false
)
return (
<header className="flex h-16 shrink-0 items-center gap-1 border-b border-border/50 bg-background/80 px-4 backdrop-blur-md sm:px-6">
<MobilePanel
item={item}
activeLinkId={activeLinkId}
onSelect={onSelect}
/>
<SearchBar />
<div className="flex-1" />
<div className="flex items-center gap-1 sm:gap-2">
<Button
variant="ghost"
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-muted-foreground transition-colors hover:bg-muted hover:text-primary"
>
{mounted && resolvedTheme === "dark" ? (
<Sun className="size-5" />
) : (
<Moon className="size-5" />
)}
</Button>
{/* <LayoutSwitcher /> */}
<Button
variant="ghost"
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-muted-foreground transition-colors hover:bg-muted hover:text-primary"
>
<Settings className="size-5" />
</Button>
<div className="mx-1 h-4 w-px bg-border" />
<NotificationDropdown />
<div className="mx-1 h-4 w-px bg-border" />
<UserProfile />
</div>
</header>
)
}
// ------------------------------ | DASHBOARD CONTENT | ------------------------------ //
function DashboardContent({ activeLink }: { activeLink?: NavItem }) {
return (
<div className="flex-1 overflow-y-auto">
<div className="flex h-full min-h-0 flex-col gap-4 p-4 sm:p-6">
<h5 className="mb-0">{activeLink?.title}</h5>
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="h-24 rounded-xl bg-card" />
<div className="h-24 rounded-xl bg-card" />
<div className="h-24 rounded-xl bg-card" />
</div>
<div className="min-h-64 flex-1 rounded-xl bg-card" />
</div>
</div>
)
}
// ------------------------------ | BLOCK - DASHBOARD LAYOUT 3 (ICON RAIL) | ------------------------------ //
export default function DashboardThree({ children }: { children?: ReactNode }) {
const pathname = usePathname()
const [activeRailId, setActiveRailId] = useState(() => {
for (const item of railItems) {
if (hasActiveDescendant(item, pathname)) return item.id
}
return railItems[0].id
})
const [activeLinkId, setActiveLinkId] = useState<string | undefined>(() => {
const railItem =
railItems.find((item) => hasActiveDescendant(item, pathname)) ??
railItems[0]
return (
railItem.children?.find((child) => hasActiveDescendant(child, pathname))
?.id ?? railItem.children?.[0]?.id
)
})
const [panelOpen, setPanelOpen] = useState(true)
const activeItem =
railItems.find((item) => item.id === activeRailId) ?? railItems[0]
const activeLink =
activeItem.children?.find((link) => link.id === activeLinkId) ??
activeItem.children?.[0]
const selectRail = (item: NavItem) => {
setActiveRailId(item.id)
setActiveLinkId(item.children?.[0]?.id)
}
return (
<TooltipProvider>
<div className="relative flex h-screen w-full overflow-hidden bg-background [.in-iframe-preview_&]:h-[600px]">
<IconRail
activeId={activeRailId}
onSelect={selectRail}
panelOpen={panelOpen}
onTogglePanel={() => setPanelOpen((open) => !open)}
/>
{panelOpen ? (
<SecondaryPanel
item={activeItem}
activeLinkId={activeLink?.id}
onSelect={(link) => setActiveLinkId(link.id)}
/>
) : null}
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
<DashboardHeader
item={activeItem}
activeLinkId={activeLink?.id}
onSelect={(link) => setActiveLinkId(link.id)}
/>
{children ? (
<main className="flex-1 overflow-y-auto p-4 sm:p-6">
{children}
</main>
) : (
<DashboardContent activeLink={activeLink} />
)}
</div>
</div>
</TooltipProvider>
)
}