"use client"
import { ReactNode, useEffect, useState } from "react"
// next
import Link from "next/link"
import { usePathname } from "next/navigation"
// shadcn
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar"
// third-party
import { useTheme } from "next-themes"
// project-imports
import branding from "@/branding.json"
import {
hasActiveDescendant,
isNavItemActive,
menuGroups,
type NavGroup,
type NavItem,
} from "@/components/uiable/blocks/layout/dashboard-layout/data/dashboard-1.data"
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 {
ChevronDown,
ChevronRight,
LockKeyhole,
LogOut,
Moon,
Power,
Settings,
Sun,
User,
} from "lucide-react"
const imgCoupon = {
src: "https://cdn.uiable.com/application/img-coupon.png",
}
const uImg1 = {
src: "https://cdn.uiable.com/user/avatar-1.jpg",
}
const menuButtonClass =
"rounded-lg px-5 py-3.5 text-foreground/70 data-[active=true]:bg-primary/10 data-[active=true]:text-primary"
const subMenuButtonClass =
"h-8 px-5 py-3.5 text-foreground/70 hover:bg-muted-foreground/6 hover:text-foreground hover:after:bg-foreground hover:after:opacity-100 data-[active=true]:text-primary"
const groupLabelClass =
"px-5.5 pt-6 pb-2 text-[10px] font-bold tracking-widest text-muted-foreground/70 uppercase"
// ------------------------------ | USER PROFILE CARD | ------------------------------ //
function UserProfileCard() {
const [isUserOpen, setIsUserOpen] = useState(false)
return (
<Card className="m-1.75 bg-muted shadow-none">
<CardContent className="p-5!">
<Collapsible
open={isUserOpen}
onOpenChange={setIsUserOpen}
className="flex flex-col gap-2"
>
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10 border-2 border-background">
<AvatarImage src={uImg1.src} alt="User" />
<AvatarFallback className="bg-primary/10 font-bold text-primary">
JS
</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col">
<h6 className="mb-0">John Smith</h6>
<small className="text-muted-foreground">Administrator</small>
</div>
<CollapsibleTrigger
render={
<Button variant="ghost" size="icon">
<ChevronDown className="size-5.5 transition-transform duration-300 ease-out group-data-panel-open/button:rotate-180" />
</Button>
}
/>
</div>
<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="pt-3 *:flex *:items-center *:gap-2.5 *:py-2 *:last:pb-0 *:hover:text-primary">
<a href="#!">
<User className="size-4" />
<span>My Account</span>
</a>
<a href="#!">
<Settings className="size-4" />
<span>Settings</span>
</a>
<a href="#!">
<LockKeyhole className="size-4" />
<span>Lock Screen</span>
</a>
<Link href="/login">
<Power className="size-4" />
<span>Logout</span>
</Link>
</div>
</CollapsibleContent>
</Collapsible>
</CardContent>
</Card>
)
}
// ------------------------------ | NESTED COLLAPSIBLE CLASSES | ------------------------------ //
// Static class strings are listed literally so Tailwind can detect them. Each
// collapsible node is mapped to one entry in DFS pre-order (see below).
const collapsibleGroups = [
{
group: "group/collapsible",
rotate: "group-data-open/collapsible:rotate-90",
},
{
group: "group/collapsible-2",
rotate: "group-data-open/collapsible-2:rotate-90",
},
{
group: "group/collapsible-3",
rotate: "group-data-open/collapsible-3:rotate-90",
},
{
group: "group/collapsible-4",
rotate: "group-data-open/collapsible-4:rotate-90",
},
{
group: "group/collapsible-5",
rotate: "group-data-open/collapsible-5:rotate-90",
},
]
// Assign a unique static group class to every collapsible node (any item with
// children) once, walking all groups in DFS pre-order so nested rotations stay
// scoped to their own trigger.
const collapsibleClassByItem = new Map<
NavItem,
(typeof collapsibleGroups)[number]
>()
;(() => {
let index = 0
const assign = (item: NavItem) => {
if (!item.children) return
collapsibleClassByItem.set(item, collapsibleGroups[index++])
item.children.forEach(assign)
}
menuGroups.forEach((group) => group.items.forEach(assign))
})()
// ------------------------------ | NAV LEAF ITEM | ------------------------------ //
function NavLeafItem({ item }: { item: NavItem }) {
const pathname = usePathname()
const Icon = item.icon
return (
<SidebarMenuItem>
<SidebarMenuButton
isActive={isNavItemActive(pathname, item.href)}
className={menuButtonClass}
render={
<Link
href={item.href ?? "#"}
target={item.target}
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
/>
}
>
{Icon ? <Icon className="size-4.5!" /> : null}
<span className="font-medium">{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
)
}
// ------------------------------ | NAV SUB NODE (NESTED) | ------------------------------ //
function NavSubNode({ node }: { node: NavItem }) {
const pathname = usePathname()
if (!node.children) {
return (
<SidebarMenuSubItem>
<SidebarMenuSubButton
isActive={isNavItemActive(pathname, node.href)}
className={subMenuButtonClass}
render={<Link href={node.href ?? "#"} />}
>
<span>{node.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
)
}
const { group, rotate } = collapsibleClassByItem.get(node)!
return (
<Collapsible
className={group}
defaultOpen={hasActiveDescendant(node, pathname)}
>
<SidebarMenuSubItem>
<CollapsibleTrigger
nativeButton={false}
render={
<SidebarMenuSubButton
className={`flex w-full justify-between ${subMenuButtonClass}`}
/>
}
>
<span>{node.title}</span>
<ChevronRight
className={`ml-auto size-4 transition-transform ${rotate}`}
/>
</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">
<SidebarMenuSub className="mt-1 ml-2 border-l border-border/50 pl-2">
{node.children.map((child) => (
<NavSubNode key={child.id} node={child} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuSubItem>
</Collapsible>
)
}
// ------------------------------ | NAV COLLAPSIBLE ITEM (TOP-LEVEL) | ------------------------------ //
function NavCollapsibleItem({ item }: { item: NavItem }) {
const pathname = usePathname()
const { group, rotate } = collapsibleClassByItem.get(item)!
const Icon = item.icon
return (
<Collapsible
className={group}
defaultOpen={hasActiveDescendant(item, pathname)}
>
<SidebarMenuItem>
<CollapsibleTrigger
render={
<SidebarMenuButton
tooltip={item.title}
className="rounded-lg px-5 py-3.5 text-foreground/70"
/>
}
>
{Icon ? <Icon className="size-4.5!" /> : null}
<span className="font-medium">{item.title}</span>
<ChevronRight
className={`ml-auto size-4 transition-transform ${rotate}`}
/>
</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">
<SidebarMenuSub>
{item.children?.map((child) => (
<NavSubNode key={child.id} node={child} />
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}
// ------------------------------ | NAV GROUP SECTION | ------------------------------ //
function NavGroupSection({ group }: { group: NavGroup }) {
return (
<SidebarGroup>
<SidebarGroupLabel className={groupLabelClass}>
{group.label}
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) =>
item.children?.length ? (
<NavCollapsibleItem key={item.id} item={item} />
) : (
<NavLeafItem key={item.id} item={item} />
)
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
)
}
// ------------------------------ | UPGRADE CARD | ------------------------------ //
function UpgradeCard() {
return (
<Card className="mx-1.75 my-3.75 shadow-none">
<CardContent className="text-center">
<img
src={imgCoupon.src}
alt="Coupon"
className="mx-auto w-2/4 max-w-full"
/>
<h5 className="mt-1 mb-0">{branding.brandName}</h5>
<p className="mb-4">Checkout pro features</p>
<Button className="gap-2 bg-yellow-500 hover:bg-yellow-600">
<LogOut />
Upgrade to Pro
</Button>
</CardContent>
</Card>
)
}
// ------------------------------ | DASHBOARD SIDEBAR | ------------------------------ //
function DashboardSidebar() {
return (
<Sidebar
collapsible="offcanvas"
variant="inset"
className="absolute z-10 h-full border-r border-dashed"
>
<SidebarHeader className="px-4 pt-2 pb-4">
<SidebarMenu>
<SidebarMenuItem>
<Logo link={false} />
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent className="gap-0 px-2 *:py-0">
<UserProfileCard />
{menuGroups.map((group) => (
<NavGroupSection key={group.id} group={group} />
))}
<UpgradeCard />
</SidebarContent>
<SidebarRail />
</Sidebar>
)
}
// ------------------------------ | DASHBOARD HEADER | ------------------------------ //
function DashboardHeader() {
const { resolvedTheme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true)
}, [])
return (
<header className="flex h-16 w-full shrink-0 items-center gap-1 bg-background/80 px-4 backdrop-blur-md">
<SidebarTrigger className="relative mx-1 -ml-1 flex h-11 w-11 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-primary [&>svg]:size-5" />
<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-11 w-11 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>
<Button
variant="ghost"
className="inline-flex h-11 w-11 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="h-4 w-px bg-border" />
<NotificationDropdown />
<div className="h-4 w-px bg-border" />
<UserProfile />
</div>
</header>
)
}
// ------------------------------ | DASHBOARD CONTENT | ------------------------------ //
function DashboardContent() {
return (
<div className="flex-1 overflow-y-auto">
<div className="flex h-full min-h-0 flex-col gap-4 p-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-card" />
<div className="aspect-video rounded-xl bg-card" />
<div className="aspect-video rounded-xl bg-card" />
</div>
<div className="min-h-64 flex-1 rounded-xl bg-card" />
</div>
</div>
)
}
// ------------------------------ | BLOCK - DASHBOARD LAYOUT 1 (SIDEBAR) | ------------------------------ //
export default function DashboardOne({ children }: { children?: ReactNode }) {
return (
<SidebarProvider className="relative h-screen min-h-0! w-full overflow-hidden bg-background has-[data-slot='sidebar-inset']:shadow-none [.in-iframe-preview_&]:h-[600px]">
<DashboardSidebar />
{/* Main content area with navbar */}
<SidebarInset className="flex flex-col overflow-hidden md:peer-data-[variant=inset]:m-0 md:peer-data-[variant=inset]:rounded-none">
<DashboardHeader />
{children ? (
<main className="flex-1 overflow-y-auto p-4">{children}</main>
) : (
<DashboardContent />
)}
</SidebarInset>
</SidebarProvider>
)
}