Skip to content

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) enquanto useRBAC().loading for verdadeiro — impede flash de conteúdo não autorizado
  • Sidebar desktop: fixa, h-screen, esconde em < lg via hidden lg:flex
  • Mobile Drawer: fixed inset-0 z-[100], overlay preto + sidebar deslizante com slide-in-from-left
  • Conteúdo principal: flex-1, margem esquerda dinâmica (lg:ml-20 colapsado, lg:ml-64 expandido), pb-16 lg:pb-0 para 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)                             │
└──────────────────────────────────────────────────────┘

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

ItemRotaPermissão
Dashboard/dashboardapp:dashboard
Clientes/clientsapp:customers
Campanhas/campaignsapp:campaigns
WhatsApp/whatsapp/dashboardapp:integrations
Templates E-mail/email-templatesapp:campaigns
Segmentos/segments/listapp:segments
Resultados Retail/results/retailapp:sales
Resultados Canais/results/channelsapp:sales
Qualidade de Dados/results/qualityapp:sales
Agenda/results/scheduleapp:sales
RFM/rfmapp:segments
Pesquisa Pós-Venda/results/post-sale-surveySuper Admin
Configurações/settingsapp:settings
Admin/adminSuper Admin (level=0)
Auditoria/auditapp:audit
Inbox/inbox
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 flutuante absolute left-full aparece 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 === 0

Itens 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() do SidebarContext
  • 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

ModalTrigger
ImageUploadModalClique em bloco de imagem
MergeTagModalBotão no Rich Text Editor
LinkModalClique 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.

Documentação interna — Galdix CRM