Files
tm_panel/src/components/Layouts/sidebar/index.tsx
T

270 lines
11 KiB
TypeScript

"use client";
import { Logo } from "@/components/logo";
import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { NAV_DATA, NavSubItem } from "./data";
import { ArrowLeftIcon, ChevronUp, MenuExpandIcon } from "./icons";
import { MenuItem } from "./menu-item";
import { useSidebarContext } from "./sidebar-context";
export function Sidebar() {
const pathname = usePathname();
const { setIsOpen, isOpen, isMobile, toggleSidebar } = useSidebarContext();
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const isVisible = (item: { visible?: boolean }) => item.visible !== false;
const toggleExpanded = (title: string) => {
setExpandedItems((prev) =>
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
);
};
useEffect(() => {
const newExpandedItems: string[] = [];
NAV_DATA.forEach((section) => {
section.items.filter(isVisible).forEach((item) => {
if (item.items.length > 0) {
const hasActiveSubItem = item.items.filter(isVisible).some(
(subItem: NavSubItem) =>
subItem.url === pathname ||
(pathname.startsWith(subItem.url) && subItem.url !== "/"),
);
if (hasActiveSubItem) {
newExpandedItems.push(item.title);
}
}
});
});
setExpandedItems(newExpandedItems);
}, [pathname]);
// Collapsed state: show only icons (for mobile)
const isCollapsed = isMobile && !isOpen;
return (
<>
{isMobile && isOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
)}
<aside
className={cn(
"shrink-0 overflow-hidden border-r border-stroke/70 bg-gradient-to-b from-white via-white to-primary/[0.03] shadow-[8px_0_30px_-24px_rgba(16,24,40,0.35)] transition-[width] duration-200 ease-linear dark:border-dark-3 dark:from-gray-dark dark:via-gray-dark dark:to-primary/[0.08]",
isMobile
? cn("fixed bottom-0 top-0", isOpen ? "z-50" : "z-20")
: "sticky top-0 h-screen",
isMobile
? isOpen
? "w-[290px]"
: "w-[70px]"
: "w-full max-w-[290px]",
)}
aria-label="Main navigation"
>
<div
className={cn(
"relative flex h-full flex-col py-10",
isCollapsed ? "items-center px-2" : "pl-[25px] pr-[7px]",
)}
>
<div className="pointer-events-none absolute -left-16 top-16 h-36 w-36 rounded-full bg-primary/10 blur-3xl dark:bg-primary/20" />
<div className="pointer-events-none absolute -right-16 bottom-10 h-36 w-36 rounded-full bg-purple-500/10 blur-3xl dark:bg-purple-500/20" />
<div className={cn("relative", isCollapsed ? "px-0" : "pr-4.5")}>
<Link
href={"/"}
onClick={() => isMobile && isOpen && toggleSidebar()}
className="inline-flex rounded-xl px-1 py-2.5 transition hover:bg-gray-2/70 min-[850px]:py-0 dark:hover:bg-white/[0.06]"
>
{isCollapsed ? (
<Image
src="/images/fav-icon.svg"
width={32}
height={32}
alt="Logo"
/>
) : (
<Logo />
)}
</Link>
{isMobile && isOpen && (
<button
onClick={toggleSidebar}
className="absolute left-3/4 right-4.5 top-1/2 -translate-y-1/2 rounded-lg p-1 text-right text-dark-4 transition hover:bg-gray-2/70 hover:text-dark dark:text-dark-6 dark:hover:bg-white/[0.08] dark:hover:text-white"
>
<span className="sr-only">Close Menu</span>
<ArrowLeftIcon className="ml-auto size-7" />
</button>
)}
</div>
<div
className={cn(
"custom-scrollbar mt-6 flex-1 overflow-y-auto min-[850px]:mt-10",
isCollapsed ? "w-full px-1" : "pr-3",
)}
>
{NAV_DATA.map((section) => {
const visibleItems = section.items.filter(isVisible);
if (!visibleItems.length) {
return null;
}
return (
<div key={section.label} className="mb-6">
{!isCollapsed && (
<h2 className="mb-4 px-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-dark-5 dark:text-dark-6">
{section.label}
</h2>
)}
<nav role="navigation" aria-label={section.label}>
<ul className={cn("space-y-2", isCollapsed && "space-y-1")}>
{visibleItems.map((item) => {
const visibleSubItems = item.items.filter(isVisible);
return (
<li key={item.title}>
{visibleSubItems.length ? (
<div>
<MenuItem
isActive={
visibleSubItems.some(
({ url }: { url: string }) =>
url === pathname,
) ||
(visibleSubItems.length > 0 &&
visibleSubItems.some(
({ url }: { url: string }) =>
pathname.startsWith(url) && url !== "/",
))
}
onClick={() => {
if (isCollapsed) {
toggleSidebar();
} else {
toggleExpanded(item.title);
}
}}
isCollapsed={isCollapsed}
title={item.title}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
{!isCollapsed && (
<>
<span>{item.title}</span>
<ChevronUp
className={cn(
"ml-auto rotate-180 transition-transform duration-200",
expandedItems.includes(item.title) &&
"rotate-0",
)}
aria-hidden="true"
/>
</>
)}
</MenuItem>
{!isCollapsed &&
expandedItems.includes(item.title) && (
<ul
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
role="menu"
>
{visibleSubItems.map((subItem: NavSubItem) => (
<li key={subItem.title} role="none">
<MenuItem
as="link"
href={subItem.url}
isActive={
pathname === subItem.url ||
(pathname.startsWith(subItem.url) &&
subItem.url !== "/")
}
>
<span>{subItem.title}</span>
</MenuItem>
</li>
))}
</ul>
)}
</div>
) : (
(() => {
const href =
"url" in item
? item.url + ""
: "/" +
item.title.toLowerCase().split(" ").join("-");
const isActive =
href === "/"
? pathname === href
: pathname.startsWith(href);
return (
<MenuItem
className={cn(
"flex items-center gap-3 py-3",
isCollapsed && "justify-center px-2",
)}
as="link"
href={href}
isActive={isActive}
isCollapsed={isCollapsed}
title={item.title}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
{!isCollapsed && <span>{item.title}</span>}
</MenuItem>
);
})()
)}
</li>
);
})}
</ul>
</nav>
</div>
);
})}
</div>
{/* Toggle button at bottom for collapsed state */}
{isCollapsed && (
<button
onClick={toggleSidebar}
className="mt-4 rounded-xl border border-stroke/70 bg-white/80 p-2 text-dark-4 shadow-theme-xs transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:text-primary dark:border-dark-3 dark:bg-dark-2 dark:text-dark-6 dark:hover:bg-white/[0.08] dark:hover:text-white"
aria-label="Expand sidebar"
>
<MenuExpandIcon className="size-6" />
</button>
)}
</div>
</aside>
</>
);
}