Comando: cat projects/tourism-portal.md
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
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 entrega21 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