chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
@@ -0,0 +1,234 @@
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import * as Icons from '../../../shared/Icons.js';
import { ChevronDownIcon } from '../../../shared/Icons.js';
/**
* Resolve icon name (string) to icon component
* Icons are passed as strings from server to avoid serialization issues
*/
function resolveIcon(iconNameOrComponent) {
// If it's already a component (function), return it
if (typeof iconNameOrComponent === 'function') {
return iconNameOrComponent;
}
// If it's a string, look up in Icons
if (typeof iconNameOrComponent === 'string') {
return Icons[iconNameOrComponent] || Icons.DashboardSquare03Icon;
}
// Default fallback
return Icons.DashboardSquare03Icon;
}
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections }) => {
const pathname = usePathname();
// State to manage collapsed sections (all open by default)
const [collapsedSections, setCollapsedSections] = useState(new Set());
// Function to toggle a section's state
const toggleSection = (sectionId) => {
// Find the section to check if it has active items
const section = navigationSections.find(s => s.id === sectionId);
// Don't allow collapsing sections with active items
if (section && isSectionActive(section)) {
return;
}
setCollapsedSections(prev => {
const newCollapsed = new Set(prev);
if (newCollapsed.has(sectionId)) {
newCollapsed.delete(sectionId);
} else {
newCollapsed.add(sectionId);
}
return newCollapsed;
});
};
// Handle mobile menu closure when clicking on a link
const handleMobileLinkClick = () => {
setIsMobileMenuOpen(false);
};
// Close mobile menu on screen size change
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= 1024) { // lg breakpoint
setIsMobileMenuOpen(false);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [setIsMobileMenuOpen]);
// Function to check if any item in a section is currently active
const isSectionActive = (section) => {
return section.items.some(item => item.current);
};
// Function to check if a section should be rendered as a direct link
const shouldRenderAsDirectLink = (section) => {
// Check if there's only one item and it has the same name as the section
return section.items.length === 1 &&
section.items[0].name.toLowerCase() === section.title.toLowerCase();
};
// Update collapsed sections when pathname changes to ensure active sections are open
useEffect(() => {
setCollapsedSections(prev => {
const newSet = new Set(prev);
// Add any sections that have active items to ensure they stay open
navigationSections.forEach(section => {
if (isSectionActive(section)) {
newSet.add(section.id);
}
});
return newSet;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
// Use server-provided navigation sections if available, otherwise use core-only fallback
// Server navigation includes module navigation, fallback only has core pages
// Update the 'current' property based on the actual pathname (client-side)
const navigationSections = serverNavigationSections.map(section => ({
...section,
items: section.items.map(item => ({
...item,
current: pathname === item.href || pathname.startsWith(item.href + '/')
}))
}));
// Function to render a complete navigation section
const renderNavSection = (section) => {
const Icon = resolveIcon(section.icon);
// If section should be rendered as a direct link
if (shouldRenderAsDirectLink(section)) {
const item = section.items[0];
return (
<div key={section.id}>
<Link
href={item.href}
onClick={handleMobileLinkClick}
className={`${
item.current
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
: 'text-neutral-900 dark:text-white hover:text-neutral-500 dark:hover:text-neutral-300'
} w-full flex items-center justify-between border-y border-neutral-200 dark:border-neutral-800/70 px-4 py-2.5 -mb-[1px] text-[13px] tracking-wide transition-colorsduration-0`}
>
<div className="flex items-center">
<Icon className="mr-3 h-4 w-4 flex-shrink-0" />
<span>{section.title}</span>
</div>
{item.badge && (
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full font-medium">
{item.badge}
</span>
)}
</Link>
</div>
);
}
// Regular section with expandable sub-items
const isCollapsed = !collapsedSections.has(section.id);
return (
<div key={section.id}>
<button
onClick={() => toggleSection(section.id)}
className="cursor-pointer w-full flex items-center justify-between border-y border-neutral-200 dark:border-neutral-800/70 px-4 py-2.5 -mb-[1px] text-[13px] text-neutral-900 dark:text-white tracking-wide hover:text-neutral-500 dark:hover:text-neutral-300 transition-colorsduration-0"
>
<div className="flex items-center">
<Icon className="mr-3 h-4 w-4 flex-shrink-0" />
<span>{section.title}</span>
</div>
<ChevronDownIcon
className={`h-3 w-3 ${
isCollapsed ? '-rotate-90' : 'rotate-0'
}`}
/>
</button>
<div
className={`overflow-hidden ${
isCollapsed
? 'max-h-0 opacity-0'
: 'max-h-[1000px] opacity-100'
}`}
>
<ul className="flex flex-col gap-0">
{section.items.map(renderNavItem)}
</ul>
</div>
</div>
);
};
// Function to render a navigation item
const renderNavItem = (item) => {
const Icon = resolveIcon(item.icon);
return (
<li key={item.name}>
<Link
href={item.href}
onClick={handleMobileLinkClick}
className={`${
item.current
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
: 'text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800/50 hover:text-neutral-900 dark:hover:text-white'
} group flex items-center justify-between px-4 py-1.5 text-[12px] font-medium transition-allduration-0`}
>
<div className="flex items-center">
<Icon className="mr-3 h-3.5 w-3.5 flex-shrink-0" />
{item.name}
</div>
{item.badge && (
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full font-medium">
{item.badge}
</span>
)}
</Link>
</li>
);
};
return (
<>
{/* Mobile overlay */}
{isMobileMenuOpen && (
<div
className="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm 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-64 bg-white dark:bg-black border-r border-neutral-200 dark:border-neutral-800/70 flex flex-col h-screen transition-transform duration-300 ease-in-out
`}>
{/* Logo Section */}
<Link href="/admin" className="px-4 h-14 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">{appName}</h1>
<span className="bg-red-700/10 border border-red-600/20 text-red-600 uppercase text-[10px] leading-none px-2 py-1 rounded-full font-semibold">
Admin
</span>
</Link>
{/* Navigation */}
<nav className="flex-1 px-0 overflow-y-auto flex flex-col gap-0 pb-12 -mt-[1px]">
{navigationSections.map(renderNavSection)}
</nav>
</div>
</>
);
};
export default AdminSidebar;