2026-09-12 07:35:35 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { usePathname } from "next/navigation";
|
|
|
|
|
import { ChevronDown, Menu, X } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
export type NavLeaf = { href: string; label: string };
|
|
|
|
|
export type NavItem =
|
|
|
|
|
| { kind: "link"; href: string; label: string; icon: React.ReactNode }
|
|
|
|
|
| { kind: "group"; label: string; icon: React.ReactNode; children: NavLeaf[] };
|
|
|
|
|
|
|
|
|
|
export function Sidebar({ items }: { items: NavItem[] }) {
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Close drawer on navigation
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setMobileOpen(false);
|
|
|
|
|
}, [pathname]);
|
|
|
|
|
|
|
|
|
|
// Lock body scroll when drawer is open
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
document.body.style.overflow = mobileOpen ? "hidden" : "";
|
|
|
|
|
return () => {
|
|
|
|
|
document.body.style.overflow = "";
|
|
|
|
|
};
|
|
|
|
|
}, [mobileOpen]);
|
|
|
|
|
|
|
|
|
|
const renderNav = (onNavigate?: () => void) => (
|
|
|
|
|
<nav className="p-3 space-y-0.5 text-sm">
|
|
|
|
|
{items.map((item, i) =>
|
|
|
|
|
item.kind === "link" ? (
|
|
|
|
|
<LeafLink
|
|
|
|
|
key={i}
|
|
|
|
|
href={item.href}
|
|
|
|
|
label={item.label}
|
|
|
|
|
icon={item.icon}
|
|
|
|
|
active={pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href))}
|
|
|
|
|
onClick={onNavigate}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Group
|
|
|
|
|
key={i}
|
|
|
|
|
item={item}
|
|
|
|
|
active={pathname.startsWith(item.children[0]?.href.split("/").slice(0, 3).join("/") ?? "")}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* Desktop: persistent sidebar (hidden on mobile) */}
|
2026-10-02 13:34:19 +00:00
|
|
|
<aside className="hidden lg:block w-64 shrink-0 bg-white dark:bg-surface-muted border-r border-gray-200 dark:border-border min-h-[calc(100vh-4rem)]">
|
2026-09-12 07:35:35 +00:00
|
|
|
{renderNav()}
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
{/* Mobile: hamburger in the top bar triggers a drawer */}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMobileOpen(true)}
|
|
|
|
|
aria-label="Open navigation"
|
|
|
|
|
className="lg:hidden w-9 h-9 grid place-items-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 text-gray-500 dark:text-gray-400"
|
|
|
|
|
>
|
|
|
|
|
<Menu className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{mobileOpen && (
|
|
|
|
|
<div
|
|
|
|
|
className="lg:hidden fixed inset-0 z-40 bg-black/40"
|
|
|
|
|
onClick={() => setMobileOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<aside
|
|
|
|
|
className={cn(
|
2026-10-02 13:34:19 +00:00
|
|
|
"lg:hidden fixed top-0 left-0 bottom-0 z-50 w-72 bg-white dark:bg-surface-muted shadow-2xl transition-transform",
|
2026-09-12 07:35:35 +00:00
|
|
|
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-10-02 13:34:19 +00:00
|
|
|
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-200 dark:border-border">
|
2026-09-12 07:35:35 +00:00
|
|
|
<div className="flex items-center gap-2">
|
2026-10-02 13:34:19 +00:00
|
|
|
<img src="/brand/logo.png" alt="Mega Plant" className="w-8 h-8 rounded-xl object-contain bg-white dark:bg-bg shadow-sm" />
|
2026-09-12 07:35:35 +00:00
|
|
|
<span className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMobileOpen(false)}
|
|
|
|
|
aria-label="Close navigation"
|
|
|
|
|
className="w-9 h-9 grid place-items-center rounded-lg hover:bg-gray-100 dark:hover:bg-white/5 text-gray-500 dark:text-gray-400"
|
|
|
|
|
>
|
|
|
|
|
<X className="w-5 h-5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
{renderNav(() => setMobileOpen(false))}
|
|
|
|
|
</aside>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LeafLink({
|
|
|
|
|
href, label, icon, active, onClick,
|
|
|
|
|
}: {
|
|
|
|
|
href: string; label: string; icon: React.ReactNode; active: boolean; onClick?: () => void;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
href={href}
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-3 px-3 py-2 rounded-lg font-medium transition",
|
|
|
|
|
active
|
|
|
|
|
? "bg-gradient-to-r from-[#30638E]/10 to-[#00798C]/5 text-[#003D5B] dark:from-[#30638E]/20 dark:to-[#00798C]/10 dark:text-white"
|
|
|
|
|
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-10-02 13:34:19 +00:00
|
|
|
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{icon}</span>
|
2026-09-12 07:35:35 +00:00
|
|
|
<span>{label}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Group({ item, active }: { item: Extract<NavItem, { kind: "group" }>; active: boolean }) {
|
|
|
|
|
const [open, setOpen] = useState(active);
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpen((o) => !o)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"w-full flex items-center gap-3 px-3 py-2 rounded-lg font-medium transition",
|
|
|
|
|
active
|
|
|
|
|
? "text-[#003D5B] dark:text-white"
|
|
|
|
|
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-10-02 13:34:19 +00:00
|
|
|
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{item.icon}</span>
|
2026-09-12 07:35:35 +00:00
|
|
|
<span className="flex-1 text-left">{item.label}</span>
|
|
|
|
|
<ChevronDown className={cn("w-4 h-4 text-gray-400 transition-transform", open && "rotate-180")} />
|
|
|
|
|
</button>
|
|
|
|
|
{open && (
|
2026-10-02 13:34:19 +00:00
|
|
|
<div className="ml-7 mt-0.5 space-y-0.5 border-l-2 border-gray-100 dark:border-border pl-2">
|
2026-09-12 07:35:35 +00:00
|
|
|
{item.children.map((child) => {
|
|
|
|
|
const childActive = pathname === child.href;
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
key={child.href}
|
|
|
|
|
href={child.href}
|
|
|
|
|
className={cn(
|
|
|
|
|
"block px-3 py-1.5 rounded-lg text-xs font-medium transition",
|
|
|
|
|
childActive
|
|
|
|
|
? "bg-[#30638E]/10 text-[#30638E] dark:bg-[#00798C]/15 dark:text-[#00798C]"
|
|
|
|
|
: "text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{child.label}
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|