Files
core/src/features/admin/components/AdminSidebar.js
T

206 lines
7.7 KiB
JavaScript

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import * as Icons from '@zen/core/shared/icons';
import { ChevronDownIcon } from '@zen/core/shared/icons';
/**
* Resolve icon name (string) to icon component
* Icons are passed as strings from server to avoid serialization issues
*/
function resolveIcon(iconNameOrComponent) {
if (typeof iconNameOrComponent === 'function') {
return iconNameOrComponent;
}
if (typeof iconNameOrComponent === 'string') {
return Icons[iconNameOrComponent] || Icons.DashboardSquare03Icon;
}
return Icons.DashboardSquare03Icon;
}
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections }) => {
const pathname = usePathname();
const [collapsedSections, setCollapsedSections] = useState(new Set());
const toggleSection = (sectionId) => {
setCollapsedSections(prev => {
const newCollapsed = new Set(prev);
if (newCollapsed.has(sectionId)) {
newCollapsed.delete(sectionId);
} else {
newCollapsed.add(sectionId);
}
return newCollapsed;
});
};
const handleMobileLinkClick = () => {
setIsMobileMenuOpen(false);
};
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) {
setIsMobileMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [setIsMobileMenuOpen]);
const isSectionActive = (section) => {
return section.items.some(item => item.current);
};
const shouldRenderAsDirectLink = (section) => {
return section.items.length === 1 &&
section.items[0].name.toLowerCase() === section.title.toLowerCase();
};
useEffect(() => {
setCollapsedSections(prev => {
const newSet = new Set(prev);
navigationSections.forEach(section => {
if (isSectionActive(section)) {
newSet.add(section.id);
}
});
return newSet;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
const navigationSections = serverNavigationSections.map(section => ({
...section,
items: section.items.map(item => ({
...item,
current: pathname === item.href || pathname.startsWith(item.href + '/')
}))
}));
const base = 'w-full flex items-center justify-between px-[10px] py-[7px] rounded-lg text-[13px] transition-colors duration-[120ms] ease-out';
const inactive = 'font-normal text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-900 hover:text-neutral-900 dark:hover:text-white';
const parentBase = `${base}`;
const parentActif = 'bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white font-medium';
const parentActifOuvert = 'text-black dark:text-white font-medium hover:bg-neutral-100 dark:hover:bg-neutral-900';
const subItemBase = base;
const subItemActif = 'bg-neutral-100 dark:bg-neutral-900 text-black dark:text-white font-medium';
const renderNavSection = (section) => {
const Icon = resolveIcon(section.icon);
if (shouldRenderAsDirectLink(section)) {
const item = section.items[0];
return (
<div key={section.id}>
<Link
href={item.href}
onClick={handleMobileLinkClick}
className={`${parentBase} ${item.current ? parentActif : inactive}`}
>
<div className="flex items-center gap-2">
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
<span>{section.title}</span>
</div>
{item.badge && (
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-lg font-medium">
{item.badge}
</span>
)}
</Link>
</div>
);
}
const isCollapsed = !collapsedSections.has(section.id);
const isActive = isSectionActive(section);
return (
<div key={section.id}>
<button
onClick={() => toggleSection(section.id)}
className={`cursor-pointer ${parentBase} ${isActive && isCollapsed ? parentActif : isActive ? parentActifOuvert : inactive}`}
>
<div className="flex items-center gap-2">
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
<span>{section.title}</span>
</div>
<ChevronDownIcon
className={`h-3 w-3 transition-transform duration-[120ms] ease-out ${
isCollapsed ? '-rotate-90' : 'rotate-0'
}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-[120ms] ease-out ${
isCollapsed
? 'max-h-0 opacity-0'
: 'max-h-[1000px] opacity-100'
}`}
>
<ul className="flex flex-col">
{section.items.map(renderNavItem)}
</ul>
</div>
</div>
);
};
const renderNavItem = (item) => {
return (
<li key={item.name}>
<Link
href={item.href}
onClick={handleMobileLinkClick}
className={`${subItemBase} ${item.current ? subItemActif : inactive}`}
>
<div className="flex items-center">
<span className="pl-[25px]">{item.name}</span>
</div>
{item.badge && (
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-lg font-medium">
{item.badge}
</span>
)}
</Link>
</li>
);
};
return (
<>
{/* Mobile overlay */}
{isMobileMenuOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/50 z-40"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Sidebar */}
<div className={`
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
fixed lg:static inset-y-0 left-0 z-40 w-[230px] bg-white dark:bg-black border-r border-neutral-200 dark:border-neutral-800/70 flex flex-col h-screen transition-transform duration-[120ms] ease-out
`}>
{/* Logo */}
<Link href="/admin/dashboard" className="px-4 h-12 flex items-center justify-start gap-2 border-b border-neutral-200 dark:border-neutral-800/70">
<h1 className="text-neutral-900 dark:text-white font-semibold text-sm">{appName}</h1>
</Link>
{/* Navigation */}
<nav className="flex-1 px-2 py-2 overflow-y-auto flex flex-col pb-12">
{navigationSections.map(renderNavSection)}
</nav>
</div>
</>
);
};
export default AdminSidebar;