Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)
Adiciona o frontend Next.js em frontend/ (setup, design system, autenticação/tema, layout/navegação, área master, área tenant completa: clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas, equipe e perfil). Estende o backend de forma aditiva e autorizada: cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe + edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Sidebar } from '@/components/layout/sidebar';
|
||||
import { Topbar } from '@/components/layout/topbar';
|
||||
import { Drawer } from '@/components/layout/drawer';
|
||||
import { BottomTabBar } from '@/components/layout/bottom-tab-bar';
|
||||
import { Breadcrumb, type BreadcrumbEntry } from '@/components/layout/breadcrumb';
|
||||
import { persistSidebarCollapsed } from '@/lib/layout/sidebar-cookie';
|
||||
import type { NavItem } from '@/lib/layout/nav-items';
|
||||
|
||||
interface AppShellProps {
|
||||
/** Logo/marca completa (sidebar desktop expandida e header do drawer). */
|
||||
brand: React.ReactNode;
|
||||
/** Só o símbolo — sidebar desktop recolhida e topbar mobile. */
|
||||
brandCollapsed: React.ReactNode;
|
||||
items: NavItem[];
|
||||
bottomHrefs: string[];
|
||||
footer: React.ReactNode;
|
||||
/** Rodapé enxuto (só avatar + logout) para a sidebar recolhida. */
|
||||
footerCollapsed: React.ReactNode;
|
||||
initialCollapsed: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function findBreadcrumb(items: NavItem[], pathname: string): BreadcrumbEntry[] {
|
||||
for (const item of items) {
|
||||
if (item.children) {
|
||||
const child = item.children.find(
|
||||
(c) => pathname === c.href || pathname.startsWith(`${c.href}/`),
|
||||
);
|
||||
if (child) return [{ label: item.label }, { label: child.label }];
|
||||
}
|
||||
if (pathname === item.href || pathname.startsWith(`${item.href}/`)) {
|
||||
return [{ label: item.label }];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Casco de navegação das áreas autenticadas (Etapa 14): compõe
|
||||
* Sidebar/Topbar/Drawer/BottomTabBar num único lugar para não duplicar essa
|
||||
* orquestração entre o shell do master e o do tenant — cada um só passa
|
||||
* marca, itens de menu e rodapé de usuário diferentes.
|
||||
*/
|
||||
export function AppShell({
|
||||
brand,
|
||||
brandCollapsed,
|
||||
items,
|
||||
bottomHrefs,
|
||||
footer,
|
||||
footerCollapsed,
|
||||
initialCollapsed,
|
||||
children,
|
||||
}: AppShellProps) {
|
||||
const pathname = usePathname();
|
||||
const [collapsed, setCollapsed] = React.useState(initialCollapsed);
|
||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||
|
||||
const toggleCollapsed = React.useCallback(() => {
|
||||
setCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
persistSidebarCollapsed(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const bottomItems = React.useMemo(
|
||||
() =>
|
||||
bottomHrefs
|
||||
.map((href) => items.find((item) => item.href === href))
|
||||
.filter((item): item is NavItem => Boolean(item)),
|
||||
[items, bottomHrefs],
|
||||
);
|
||||
const moreItems = React.useMemo(
|
||||
() => items.filter((item) => !bottomHrefs.includes(item.href)),
|
||||
[items, bottomHrefs],
|
||||
);
|
||||
const breadcrumbItems = React.useMemo(() => findBreadcrumb(items, pathname), [items, pathname]);
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-neutral-50">
|
||||
<Sidebar
|
||||
brand={brand}
|
||||
brandCollapsed={brandCollapsed}
|
||||
items={items}
|
||||
footer={footer}
|
||||
footerCollapsed={footerCollapsed}
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={toggleCollapsed}
|
||||
/>
|
||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen} brand={brand} items={items} footer={footer} />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-h-dvh flex-col transition-[padding] duration-150',
|
||||
collapsed ? 'md:pl-16' : 'md:pl-60',
|
||||
)}
|
||||
>
|
||||
<Topbar
|
||||
breadcrumb={<Breadcrumb items={breadcrumbItems} />}
|
||||
mobileBrand={brandCollapsed}
|
||||
onMenuClick={() => setDrawerOpen(true)}
|
||||
/>
|
||||
<main className="flex-1 p-4 pb-20 md:p-6 md:pb-6">{children}</main>
|
||||
</div>
|
||||
|
||||
<BottomTabBar items={bottomItems} moreItems={moreItems} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user