Mejora la navegación móvil del panel y admin
Extrae un AppNav reutilizable con menú hamburguesa desplegable en móvil y resaltado del enlace activo. Antes la marca y los enlaces se apretaban en una sola fila en pantallas pequeñas; ahora el nombre de empresa se oculta en móvil y los enlaces pasan a un desplegable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,29 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { requireAdmin } from '@/lib/auth/current-user';
|
import { requireAdmin } from '@/lib/auth/current-user';
|
||||||
|
import AppNav from '@/components/AppNav';
|
||||||
|
|
||||||
export const metadata: Metadata = { title: 'Admin · Reformix' };
|
export const metadata: Metadata = { title: 'Admin · Reformix' };
|
||||||
|
|
||||||
|
const ADMIN_LINKS = [
|
||||||
|
{ href: '/admin', label: 'Resumen' },
|
||||||
|
{ href: '/admin/usuarios', label: 'Usuarios' },
|
||||||
|
{ href: '/admin/planes', label: 'Planes' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||||||
await requireAdmin();
|
await requireAdmin();
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b border-gray-200">
|
<header className="sticky top-0 z-20 bg-white border-b border-gray-200">
|
||||||
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
<div className="relative max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||||
<Link href="/admin" className="flex items-center gap-3">
|
<Link href="/admin" className="flex items-center gap-2 min-w-0">
|
||||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-black text-white font-black italic text-lg leading-none">R</span>
|
<span className="inline-flex shrink-0 items-center justify-center w-8 h-8 rounded-lg bg-black text-white font-black italic text-lg leading-none">R</span>
|
||||||
<span className="font-extrabold tracking-tight text-black">Reformix</span>
|
<span className="font-extrabold tracking-tight text-black">Reformix</span>
|
||||||
<span className="text-gray-300">/</span>
|
<span className="hidden sm:inline text-gray-300">/</span>
|
||||||
<span className="text-sm font-medium text-gray-600">Admin</span>
|
<span className="hidden sm:inline text-sm font-medium text-gray-600">Admin</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-4 text-xs font-medium">
|
<AppNav links={ADMIN_LINKS} />
|
||||||
<Link href="/admin" className="text-gray-500 hover:text-black">Resumen</Link>
|
|
||||||
<Link href="/admin/usuarios" className="text-gray-500 hover:text-black">Usuarios</Link>
|
|
||||||
<Link href="/admin/planes" className="text-gray-500 hover:text-black">Planes</Link>
|
|
||||||
<form action="/logout" method="post"><button type="submit" className="text-gray-500 hover:text-black">Salir</button></form>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import { requireUser } from '@/lib/auth/current-user';
|
|||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { tenants } from '@/db/schema';
|
import { tenants } from '@/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import AppNav from '@/components/AppNav';
|
||||||
|
|
||||||
|
const PANEL_LINKS = [
|
||||||
|
{ href: '/panel', label: 'Leads' },
|
||||||
|
{ href: '/panel/precios', label: 'Precios' },
|
||||||
|
{ href: '/panel/empresa', label: 'Empresa' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Panel · Reformix',
|
title: 'Panel · Reformix',
|
||||||
@@ -19,24 +26,19 @@ export default async function PanelLayout({ children }: { children: React.ReactN
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<header className="sticky top-0 z-10 bg-white border-b border-gray-200">
|
<header className="sticky top-0 z-20 bg-white border-b border-gray-200">
|
||||||
<div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
<div className="relative max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||||
<Link href="/panel" className="flex items-center gap-3">
|
<Link href="/panel" className="flex items-center gap-2 min-w-0">
|
||||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-black text-white font-black italic text-lg leading-none">
|
<span className="inline-flex shrink-0 items-center justify-center w-8 h-8 rounded-lg bg-black text-white font-black italic text-lg leading-none">
|
||||||
R
|
R
|
||||||
</span>
|
</span>
|
||||||
<span className="font-extrabold tracking-tight text-black">Reformix</span>
|
<span className="font-extrabold tracking-tight text-black">Reformix</span>
|
||||||
<span className="text-gray-300">/</span>
|
<span className="hidden sm:inline text-gray-300">/</span>
|
||||||
<span className="text-sm font-medium text-gray-600">{nombreEmpresa}</span>
|
<span className="hidden sm:inline text-sm font-medium text-gray-600 truncate">
|
||||||
|
{nombreEmpresa}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-4 text-xs font-medium">
|
<AppNav links={PANEL_LINKS} />
|
||||||
<Link href="/panel" className="text-gray-500 hover:text-black">Leads</Link>
|
|
||||||
<Link href="/panel/precios" className="text-gray-500 hover:text-black">Precios</Link>
|
|
||||||
<Link href="/panel/empresa" className="text-gray-500 hover:text-black">Empresa</Link>
|
|
||||||
<form action="/logout" method="post">
|
|
||||||
<button type="submit" className="text-gray-500 hover:text-black">Salir</button>
|
|
||||||
</form>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
<main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
|
||||||
|
|||||||
105
mvp/b2c/src/components/AppNav.tsx
Normal file
105
mvp/b2c/src/components/AppNav.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
export type AppNavLink = { href: string; label: string };
|
||||||
|
|
||||||
|
// El enlace activo es el href más específico (más largo) que prefija el pathname.
|
||||||
|
function activeHref(pathname: string, links: readonly AppNavLink[]): string {
|
||||||
|
let best = '';
|
||||||
|
for (const l of links) {
|
||||||
|
if ((pathname === l.href || pathname.startsWith(l.href + '/')) && l.href.length > best.length) {
|
||||||
|
best = l.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppNav({ links }: { links: readonly AppNavLink[] }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const active = activeHref(pathname, links);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Escritorio */}
|
||||||
|
<nav className="hidden sm:flex items-center gap-4 text-xs font-medium">
|
||||||
|
{links.map((l) => (
|
||||||
|
<Link
|
||||||
|
key={l.href}
|
||||||
|
href={l.href}
|
||||||
|
className={active === l.href ? 'text-black font-semibold' : 'text-gray-500 hover:text-black'}
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<form action="/logout" method="post">
|
||||||
|
<button type="submit" className="text-gray-500 hover:text-black">
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Móvil: botón hamburguesa */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
aria-label={open ? 'Cerrar menú' : 'Abrir menú'}
|
||||||
|
aria-expanded={open}
|
||||||
|
className="sm:hidden inline-flex items-center justify-center w-10 h-10 -mr-2 rounded-lg text-gray-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||||
|
{open ? (
|
||||||
|
<>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
<line x1="6" y1="18" x2="18" y2="6" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<line x1="3" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Móvil: panel desplegable */}
|
||||||
|
{open && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-hidden="true"
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="sm:hidden fixed inset-0 top-16 z-10 bg-black/20 cursor-default"
|
||||||
|
/>
|
||||||
|
<nav className="sm:hidden absolute left-0 right-0 top-16 z-20 bg-white border-b border-gray-200 shadow-lg">
|
||||||
|
<div className="px-6 py-2 flex flex-col">
|
||||||
|
{links.map((l) => (
|
||||||
|
<Link
|
||||||
|
key={l.href}
|
||||||
|
href={l.href}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className={
|
||||||
|
'py-3 text-sm border-b border-gray-100 ' +
|
||||||
|
(active === l.href ? 'text-black font-semibold' : 'text-gray-600')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<form action="/logout" method="post" className="py-3">
|
||||||
|
<button type="submit" className="text-sm text-gray-600">
|
||||||
|
Salir
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user