| name | frontend |
| description | Convenções do frontend deste projeto (web/) — SPA React + Vite + TypeScript que consome a API Express via proxy /api. Use ao criar/alterar componentes, o estado do App, chamadas à API ou tipos compartilhados. Cobre o fluxo de estado centralizado em app.tsx, o cliente único api/client.ts, o padrão de painéis com callback onXxx, race-guard de requisições, e a nomenclatura kebab-case.tsx.
|
Frontend — SPA React + Vite (web/)
CSR puro. Proxy do Vite manda /api/* → http://localhost:3000. Sem lib de estado, sem
router: uma tela só, estado centralizado no App.
Arquitetura de estado (não fuja disso)
app.tsx é o dono do estado que importa (matches, page, level, weights, loading, error).
Os painéis laterais (upload-resume, crawl-panel, weight-sliders) fazem suas
próprias ações e, ao terminar, chamam um callback onXxx recebido por props. O App
responde disparando requestRefresh → refaz refreshMatches.
UploadResume onIngested ─┐
CrawlPanel onCrawled ─┼─► App.requestRefresh() ─► refreshMatches() ─► setMatches(...)
WeightSliders onChanged ─┘
MatchList ◄── recebe matches/page/level e emite onLevelChange / onPageChange
Regras que já estão no código e devem ser preservadas:
- Race-guard:
refreshMatches usa requestSeq (um useRef incremental). Toda resposta
checa if (seq !== requestSeq.current) return; antes de setar estado. Ao adicionar
qualquer fetch que compete (mudança rápida de filtro/página), replique esse padrão —
senão uma resposta antiga sobrescreve uma nova.
requestRefresh reseta page para 1 e bumpa refreshKey (que entra nas deps de
refreshMatches). Use-o para "recarregar do zero" após upload/crawl/mudança de pesos.
- Se o peso do nível filtrado zera, o filtro reseta sozinho — mantenha esse comportamento.
Cliente da API (ponto único)
Toda chamada HTTP passa por web/src/api/client.ts (objeto api). Componentes nunca
chamam fetch direto. Ao adicionar um endpoint:
- Adicione o tipo de request/response em
web/src/api/types.ts.
- Adicione o método em
api seguindo o padrão existente (helper request<T> já trata
!response.ok e extrai { error } do body). GET com querystring via URLSearchParams;
PUT/POST JSON com headers: { 'content-type': 'application/json' }.
- Os tipos são compartilhados conceitualmente com o backend (Seniority, weights, etc.) —
mantenha os nomes/estrutura em sincronia com as respostas reais das rotas.
Componentes
- Um componente por arquivo em
web/src/components/, nome kebab-case.tsx, export nomeado
(export function CrawlPanel(...)). Props tipadas por uma interface Props local.
- Padrão de ação assíncrona no componente:
busy/loading + error locais, try/catch/finally,
e chamar o callback de sucesso (onCrawled()) só depois do await dar certo. Ver
crawl-panel.tsx como referência.
- Estilos ficam em
web/src/styles.css com classes utilitárias já existentes (card,
hint, switch-row, ok, err, sources). Reuse-as antes de inventar CSS novo.
- Constantes de UI em
web/src/constants.ts (SENIORITY_LEVELS, MATCH_LEVEL_FILTERS,
WEIGHT_COMMIT_DELAY_MS, JOB_DESCRIPTION_PREVIEW_CHARS). Nada de número mágico solto.
Rodando e verificando
cd web && npm run dev
cd web && npm run build
O build faz o typecheck; um componente novo tem que passar por ele sem erro de tipo.