Tema
Componentes — Layout e Editor
Componentes compartilhados localizados em frontend/app/components/.
LayoutWrapper
Arquivo: app/components/LayoutWrapper.tsx | Diretiva: "use client"
Componente de decisão de layout. Lê usePathname() e decide se renderiza o shell completo ou apenas os filhos sem decoração.
typescript
// Rotas sem layout (auth e legais):
/sign-in, /sign-up, /marketing, /politica-de-privacidade,
/exclusao-de-dados, /termos-de-uso
// Todas as demais rotas recebem:
<RBACProvider>
<SidebarProvider>
<LayoutShell>
{children}
</LayoutShell>
</SidebarProvider>
</RBACProvider>Ordem de providers é intencional: RBAC deve ser externo ao Sidebar pois a Sidebar usa useRBAC() para filtrar itens de menu por permissão.
LayoutShell
Arquivo: app/components/LayoutShell.tsx | Diretiva: "use client"
Monta a estrutura física da tela: sidebar, conteúdo, header mobile e bottom nav.
Comportamento:
- Bloqueia a renderização (
return null) enquantouseRBAC().loadingfor verdadeiro — impede flash de conteúdo não autorizado - Sidebar desktop: fixa,
h-screen, esconde em< lgviahidden lg:flex - Mobile Drawer:
fixed inset-0 z-[100], overlay preto + sidebar deslizante comslide-in-from-left - Conteúdo principal:
flex-1, margem esquerda dinâmica (lg:ml-20colapsado,lg:ml-64expandido),pb-16 lg:pb-0para acomodar BottomNav no mobile - Elementos
print-hide: Sidebar, MobileHeader e BottomNav são ocultos ao imprimir/exportar PDF
Desktop (lg+):
┌──────────────────────────────────────────────────────┐
│ Sidebar (fixed) │ Conteúdo Principal │
│ w-64 ou w-20 │ flex-1, min-h-screen │
└──────────────────────────────────────────────────────┘
Mobile (< lg):
┌──────────────────────────────────────────────────────┐
│ MobileHeader (sticky) │
│ Conteúdo Principal │
│ BottomNav (fixed bottom) │
└──────────────────────────────────────────────────────┘Sidebar
Arquivo: app/components/Sidebar.tsx | Diretiva: "use client"
Sidebar de navegação principal com dois modos de operação.
Props
typescript
interface SidebarProps {
isMobileDrawer?: boolean // default: false
onClose?: () => void
}Modo desktop: fixed left-0 top-0 h-screen, largura animada entre w-20 (colapsado) e w-64 (expandido).
Modo mobile drawer: relative w-72 h-full, sempre expandido, sem botão de toggle.
Itens de navegação por permissão
| Item | Rota | Permissão |
|---|---|---|
| Dashboard | /dashboard | app:dashboard |
| Clientes | /clients | app:customers |
| Campanhas | /campaigns | app:campaigns |
/whatsapp/dashboard | app:integrations | |
| Templates E-mail | /email-templates | app:campaigns |
| Segmentos | /segments/list | app:segments |
| Resultados Retail | /results/retail | app:sales |
| Resultados Canais | /results/channels | app:sales |
| Qualidade de Dados | /results/quality | app:sales |
| Agenda | /results/schedule | app:sales |
| RFM | /rfm | app:segments |
| Pesquisa Pós-Venda | /results/post-sale-survey | Super Admin |
| Configurações | /settings | app:settings |
| Admin | /admin | Super Admin (level=0) |
| Auditoria | /audit | app:audit |
| Inbox | /inbox | — |
NavItem (sub-componente interno)
typescript
function NavItem({
icon: React.ReactNode,
label: string,
href: string,
isCollapsed: boolean,
onClick?: () => void,
})- Detecta rota ativa via
usePathname() === href - Ativo:
bg-galdix-purple text-white - Inativo:
text-slate-500 hover:bg-slate-100(light) /hover:bg-slate-800/50(dark) - Modo colapsado: texto com
w-0 opacity-0; tooltip flutuanteabsolute left-fullaparece no hover
Ícones inline
Cinco ícones são declarados como SVGs inline para evitar bug de HMR do Turbopack com imports de novos ícones do lucide-react: IconMessagesSquare, IconBookText, IconGauge, IconStarSurvey, IconInboxManage.
Super Admin detection
typescript
const isSuperAdmin = profile?.role?.level === 0Itens de Admin e Pesquisa Pós-Venda só aparecem quando isSuperAdmin é verdadeiro.
MobileHeader
Arquivo: app/components/MobileHeader.tsx
Header fixo no topo em telas < lg. Contém:
- Botão hambúrguer →
openMobile()doSidebarContext - Logo GALDIX (link para
/dashboard) - Toggle dark/light mode
Oculto em lg+ via lg:hidden. Adicionado ao DOM com print-hide para não aparecer em PDFs.
BottomNav
Arquivo: app/components/BottomNav.tsx
Barra de navegação fixa na parte inferior para mobile (< lg). Exibe até 5 atalhos principais filtrados por permissão. Usa usePathname() para marcar item ativo.
ThemeProvider
Arquivo: app/components/ThemeProvider.tsx
Wrapper fino sobre next-themes. Configurado com attribute="class" e defaultTheme="light".
typescript
// Instanciado no RootLayout (app/layout.tsx):
<ThemeProvider>
<LayoutWrapper>{children}</LayoutWrapper>
</ThemeProvider>Componentes que precisam ajustar comportamento por tema usam useTheme() do next-themes (ex: cores de gráficos Recharts que diferem entre dark/light).
GaldixIcon
Arquivo: app/components/GaldixIcon.tsx
SVG inline do logotipo Galdix. Aceita:
typescript
interface GaldixIconProps {
size?: number // default: 24
color?: string // default: "currentColor"
variant?: 'full' | 'mark' // 'full' = símbolo completo, 'mark' = só marca
}Usado na Sidebar (dentro do botão roxo no header) e nos favicons.
EmailEditor
Arquivo: app/components/editor/EmailEditor.tsx
Editor visual de emails baseado em GrapesJS com plugin grapesjs-mjml.
Props
typescript
interface EmailEditorProps {
initialData?: any // JSON salvo do GrapesJS para carregar estado anterior
onSave: (html: string, json: any, thumbnail?: Blob | null) => void
onClose: () => void
getToken?: () => Promise<string | null> // para upload de imagens no MinIO
}Inicialização
O editor é inicializado com require() (não import) para evitar problemas de SSR do Next.js com módulos que dependem de document:
typescript
const grapesjs = require('grapesjs')
const grapesjsMjml = require('grapesjs-mjml')O CSS do GrapesJS é injetado dinamicamente no <head> via <link> (o import de CSS quebraria o Turbopack).
Estrutura de painéis
┌────────────────────────────────────────────────────────┐
│ Toolbar: Undo | Redo | Desktop | Mobile | Preview │
├───────────────┬────────────────────────────────────────┤
│ Sidebar │ Canvas (GrapesJS iframe) │
│ ├ Conteúdo │ │
│ │ BlocksPanel (blocos MJML arrastáveis) │
│ │ PropertiesPanel (propriedades do bloco selecionado)│
│ └ Configurações │
│ SettingsPanel (background, fonte, largura global) │
└───────────────┴────────────────────────────────────────┘Blocos MJML registrados
Text, Image, Button, Divider, Spacer, Social, Column (1, 2, 3 cols), Section, Hero, HTML raw.
Modais integrados
| Modal | Trigger |
|---|---|
ImageUploadModal | Clique em bloco de imagem |
MergeTagModal | Botão no Rich Text Editor |
LinkModal | Clique em link no RTE |
Upload de imagens
Ao inserir imagem no editor, o ImageUploadModal faz upload para o backend (POST /email-templates/upload-image), que repassa para o MinIO. A URL pública retornada é inserida como src no bloco de imagem.
Thumbnail para listagem
Ao salvar, o editor captura um screenshot do canvas via API interna do GrapesJS e gera um Blob PNG passado no callback onSave. O backend armazena no MinIO e usa como thumbnail na listagem de templates.
Exportação MJML → HTML
O GrapesJS salva o estado como JSON (editor.getProjectData()). Para envio de campanha, o backend recebe esse JSON e usa o html-processor.ts para compilar o MJML gerado para HTML final com tracking injetado.
PostSaleSurveySettings
Arquivo: app/components/PostSaleSurveySettings.tsx
Componente de configuração de pesquisa pós-venda. Renderizado na aba correspondente em /settings. Gerencia configurações de CSAT e NPS incluindo template de mensagem, janela de envio (horas após a venda) e lojas habilitadas.