feat(shell): permitir mostrar y ocultar paginas dentro de secciones del menu con acordeon
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
|||||||
Images,
|
Images,
|
||||||
History,
|
History,
|
||||||
Plug,
|
Plug,
|
||||||
|
ChevronDown,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import SidebarTenantSwitcher from '@/components/SidebarTenantSwitcher';
|
import SidebarTenantSwitcher from '@/components/SidebarTenantSwitcher';
|
||||||
import type { DashboardModuleId } from '@/lib/tenant-modules';
|
import type { DashboardModuleId } from '@/lib/tenant-modules';
|
||||||
@@ -145,19 +146,43 @@ function isActive(pathname: string, href: string): boolean {
|
|||||||
function NavLink({
|
function NavLink({
|
||||||
item,
|
item,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
|
hasSubItems,
|
||||||
|
isOpen,
|
||||||
|
onToggle,
|
||||||
}: {
|
}: {
|
||||||
item: NavItem | (Omit<NavItem, 'moduleId'> & { moduleId?: string });
|
item: NavItem | (Omit<NavItem, 'moduleId'> & { moduleId?: string });
|
||||||
onNavigate?: () => void;
|
onNavigate?: () => void;
|
||||||
|
hasSubItems?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const active = isActive(pathname, item.href);
|
const active = isActive(pathname, item.href);
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (hasSubItems && onToggle) {
|
||||||
|
if (active) {
|
||||||
|
// Si ya está en esta página, hacer clic en la sección pliega/despliega sin recargar
|
||||||
|
e.preventDefault();
|
||||||
|
onToggle();
|
||||||
|
return;
|
||||||
|
} else if (!isOpen) {
|
||||||
|
// Al entrar a la sección, asegurar que se abre
|
||||||
|
onToggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onNavigate) {
|
||||||
|
onNavigate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={onNavigate}
|
onClick={handleClick}
|
||||||
aria-current={active ? 'page' : undefined}
|
aria-current={active ? 'page' : undefined}
|
||||||
className={`group relative flex items-center gap-3 rounded-md px-3 py-2 text-[14px] font-medium transition-colors ${
|
className={`group relative flex items-center justify-between gap-2 rounded-md px-3 py-2 text-[14px] font-medium transition-colors ${
|
||||||
active
|
active
|
||||||
? 'bg-white/[0.07] text-white'
|
? 'bg-white/[0.07] text-white'
|
||||||
: 'text-mist-300 hover:bg-white/[0.05] hover:text-white'
|
: 'text-mist-300 hover:bg-white/[0.05] hover:text-white'
|
||||||
@@ -166,8 +191,36 @@ function NavLink({
|
|||||||
{active ? (
|
{active ? (
|
||||||
<span aria-hidden className="absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-r bg-brand-500" />
|
<span aria-hidden className="absolute left-0 top-1/2 h-5 w-0.5 -translate-y-1/2 rounded-r bg-brand-500" />
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<Icon className={`h-4 w-4 shrink-0 ${active ? 'text-brand-500' : 'text-mist-400 group-hover:text-mist-300'}`} />
|
<Icon className={`h-4 w-4 shrink-0 ${active ? 'text-brand-500' : 'text-mist-400 group-hover:text-mist-300'}`} />
|
||||||
{item.label}
|
<span className="truncate">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
{hasSubItems ? (
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggle?.();
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggle?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-1 rounded hover:bg-white/10 text-mist-400 hover:text-white transition-colors shrink-0"
|
||||||
|
aria-label={isOpen ? 'Ocultar páginas' : 'Mostrar páginas'}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-3.5 w-3.5 transition-transform duration-200 ${
|
||||||
|
isOpen ? 'rotate-0 text-white' : '-rotate-90 text-mist-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -204,6 +257,33 @@ export default function DashboardShell({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const showDateHeader = shouldShowDateHeader(pathname);
|
const showDateHeader = shouldShowDateHeader(pathname);
|
||||||
|
|
||||||
|
const isSocialRoute =
|
||||||
|
pathname === '/dashboard/social' ||
|
||||||
|
pathname === '/dashboard/calendario' ||
|
||||||
|
pathname === '/dashboard/creatives';
|
||||||
|
|
||||||
|
const isAgenciaRoute =
|
||||||
|
pathname === '/dashboard/agencia' ||
|
||||||
|
(pathname !== null && pathname.startsWith('/dashboard/agencia/'));
|
||||||
|
|
||||||
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const isSocialOpen = expandedSections['social'] ?? isSocialRoute;
|
||||||
|
const isAgenciaOpen = expandedSections['agencia'] ?? isAgenciaRoute;
|
||||||
|
|
||||||
|
const toggleSection = (key: 'social' | 'agencia') => {
|
||||||
|
setExpandedSections((prev) => {
|
||||||
|
const current =
|
||||||
|
key === 'social'
|
||||||
|
? (prev['social'] ?? isSocialRoute)
|
||||||
|
: (prev['agencia'] ?? isAgenciaRoute);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[key]: !current,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const moduleOn = (id: DashboardModuleId) => {
|
const moduleOn = (id: DashboardModuleId) => {
|
||||||
if (isAgency) return true;
|
if (isAgency) return true;
|
||||||
if (!enabledModules) return true;
|
if (!enabledModules) return true;
|
||||||
@@ -246,8 +326,14 @@ export default function DashboardShell({
|
|||||||
const underSocial = item.moduleId === 'social';
|
const underSocial = item.moduleId === 'social';
|
||||||
return (
|
return (
|
||||||
<div key={item.href} className="space-y-0.5">
|
<div key={item.href} className="space-y-0.5">
|
||||||
<NavLink item={item} onNavigate={close} />
|
<NavLink
|
||||||
{underSocial ? (
|
item={item}
|
||||||
|
hasSubItems={underSocial}
|
||||||
|
isOpen={isSocialOpen}
|
||||||
|
onToggle={() => toggleSection('social')}
|
||||||
|
onNavigate={close}
|
||||||
|
/>
|
||||||
|
{underSocial && isSocialOpen ? (
|
||||||
<>
|
<>
|
||||||
{moduleOn('calendario') ? (
|
{moduleOn('calendario') ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -289,8 +375,12 @@ export default function DashboardShell({
|
|||||||
icon: Layers,
|
icon: Layers,
|
||||||
iconClass: 'text-amber-500',
|
iconClass: 'text-amber-500',
|
||||||
}}
|
}}
|
||||||
|
hasSubItems
|
||||||
|
isOpen={isAgenciaOpen}
|
||||||
|
onToggle={() => toggleSection('agencia')}
|
||||||
onNavigate={close}
|
onNavigate={close}
|
||||||
/>
|
/>
|
||||||
|
{isAgenciaOpen ? (
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/agencia/conexiones"
|
href="/dashboard/agencia/conexiones"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
@@ -299,6 +389,7 @@ export default function DashboardShell({
|
|||||||
<Plug className="h-3.5 w-3.5 shrink-0" />
|
<Plug className="h-3.5 w-3.5 shrink-0" />
|
||||||
Panel de control · clientes y conexiones
|
Panel de control · clientes y conexiones
|
||||||
</Link>
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<NavLink
|
<NavLink
|
||||||
item={{
|
item={{
|
||||||
|
|||||||
Reference in New Issue
Block a user