1032276d49
- swap `ChevronDownIcon` and `ChevronRightIcon` for `ArrowDown01Icon` and `ArrowRight01Icon` in AdminSidebar and AdminTop - add `ArrowDown01Icon`, `ArrowLeft01Icon`, `ArrowRight01Icon`, and `ArrowUp01Icon` to shared icons index - remove `ChevronDownIcon` and `ChevronRightIcon` from shared icons index
224 lines
8.7 KiB
JavaScript
224 lines
8.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 { ArrowDown01Icon } 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, bottomNavItems = [] }) => {
|
|
const pathname = usePathname();
|
|
|
|
const [collapsedSections, setCollapsedSections] = useState(() => {
|
|
const initial = new Set();
|
|
serverNavigationSections.forEach(section => {
|
|
const isActive = section.items.some(item =>
|
|
pathname === item.href || pathname.startsWith(item.href + '/')
|
|
);
|
|
if (!isActive) initial.add(section.id);
|
|
});
|
|
return initial;
|
|
});
|
|
|
|
const toggleSection = (sectionId) => {
|
|
setCollapsedSections(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(sectionId)) {
|
|
next.delete(sectionId);
|
|
} else {
|
|
next.add(sectionId);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
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();
|
|
};
|
|
|
|
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>
|
|
<ArrowDown01Icon
|
|
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">
|
|
{navigationSections.map(renderNavSection)}
|
|
</nav>
|
|
|
|
{/* Bottom pinned items */}
|
|
{bottomNavItems.length > 0 && (
|
|
<div className="px-2 py-2 border-t border-neutral-200 dark:border-neutral-800/70 shrink-0">
|
|
{bottomNavItems.map((item) => {
|
|
const Icon = resolveIcon(item.icon);
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
onClick={handleMobileLinkClick}
|
|
className={`${base} ${item.current ? parentActif : inactive}`}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
|
|
<span>{item.name}</span>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default AdminSidebar;
|