All checks were successful
Build and Release / release (push) Successful in 27s
297 lines
9.5 KiB
TypeScript
297 lines
9.5 KiB
TypeScript
"use client";
|
|
import React, { useEffect, useRef, useState, useCallback } from "react";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { usePathname } from "next/navigation";
|
|
import { useSidebar } from "../context/SidebarContext";
|
|
import {
|
|
BoxCubeIcon,
|
|
CalenderIcon,
|
|
ChevronDownIcon,
|
|
FileIcon,
|
|
GridIcon,
|
|
HorizontaLDots,
|
|
ListIcon,
|
|
PageIcon,
|
|
PieChartIcon,
|
|
PlugInIcon,
|
|
TableIcon,
|
|
UserCircleIcon,
|
|
} from "../icons/index";
|
|
|
|
type NavItem = {
|
|
name: string;
|
|
icon: React.ReactNode;
|
|
path?: string;
|
|
subItems?: {
|
|
name: string;
|
|
path: string;
|
|
pro?: boolean;
|
|
new?: boolean;
|
|
}[];
|
|
};
|
|
|
|
const ALL_NAV_ITEMS: NavItem[] = [
|
|
{
|
|
icon: <GridIcon />,
|
|
name: "Trang Chủ",
|
|
// subItems: [{ name: "Ecommerce", path: "/", pro: false }],
|
|
path: "/",
|
|
},
|
|
{
|
|
icon: <BoxCubeIcon />,
|
|
name: "Dự Án",
|
|
path: "/user/projects",
|
|
},
|
|
{
|
|
icon: <FileIcon />,
|
|
name: "Thư Viện",
|
|
path: "/user/library",
|
|
},
|
|
{
|
|
icon: <UserCircleIcon />,
|
|
name: "Tài Khoản",
|
|
path: "/user/account",
|
|
},
|
|
];
|
|
|
|
const OTHERS_ITEMS: NavItem[] = [
|
|
{
|
|
icon: <PieChartIcon />,
|
|
name: "Charts",
|
|
subItems: [
|
|
{ name: "Line Chart", path: "/line-chart", pro: false },
|
|
{ name: "Bar Chart", path: "/bar-chart", pro: false },
|
|
],
|
|
},
|
|
{
|
|
icon: <BoxCubeIcon />,
|
|
name: "UI Elements",
|
|
subItems: [
|
|
{ name: "Alerts", path: "/alerts", pro: false },
|
|
{ name: "Avatar", path: "/avatars", pro: false },
|
|
{ name: "Badge", path: "/badge", pro: false },
|
|
{ name: "Buttons", path: "/buttons", pro: false },
|
|
{ name: "Images", path: "/images", pro: false },
|
|
{ name: "Videos", path: "/videos", pro: false },
|
|
],
|
|
},
|
|
{
|
|
icon: <PlugInIcon />,
|
|
name: "Authentication",
|
|
subItems: [
|
|
{ name: "Sign In", path: "/signin", pro: false },
|
|
{ name: "Sign Up", path: "/signup", pro: false },
|
|
],
|
|
},
|
|
];
|
|
|
|
const AppSidebar: React.FC = () => {
|
|
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
|
const pathname = usePathname();
|
|
|
|
const [openSubmenu, setOpenSubmenu] = useState<{
|
|
type: "main" | "others";
|
|
index: number;
|
|
} | null>(null);
|
|
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
|
|
{},
|
|
);
|
|
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
|
|
const isActive = useCallback((path: string) => path === pathname, [pathname]);
|
|
|
|
const handleSubmenuToggle = (index: number, menuType: "main" | "others") => {
|
|
setOpenSubmenu((prev) =>
|
|
prev?.type === menuType && prev?.index === index
|
|
? null
|
|
: { type: menuType, index },
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
let submenuMatched = false;
|
|
[
|
|
{ items: ALL_NAV_ITEMS, type: "main" },
|
|
{ items: OTHERS_ITEMS, type: "others" },
|
|
].forEach(({ items, type }) => {
|
|
items.forEach((nav, index) => {
|
|
nav.subItems?.forEach((sub) => {
|
|
if (isActive(sub.path)) {
|
|
setOpenSubmenu((prev) => {
|
|
if (prev?.type === type && prev?.index === index) return prev;
|
|
return { type: type as "main" | "others", index };
|
|
});
|
|
submenuMatched = true;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
if (!submenuMatched) {
|
|
setOpenSubmenu((prev) => (prev !== null ? null : prev));
|
|
}
|
|
}, [pathname, isActive]);
|
|
|
|
useEffect(() => {
|
|
if (openSubmenu !== null) {
|
|
const key = `${openSubmenu.type}-${openSubmenu.index}`;
|
|
if (subMenuRefs.current[key]) {
|
|
setSubMenuHeight((prev) => ({
|
|
...prev,
|
|
[key]: subMenuRefs.current[key]?.scrollHeight || 0,
|
|
}));
|
|
}
|
|
}
|
|
}, [openSubmenu]);
|
|
|
|
const renderMenuItems = (items: NavItem[], menuType: "main" | "others") => (
|
|
<ul className="flex flex-col gap-4">
|
|
{items.map((nav, index) => (
|
|
<li key={nav.name}>
|
|
{nav.subItems ? (
|
|
<button
|
|
onClick={() => handleSubmenuToggle(index, menuType)}
|
|
className={`menu-item group uppercase ${
|
|
openSubmenu?.type === menuType && openSubmenu?.index === index
|
|
? "menu-item-active"
|
|
: "menu-item-inactive"
|
|
} cursor-pointer ${!isExpanded && !isHovered ? "lg:justify-center" : "lg:justify-start"}`}
|
|
>
|
|
<span
|
|
className={
|
|
openSubmenu?.type === menuType && openSubmenu?.index === index
|
|
? "menu-item-icon-active"
|
|
: "menu-item-icon-inactive"
|
|
}
|
|
>
|
|
{nav.icon}
|
|
</span>
|
|
{(isExpanded || isHovered || isMobileOpen) && (
|
|
<>
|
|
<span className={`menu-item-text`}>{nav.name}</span>
|
|
<ChevronDownIcon
|
|
className={`ml-auto w-5 h-5 transition-transform duration-200 ${openSubmenu?.type === menuType && openSubmenu?.index === index ? "rotate-180 text-brand-500" : ""}`}
|
|
/>
|
|
</>
|
|
)}
|
|
</button>
|
|
) : (
|
|
nav.path && (
|
|
<Link
|
|
href={nav.path}
|
|
className={`menu-item group ${isActive(nav.path) ? "menu-item-active" : "menu-item-inactive"}`}
|
|
>
|
|
<span
|
|
className={
|
|
isActive(nav.path)
|
|
? "menu-item-icon-active"
|
|
: "menu-item-icon-inactive"
|
|
}
|
|
>
|
|
{nav.icon}
|
|
</span>
|
|
{(isExpanded || isHovered || isMobileOpen) && (
|
|
<span className={`menu-item-text`}>{nav.name}</span>
|
|
)}
|
|
</Link>
|
|
)
|
|
)}
|
|
{nav.subItems && (isExpanded || isHovered || isMobileOpen) && (
|
|
<div
|
|
ref={(el) => {
|
|
subMenuRefs.current[`${menuType}-${index}`] = el;
|
|
}}
|
|
className="overflow-hidden transition-all duration-300"
|
|
style={{
|
|
height:
|
|
openSubmenu?.type === menuType && openSubmenu?.index === index
|
|
? `${subMenuHeight[`${menuType}-${index}`]}px`
|
|
: "0px",
|
|
}}
|
|
>
|
|
<ul className="mt-2 space-y-1 ml-9">
|
|
{nav.subItems.map((subItem) => (
|
|
<li key={subItem.name}>
|
|
<Link
|
|
href={subItem.path}
|
|
className={`menu-dropdown-item ${isActive(subItem.path) ? "menu-dropdown-item-active" : "menu-dropdown-item-inactive"}`}
|
|
>
|
|
{subItem.name}
|
|
<span className="flex items-center gap-1 ml-auto">
|
|
{subItem.new && (
|
|
<span
|
|
className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}
|
|
>
|
|
new
|
|
</span>
|
|
)}
|
|
{subItem.pro && (
|
|
<span
|
|
className={`${isActive(subItem.path) ? "menu-dropdown-badge-active" : "menu-dropdown-badge-inactive"} menu-dropdown-badge`}
|
|
>
|
|
pro
|
|
</span>
|
|
)}
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
return (
|
|
<aside
|
|
className={`fixed mt-16 flex flex-col lg:mt-0 top-0 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-50 border-r border-gray-200
|
|
${isExpanded || isMobileOpen || isHovered ? "w-[290px] px-5" : "w-0 px-0 border-none overflow-hidden"}
|
|
${isMobileOpen ? "translate-x-0" : "-translate-x-full"} lg:translate-x-0`}
|
|
onMouseEnter={() => !isExpanded && setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<div
|
|
className={`py-8 flex ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}
|
|
>
|
|
<Link href="/">
|
|
<Image
|
|
src="/images/logo/logo.svg"
|
|
alt="Logo"
|
|
width={isExpanded || isHovered || isMobileOpen ? 80 : 32}
|
|
height={isExpanded || isHovered || isMobileOpen ? 50 : 32}
|
|
/>
|
|
</Link>
|
|
</div>
|
|
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
|
|
<nav className="mb-6">
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<h2
|
|
className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}
|
|
>
|
|
{isExpanded || isHovered || isMobileOpen ? (
|
|
"Menu"
|
|
) : (
|
|
<HorizontaLDots />
|
|
)}
|
|
</h2>
|
|
{renderMenuItems(ALL_NAV_ITEMS, "main")}
|
|
</div>
|
|
{/* <div>
|
|
<h2 className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}>
|
|
{isExpanded || isHovered || isMobileOpen ? "Others" : <HorizontaLDots />}
|
|
</h2>
|
|
{renderMenuItems(OTHERS_ITEMS, "others")}
|
|
</div> */}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
export default AppSidebar;
|