carlos@cesaints: ~/projects/tourism-portal.md — zsh

Comando: cat projects/tourism-portal.md

projects/tourism-portal.md · 2,5 KB

Protótipo de portal de turismo de alto padrão, com contrato de dados pronto para o back end

Protótipo navegável, só de front end e com dados fictícios, de um portal de turismo de alto padrão para uma região de lago no interior do Brasil, apresentado a um cliente. Reúne hospedagens, gastronomia, passeios, imóveis, eventos e serviços de parceiros locais, e cada conversão termina numa conversa de WhatsApp já preenchida, sem reserva nem pagamento. Os tipos compartilhados têm a forma de uma API real, para o back end entrar sem reescrever as telas.

abrir demonstraçãosimulação do sistema com dados fictícios · 6 telas

Papel
Único engenheiro, do front end ao pacote de entrega
Período
set/2026 – set/2026
O que eu fiz

Eu: Produto, arquitetura, contrato de dados, todas as telas do portal e do painel, portões de qualidade, testes e a documentação de entrega para o cliente e para o time de back end.

Projeto privado, descrito sem identificar cliente, produto ou empresa.

Diagrama

Telas presas a um contrato, não à origem do dado

Num monorepo com pnpm workspaces, um pacote de contratos só com tipos, sem runtime, define a forma dos dados. Uma camada de repositório assíncrona entrega esses dados às telas: hoje lê dados fictícios em memória, e a API entra nela depois. O portal público e o painel interno saem em bundles separados, e o painel publica no portal pelo armazenamento do navegador. O portal monta toda conversa pelo construtor único de link de WhatsApp, e um portão de entrega roda tipos, lint, testes, build e uma varredura do bundle.

Contexto

Um cliente queria um portal de turismo de alto padrão para uma região de lago no interior do Brasil: hospedagens, restaurantes, passeios náuticos, experiências, imóveis, espaços de evento e serviços de parceiros locais, cada item com página própria. Esta fase foi só de front end, com dados fictícios, para o cliente ver o produto funcionando antes de existir servidor e para outro time construir o back end depois. É um protótipo apresentado ao cliente: não foi publicado, não tem back end nem parceiro real, e não tem resultado de negócio. O portal não reserva nem cobra; por decisão jurídica, cada conversão termina numa conversa de WhatsApp com a casa, já preenchida.

O que eu construí

  • O portal. Home com a região desenhada em SVG, busca geral com filtros e contagem por faceta, índice por categoria, busca de estadia que confere a agenda de cada casa e a noite mínima, e fichas em sete formas: hospedagem com calendário, gastronomia com cardápio e comanda, passeio com roteiro e turnos, experiência, imóvel, evento e serviço. Todo preço é “a partir de”, com a data de atualização e o que está e não está incluso.
  • A conversão. Um construtor único monta a mensagem de WhatsApp no gabarito de cada intenção, com o código do registro e uma referência curta de origem, derivada de forma determinística com um alfabeto sem letras e números que se confundem ao telefone. Sem essa referência, o portal não teria como mostrar ao parceiro de onde veio o cliente.
  • A página do parceiro. Uma landing com a identidade da casa, fora da moldura do portal, e a cor testada em contraste.
  • O painel interno de demonstração. O que pede atenção hoje, conversas por estado com a mediana da primeira resposta, parceiros, registros e o conteúdo do site, que publica no portal em tempo real, no mesmo navegador. A entrada é uma maquete declarada: não autentica ninguém, de propósito, e o painel avisa que o conteúdo fica salvo só naquele navegador.
  • A documentação de entrega. Escrita para dois leitores, o cliente e o time de back end, com cada documento declarando o seu: arquitetura, contrato de API, esquema de banco, autenticação e papéis, portões de qualidade e a medição de fechamento.

O que eu faria diferente

Ampliaria a cobertura de ponta a ponta no começo, e não na reta final. Os dois defeitos que ela achou estavam no produto havia tempo e tinham passado por revisão visual; um deles só aparecia a 390 px de largura.

Restrições

  • Só front end nesta fase, com dados fictícios marcados no próprio dado.
  • O portal divulga e conecta; não reserva, não cobra e não tem conta de visitante, por decisão jurídica.
  • Nenhuma requisição de rede em tempo de execução, nem de fonte, nem de imagem.
  • Outro time vai construir o back end, então nenhuma tela pode depender de onde o dado vem.
  • Regras de texto (acentuação completa, sem travessão) conferidas por script.

Decisões

Contrato de dados num pacote só de tipos

Contexto
O back end viria depois, feito por outro time, e tipos copiados de um lado para o outro divergiriam no primeiro campo novo.
Escolha
Um pacote de contratos só com tipos, sem runtime nem dependência, consumido como fonte pelo app: ids e URLs marcados, dinheiro em centavos inteiros, datas ISO e uniões fechadas por categoria.
Ganhos
  • Divergência de forma vira erro de compilação no front.
  • O back end nasce aditivo, implementando o que o contrato já descreve.
Custos
  • Mais cerimônia para mudar um campo em plena fase de protótipo.

Forma de API desde o primeiro dia

Contexto
Nesta fase os dados moram em memória, mas a integração com a API não podia virar uma reescrita das telas.
Escolha
Uma camada de repositório assíncrona, com erro tipado em vez de exceção, cancelamento em todo método e paginação por cursor opaco, mesmo lendo um array.
Ganhos
  • Ligar a API troca a implementação das funções, não os componentes.
Custos
  • Código assíncrono e tratamento de erro para um dado que ainda é local.

Agenda determinística

Contexto
O calendário precisava de uma ocupação fictícia estável: o sábado livre no ensaio tinha de continuar livre na apresentação, e os testes não podiam oscilar.
Escolha
Ocupação calculada por hash do registro com a data, e o dia de origem recebido por parâmetro, nunca lido do relógio.
Ganhos
  • Mesma entrada, mesma agenda.
  • O gerador continua útil como fonte de teste depois que houver dado real.
Custos
  • É dado inventado com cara de real, então cada tela precisa dizer que nenhuma data fica reservada.

Página do parceiro fora da moldura do portal

Contexto
A página de cada parceiro precisava parecer o site da casa, com a cor e o ritmo dela, sem perder acessibilidade.
Escolha
Uma landing com cabeçalho e rodapé próprios; a cor da casa passa por teste de contraste nas duas direções e, se reprovar, a página usa a tinta do portal. Esconder a moldura com CSS foi descartado, porque deixaria navegação invisível na ordem de tabulação.
Ganhos
  • A casa ganha identidade própria sem abrir mão do contraste mínimo.
Custos
  • O portal perde presença justamente na página que mais converte.

Portões de entrega executados por máquina

Contexto
Um protótipo público não pode levar dado real, segredo nem o painel interno no bundle, e revisão visual não pega tudo.
Escolha
Um portão em sequência: texto, acentuação, tipos, lint com regras de segurança e acessibilidade, testes, build e uma varredura do bundle pronto que reprova telefone real, e-mail, CPF, CNPJ, chave, caminho de disco, sourcemap e código do painel.
Ganhos
  • O que dependia de disciplina vira falha de build.
Custos
  • Cada rodada de ajuste espera o portão inteiro.

Stack e por quê

React 19 e React Router 7
Portal e painel em duas entradas e dois bundles, sem código compartilhado entre eles.
TypeScript 5.9
O contrato de tipos consumido como fonte; versão estável escolhida pensando em quem herda o projeto.
Vite e pnpm workspaces
Monorepo com o app e o pacote de contratos.
Tailwind CSS
Os tokens da paleta, num tema claro com contraste conferido.
Vitest
Contrato, agenda, construtor de mensagens e a publicação do painel.
Playwright com axe
Ponta a ponta com acessibilidade em 1440 e 390 px; as capturas da entrega saem dos próprios testes.
ESLint 9
Regras de segurança e acessibilidade, e fetch proibido em tempo de execução.

Resultados

  • Um teste reprova o build se algum botão disser "reservar", "comprar" ou "pagar", e um portão sobre o bundle pronto reprova dado pessoal, chave, sourcemap e código do painel.

    repositório privado auditadoTestes e scripts do repositório(Auditoria do repositório privado, set/2026)
  • 1.237 casos de teste de unidade aprovados, em 22 arquivos, na medição de fechamento da entrega.

    declaradoMedição de fechamento do pacote de entrega (22/09/2026)
  • 306 casos de ponta a ponta com auditoria de acessibilidade, com as únicas falhas conhecidas, duas, no painel interno, fora do escopo da entrega.

    declaradoMedição de fechamento do pacote de entrega (22/09/2026)
  • A ampliação de 240 para 306 casos de ponta a ponta achou dois defeitos que tinham passado pela revisão visual: um mosaico de fotos estourando 24 px a 390 px de largura e uma lista de descrição com um nível a mais em todas as fichas.

    declaradoMedição de fechamento do pacote de entrega
  • 21 decisões técnicas registradas, cada uma com a alternativa recusada e o custo de reabrir.

    declaradoRegistro de decisões do pacote de entrega

Ângulo de segurança

Superfície de ataque

  • Bundle público que qualquer pessoa baixa, com o catálogo inteiro.
  • Links de WhatsApp montados a partir do item, das datas e do pedido.
  • Parâmetros de rota e de busca vindos da URL.
  • Painel interno que publica conteúdo no portal pelo armazenamento do navegador.

Controles implementados

  • Construtor único de link de WhatsApp: número validado, mensagem codificada e nada vindo da URL ou de texto digitado.
  • Parâmetro de rota validado e nunca usado como chave de objeto; sete rotas literais em vez de uma parametrizada.
  • Política de segurança num arquivo só, que gera os cabeçalhos da hospedagem no build e é auditada sobre o build servido.
  • Nenhuma requisição de rede em tempo de execução: o lint proíbe fetch.
  • Documento publicado pelo painel tratado como entrada hostil e remontado campo a campo contra listas fechadas.
  • Painel sem link no site, com noindex e em bundle separado, e um portão que prova que nada dele vaza para o público.

O que eu testaria hoje

  • Documento adulterado no armazenamento do navegador tentando injetar conteúdo ou link no portal.
  • Parâmetros de rota como __proto__ e constructor.
  • Quebra de linha ou texto extra injetado pela URL na mensagem de WhatsApp.

Evidências

  • repositório privado auditadoRepositório privado auditado(Auditoria do repositório privado, set/2026)
  • declaradoPacote de entrega com a medição de fechamento