Pular para o conteúdo principal

Página Contato — Site Público Abracaf

Página pública da Abracaf em /abracaf/contato. Reúne os canais de contato, um formulário validado de envio de mensagem e uma sidebar com mapa da sede, horários de atendimento e FAQ.

Estrutura de arquivos

apps/web/app/[locale]/(extranet_public)/abracaf/contato/
├── page.tsx # RSC — hero + cards + form + sidebar
└── _components/
│ ├── abracaf-contact-form.tsx # 'use client' — formulário react-hook-form + Zod
│ ├── abracaf-map-wrapper.tsx # next/dynamic ssr:false do mapa da sede
│ └── abracaf-map.tsx # 'use client' — mapa Leaflet da sede
└── _lib/
├── contact-abracaf.schema.ts # Schema Zod + tipo ContactData
└── server/
└── contact-abracaf-action.ts # Server action de envio (next-safe-action)

Estrutura da página

O page.tsx é um Server Component que monta o hero e a seção de conteúdo:

BlocoDescrição
HeroFundo bg-abc-navy-900, eyebrow "Fale com a Abracaf", H1 e subtítulo. Inclui o ScrollDownButton.
Cards de canais3 cards (Associados, Imprensa, Geral) com e-mail dedicado
FormulárioCard com ContactForm
SidebarCard da sede (mapa + endereço + contatos), card de atendimento e card de FAQ

A seção de conteúdo tem id="contato-conteudo" — destino do ScrollDownButton do hero.

Formulário de contato

ContactForm usa react-hook-form com Zod (zodResolver) e envia via next-safe-action (useAction + submitContact).

  • Campos: nome, cargo, e-mail, telefone, concessionária, tipo de solicitação, mensagem e aceite de LGPD.
  • Validação no modo onBlur.
  • Telefone com máscara dinâmica (maskPhone) — formata 10 ou 11 dígitos.
  • Estados de envio: idle / success / error. Em sucesso, mostra Alert de confirmação e reseta o form após 5s.
  • Cores de status usam tokens da Abracaf: abc-danger-* (erros) e abc-success-* (sucesso) — nunca red-*/green-*.
Nomenclatura: Intranet

A opção de assunto e o FAQ referem-se ao portal interno como Intranet (não "extranet"). O valor interno da option (suporte_extranet) é mantido por compatibilidade com o backend, mas o rótulo visível é "Suporte Intranet".

Select de assunto

O campo "Tipo de solicitação" usa o componente @kit/ui/select (shadcn / Base UI) — o mesmo padrão dos filtros de concessionárias. Antes era um <select> HTML nativo; foi substituído para manter consistência visual com o resto do site.

<Select value={field.value ?? ''} onValueChange={(v) => field.onChange(v as ContactData['tipo'])}>
<FormControl>
<SelectTrigger className={cn(inputClass, 'w-full')}>
<SelectValue placeholder="Selecione o assunto" />
</SelectTrigger>
</FormControl>
<SelectContent
side="bottom"
sideOffset={4}
alignItemWithTrigger={false}
collisionAvoidance={{ side: 'none' }}
>
{TIPO_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
collisionAvoidance — sempre abrir abaixo

O SelectContent deve abrir sempre abaixo do input. Como o projeto usa Base UI (não Radix), a prop é collisionAvoidance={{ side: 'none' }}side: 'none' desativa o flip de direção. A prop do Radix (avoidCollisions) não existe aqui e dispara erro de DOM. O SelectContent de @kit/ui/select repassa collisionAvoidance para o Positioner.

Textarea com auto-resize

O campo de mensagem cresce com o conteúdo (de 1 linha até no máximo 220px):

<Textarea
className={cn(inputClass, 'resize-none overflow-hidden py-2.5')}
rows={1}
{...field}
onInput={(e) => {
const el = e.currentTarget;
el.style.height = 'auto';
el.style.height = `${Math.min(el.scrollHeight, 220)}px`;
}}
/>

Botão de scroll do hero

O ScrollDownButton foi adicionado ao final do hero apontando para #contato-conteudo. Ver detalhes do componente em Design System → Botões de scroll.

import { ScrollDownButton } from '../_components/scroll-down-button';
// ...
<ScrollDownButton href="#contato-conteudo" />