diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..62cee5c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,107 @@ +# CI e deploy num arquivo só: o Gitea lê .gitea/workflows e ignora .github/workflows quando esta pasta existe. +# - verify: lint, tipos, testes e build. O deploy só roda se ele passar. +# - audit: alertas de segurança das dependências (fica vermelho sozinho, sem travar o deploy). +# - e2e: testes de ponta a ponta na build de produção (informativo). +# - deploy: Vercel CLI. Branch padrão -> produção; demais branches -> preview; pull requests não publicam. +# Token: secret VERCEL_TOKEN de nível de usuário no Gitea. Projeto na Vercel = nome do repo +# (ou a variável VERCEL_PROJECT do repo). O build roda na Vercel; o runner só envia o código. +name: CI + +"on": + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm test -- --runInBand + - run: pnpm build + + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm audit --prod --audit-level=high + - run: pnpm audit --dev --audit-level=high + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm build + - name: Subir o servidor de produção + run: pnpm start -p 3001 & + - name: Esperar a porta 3001 + run: | + for attempt in $(seq 1 60); do + if curl --silent --fail --output /dev/null http://localhost:3001/; then + exit 0 + fi + sleep 2 + done + echo "O servidor não respondeu na porta 3001 em 120 s" >&2 + exit 1 + - name: Testes de ponta a ponta + run: pnpm test:e2e + env: + BASE_URL: http://localhost:3001 + - name: Publicar test-results/ + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: e2e-test-results + path: test-results/ + if-no-files-found: ignore + retention-days: 7 + + deploy: + needs: verify + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_SCOPE: asemlk54-1928s-projects + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 22 + # versão fixa do CLI: um pacote sem versão num pipeline de produção é risco de cadeia de suprimentos + - run: npm install --global vercel@61.1.0 + - name: Deploy + run: | + PROJECT=$(echo "${{ vars.VERCEL_PROJECT || github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') + if [ "${{ github.ref_name }}" = "${{ github.event.repository.default_branch }}" ]; then + PROD=--prod + else + PROD= + fi + vercel link --yes --project "$PROJECT" --scope "$VERCEL_SCOPE" --token "$VERCEL_TOKEN" + vercel deploy $PROD --token "$VERCEL_TOKEN" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index db405ea..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CI - -"on": - push: - pull_request: - -permissions: - contents: read - -jobs: - verify: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm typecheck - - run: pnpm test -- --runInBand - - run: pnpm build - - run: pnpm audit --prod --audit-level=high - - run: pnpm audit --dev --audit-level=high - - e2e: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm exec playwright install --with-deps chromium - - run: pnpm build - - name: Subir o servidor de produção - run: pnpm start -p 3001 & - - name: Esperar a porta 3001 - run: | - for attempt in $(seq 1 60); do - if curl --silent --fail --output /dev/null http://localhost:3001/; then - exit 0 - fi - sleep 2 - done - echo "O servidor não respondeu na porta 3001 em 120 s" >&2 - exit 1 - - name: Testes de ponta a ponta - run: pnpm test:e2e - env: - BASE_URL: http://localhost:3001 - - name: Publicar test-results/ - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: e2e-test-results - path: test-results/ - if-no-files-found: ignore - retention-days: 7 diff --git a/README.md b/README.md index c6ceec7..9869826 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,142 @@
-# Evandro.dev.br: software sob medida +Logo do Evandro.dev.br -Site comercial de Evandro Fonseca Junior para projetos de software sob medida: sistemas empresariais, aplicativos e integrações, com cases e contato direto por WhatsApp. +# Evandro.dev.br — portfólio + +**Portfólio profissional de Evandro Fonseca Junior: projetos, cases, serviços, +currículo e contato por WhatsApp — com hero e notebook 3D progressivos que caem +para HTML puro quando o dispositivo não comporta.** + +![Next.js](https://img.shields.io/badge/Next.js-16.3.0-informational) +![React](https://img.shields.io/badge/React-19.1.0-informational) +![Node](https://img.shields.io/badge/Node-22.x-informational) + +[Rotas](#o-que-o-site-entrega) · [Como funciona](#como-funciona) · [Execução local](#execu%C3%A7%C3%A3o-local) · [CI/CD](#cicd)
-## Requisitos +> [!NOTE] +> **Estado do projeto:** em desenvolvimento ativo (último commit: "refine portfolio +> showcase, services and quality lab"); o site de produção é `https://evandro.dev.br`, +> que é o host declarado em `app/sitemap.ts` e `app/robots.ts`. As ações externas +> de SEO/GEO (Search Console, Bing, Cloudflare, CrUX) estão descritas como runbook e +> **não executadas** em [`docs/seo-geo-operations.md`](docs/seo-geo-operations.md). -- Node.js `22.x` -- pnpm `9.15.9`, fixado em `package.json#packageManager` +> [!IMPORTANT] +> **Next.js `16.3.0` é intencional.** A `16.2.11` ainda mantinha três advisories +> transitivos de severidade alta; a `16.3.0` foi a menor versão estável da mesma +> major que deixou as auditorias atuais sem vulnerabilidades high. Não faça +> downgrade sem repetir `pnpm audit`. -O projeto usa Next.js `16.3.3`, patch de segurança que corrige [GHSA-p293-qw3h-jr36](https://github.com/advisories/GHSA-p293-qw3h-jr36) e [GHSA-2xp9-vwfh-vxw4](https://github.com/advisories/GHSA-2xp9-vwfh-vxw4). A atualização substitui `16.3.0`, afetada por esses avisos. As auditorias de produção e desenvolvimento continuam sendo verificadas na CI. - -## Início rápido - -```bash -corepack pnpm@9.15.9 install --frozen-lockfile -corepack pnpm@9.15.9 dev -``` - -O servidor de desenvolvimento usa Turbopack. Para executar o artefato de produção: - -```bash -corepack pnpm@9.15.9 build -corepack pnpm@9.15.9 start -``` - -## Scripts - -| Comando | Comportamento | -| --- | --- | -| `pnpm dev` | Inicia o Next.js em desenvolvimento com Turbopack. | -| `pnpm build` | Gera o build de produção com Turbopack. | -| `pnpm start` | Serve um build já gerado. | -| `pnpm lint` | Executa `biome check`. | -| `pnpm typecheck` | Gera os tipos de rota com `next typegen` e executa TypeScript sem emitir arquivos. | -| `pnpm format` | Formata o repositório com Biome. | -| `pnpm test` | Executa a suíte Jest. | -| `pnpm test:watch` | Executa Jest em modo watch. | -| `pnpm check:projects` | Verifica URLs de demonstração e substitui atomicamente `data/project-health.json`. | - -## Vitrine de vídeos dos projetos - -Cada projeto com site gravado aparece de três formas na seção Projetos da home. Tudo mora em `app/components/showcase/`, os dados em `lib/project-showcase.ts` e os estilos em `showcase.css`. - -- **Cards.** O vídeo de rolagem fica numa moldura de navegador com o endereço real do site. Só um vídeo toca por vez: o do card sob o mouse (ou com foco) e, sem mouse, o mais perto do centro da tela. Nada é baixado até o card tocar pela primeira vez, e um botão pausa os vídeos que se mexem sozinhos. Clicar na moldura abre o vídeo ampliado (`` nativo, com troca computador/celular que mantém o ponto do site, capítulos clicáveis e controles próprios). -- **Vitrine animada.** Logo abaixo dos cards, uma cena presa na rolagem (`position: sticky`) em que o site de cada projeto rola dentro de um notebook, com um celular ao lado, conforme o visitante rola a página. Os capítulos de cada projeto (`chapters` em `lib/project-showcase.ts`, marcados nos quadros dos vídeos) nomeiam o trecho na tela e mantêm notebook e celular no mesmo ponto do site. Só o projeto em cena e os vizinhos baixam vídeo, e nada baixa com "economizar dados" ligado. Com `prefers-reduced-motion` ou sem JavaScript ela some e os cards continuam mostrando os posters. -- **Capa de compartilhamento.** `/cases/` usa o poster do projeto como fundo da imagem Open Graph (`lib/og-card.tsx`), quando existe. - -Arquivos de cada projeto em `public/videos//`: - -| Arquivo | Para quê | -| --- | --- | -| `desktop.{webm,mp4,webp}` e `mobile.{webm,mp4,webp}` | cards e vídeo ampliado (até 3 MB cada vídeo) | -| `scrub-desktop.mp4` | vitrine animada: quadro-chave a cada 6 quadros, sem B-frames, para o tempo mudar a cada quadro da rolagem (o celular da moldura mostra só o pôster) | -| `og.jpg` | capa 1200x630 de compartilhamento | - -Para gravar ou regravar um projeto, use `scripts/record-project-videos.mjs`; para gerar os `scrub-*.mp4`, `scripts/encode-scrub-video.mjs` (as instruções de instalação do Playwright e do ffmpeg estão no cabeçalho de cada script). Depois de gravar, marque os capítulos do projeto em `lib/project-showcase.ts`. - -## Stack - -| Área | Pacotes usados | -| --- | --- | -| Aplicação | Next.js 16.3.3, React 19.1.0, React DOM 19.1.0 e TypeScript 5.9.2. | -| Estilos e composição | Tailwind CSS 4, `clsx` e `tailwind-merge`. | -| Movimento | Motion 12 e GSAP 3. | -| 3D e gráficos | Three.js, React Three Fiber e Drei. | -| Interface | Lucide React. | -| Qualidade | Biome, Jest, Testing Library e ambiente JSDOM. | - -Todas as dependências diretas estão fixadas em versões exatas no manifesto; o lockfile é instalado sem atualização implícita. - -## Rotas +## O que o site entrega | Rota | Conteúdo | | --- | --- | -| `/` | Homepage comercial com hero, notebook 3D, projetos, soluções, processo de trabalho, engenharia com IA, critérios de entrega, perfil e contato. | -| `/cases` | Índice dos seis cases. | -| `/cases/nutriscan` | Case NutriScan. | -| `/cases/twins-lab` | Case Twins Lab. | -| `/cases/reddit-explorer` | Case Reddit Posts Explorer. | -| `/cases/sc-plus` | Case SC Plus Aquicultura. | -| `/cases/gcc-installer` | Case MinGW GCC Installer. | -| `/cases/as-servicos` | Case AS Serviços. | -| `/servicos` | Página de destino comercial: sites, sistemas e aplicativos, contato direto no WhatsApp, cases, processo e acesso aos seis serviços. | -| `/servicos/criacao-de-sites` | Criação de sites. | -| `/servicos/engenharia-de-software` | Sistemas web e aplicativos mobile sob medida. | -| `/servicos/sob-medida` | Redirecionamento permanente (308) para `/servicos/engenharia-de-software`, preservando query string, UTMs e identificadores de clique. | -| `/servicos/consultoria-nextjs` | Consultoria Next.js. | -| `/servicos/tauri-rust` | Aplicativos desktop para Windows e Linux, com compatibilidade definida por projeto. | -| `/servicos/integracao-hardware-software-iot` | Integração de hardware, software e IoT. | -| `/servicos/arquitetura-de-software` | Arquitetura de software. | -| `/curriculo` | Currículo em HTML, com download de `/Profile.pdf`. | +| `/` | Homepage com hero, notebook de projetos, perfil, competências, experiência, projetos, modalidades de trabalho e contato | +| `/cases` | Índice dos seis cases | +| `/cases/nutriscan` | Case NutriScan | +| `/cases/twins-lab` | Case Twins Lab | +| `/cases/reddit-explorer` | Case Reddit Posts Explorer | +| `/cases/sc-plus` | Case SC Plus Aquicultura | +| `/cases/gcc-installer` | Case MinGW GCC Installer | +| `/cases/as-servicos` | Case AS Serviços | +| `/servicos` | Índice dos seis serviços, com explicações de uso e acesso ao detalhamento técnico | +| `/servicos/criacao-de-sites` | Criação de sites | +| `/servicos/engenharia-de-software` | Sistemas web e aplicativos mobile sob medida | +| `/servicos/consultoria-nextjs` | Consultoria Next.js | +| `/servicos/tauri-rust` | Aplicativos desktop para Windows e Linux, com compatibilidade definida por projeto | +| `/servicos/integracao-hardware-software-iot` | Integração de hardware, software e IoT | +| `/servicos/arquitetura-de-software` | Arquitetura de software | +| `/curriculo` | Currículo em HTML, com download de `/Profile.pdf` | +| `/privacidade` | Página de privacidade e consentimento de analytics | -O App Router também gera `/manifest.webmanifest`, `/robots.txt`, `/sitemap.xml`, `/opengraph-image`, `/icon.png`, `/apple-icon.png` e `/favicon.ico`. Caminhos desconhecidos usam a página 404 do projeto. Não há API route ativa. +O App Router também gera `/manifest.webmanifest`, `/robots.txt`, `/sitemap.xml`, +`/opengraph-image`, `/icon.svg` e `/favicon.ico`. Caminhos desconhecidos usam a +página 404 do projeto. Não há API route ativa. -As páginas de serviços mantêm escopo, exemplos ilustrativos e etapas visíveis; tecnologias e decisões de implementação ficam em um `
` nativo. Na homepage, competências e experiência também ficam recolhidas. O header prioriza Soluções (`/#modalities`), Projetos (`/cases`), Como trabalho (`/#process`), Sobre (`/#about`) e Contato (`/#contact`), com ajuste de chegada após a definição do layout do notebook. Currículo e download do PDF são links secundários no final do rodapé. +As páginas de serviços mantêm escopo, exemplos ilustrativos e etapas visíveis; +tecnologias e decisões de implementação ficam em um `
` nativo. Sobre e +Contato apontam para `/#about` e `/#contact`, com ajuste de chegada após a +definição do layout do notebook. -## Comportamento da aplicação +## Como funciona -### Conteúdo e project health +```mermaid +flowchart LR + A["public/portfolio-data.json
+ lib/site-content.ts"] --> B["lib/portfolio-data.ts
loadFeaturedProjects()"] + C["data/project-health.json
snapshot versionado"] --> B + D["scripts/check-projects.mjs
pnpm check:projects"] -->|rename atômico| C + B --> E["app/ — App Router
homepage, cases, serviços"] + E --> F["Next.js 16.3.0
build e start (Turbopack)"] +``` -O conteúdo principal vem de `public/portfolio-data.json` e `lib/site-content.ts`. A seleção da homepage usa o campo `featured`; o snapshot de disponibilidade não remove cards. Ele apenas controla o estado do CTA de cada demo: disponível, indisponível, desconhecido ou sem demo. +
+Conteúdo e project health -Em produção, a renderização só lê o snapshot versionado e não consulta demos externas. Em desenvolvimento, um snapshot ausente ou com mais de 15 minutos é atualizado em memória. A atualização manual opcional é `pnpm check:projects` e grava o arquivo por rename atômico. O contrato completo está em [`docs/project-health.md`](docs/project-health.md). +O conteúdo principal vem de `public/portfolio-data.json` e `lib/site-content.ts`. +A seleção da homepage usa o campo `featured`; o snapshot de disponibilidade não +remove cards. Ele apenas controla o estado do CTA de cada demo: disponível, +indisponível, desconhecido ou sem demo. -### Posicionamento comercial e uso de IA +Em produção, a renderização só lê o snapshot versionado e não consulta demos +externas. Em desenvolvimento, um snapshot ausente ou com mais de 15 minutos é +atualizado em memória. A atualização manual opcional é `pnpm check:projects` e +grava o arquivo por rename atômico. O contrato completo está em +[`docs/project-health.md`](docs/project-health.md). -A comunicação apresenta Evandro como responsável direto pelo projeto, sem sugerir equipe, disponibilidade garantida, prazo fixo ou certificação. Os cards de projetos reutilizam os desafios e resumos dos cases quando disponíveis. O antigo painel de telemetria fictícia foi removido; os critérios de qualidade não representam notas de auditoria. +
-Cada card de projeto aparece ao entrar na tela, observado individualmente e sem recorte por `clip-path`. A revelação não depende de uma porcentagem visível do grid inteiro: em celulares, a lista de uma coluna pode ser várias vezes mais alta que a tela e tornar esse gatilho inalcançável. +
+3D progressivo e movimento reduzido -`app/ia/page.tsx` (rota `/ia`) explica arquitetura, permissões, validação e recuperação de dados, com três relatos históricos de 2025 (na seção recolhida "O que casos documentados ensinam") e links junto de cada afirmação: +O fundo 3D do hero (Three.js, sem React Three Fiber) só é montado a partir de +`1024px`, com ponteiro fino, sem `prefers-reduced-motion: reduce`, sem economia +de dados e com placa de vídeo real (WebGL por software mostra o hero estático). +A cena monta depois da primeira interação ou com o navegador ocioso, desenha no +máximo ~30 quadros por segundo e para quando nada se move. No celular e nos +demais casos, o hero é estático e ocupa uma tela. -- Lovable: exposição relatada pela Semafor e CVE-2025-48757, incluindo a contestação do fornecedor no NVD; exposição não é apresentada como roubo comprovado. -- Replit: exclusão de dados e restauração integral, conforme a resposta oficial da empresa. -- Base44: falha de autenticação identificada pela Wiz, corrigida em menos de 24 horas, sem evidência reportada de exploração maliciosa. +As passagens entre seções e a barra de progresso são CSS (`animation-timeline`); +a rolagem suave (Lenis) só existe com ponteiro fino. -As obrigações dos pequenos agentes de tratamento são referenciadas à Resolução CD/ANPD nº 2/2022, arts. 6º, 12 e 13. Ao atualizar esse conteúdo, preservar datas, limites dos relatos e a distinção entre vulnerabilidade, exposição e incidente confirmado. +
-O formulário valida os campos no navegador e abre um rascunho no WhatsApp para revisão. Ele não envia a mensagem automaticamente e orienta a não informar senhas nem dados pessoais de clientes. +
+Contato e consentimento de analytics -A seção de contato tem título estático único, canais diretos à esquerda e formulário à direita (empilhados no celular). A silhueta do WhatsApp aparece no contato direto, no cabeçalho do formulário e no botão verde. O formulário não se move no hover; campos mantêm rótulos, validação nativa e foco visível. O link direto dispensa preenchimento e o e-mail usa `mailto:`. +O formulário da homepage valida os campos nativos de nome, e-mail, tipo e +contexto. Ao enviar, ele abre `wa.me` em uma nova aba com o briefing +preenchido; não existe backend de formulário. O rodapé mantém contato direto por +e-mail. -Na página `/servicos` e nos seis detalhamentos, `WhatsAppLink` oferece um link nativo com mensagem contextual, sem exigir o formulário da home. Funciona mesmo sem consentimento de analytics ou JavaScript; a medição depende da hidratação e da tag. A página principal prioriza as três intenções dos anúncios antes das especialidades técnicas e usa os cases existentes NutriScan e SC Plus como exemplos de trabalho, não como promessa de retorno comercial. +Google Analytics só é carregado depois de aceite explícito. A decisão fica em +`evandro.analytics-consent.v1`; recusar não monta o loader. As preferências +permanecem acessíveis e a revogação envia consentimento negado, ativa a flag de +desabilitação, remove o loader e expira apenas cookies `_ga`/`_ga_*` +acessíveis. -### 3D progressivo e movimento reduzido +
-O hero combina a mensagem comercial com três placas 3D: interface, integrações e dados. A interface usa a captura real do NutriScan, recortada acima do rodapé; as outras camadas são ilustrativas, não uma declaração da arquitetura do projeto. Ponteiro, arraste horizontal e quatro botões permitem explorar a composição. O toque vertical continua rolando a página. A renderização para quando os movimentos terminam, fora do viewport e com a aba oculta. Movimento reduzido ou falha gráfica mantêm a captura HTML, os controles e o link do case disponíveis. +
+Stack e estrutura -O notebook 3D permanece em celulares, tablets e desktop: tampa, tela, abas e cursor seguem o progresso do scroll. No mobile, a câmera adapta o enquadramento e botões externos à tela do notebook permitem selecionar os projetos. A cena renderiza sob demanda e pausa fora do viewport ou com a página oculta. Movimento reduzido, falha de WebGL ou perda de contexto ativam a navegação HTML sem animação. - -As quatro capturas desktop reais ficam em `public/assets/notebook/`, em WebP de 1600 × 1000. A troca aguarda a decodificação da próxima imagem, mantendo a anterior visível durante o carregamento ou uma falha. - -Uma entrada normal na homepage começa no topo. A recarga também volta ao início, removendo o fragmento deixado por uma navegação anterior. Links diretos como `/#about` continuam chegando à seção após a estabilização do notebook; rolagem deliberada durante a abertura cancela esse ajuste. A política inicial de scroll é aplicada antes da hidratação, sem disputar com a rolagem suave dos links posteriores. Assim que a chegada se resolve, a restauração volta a pertencer ao navegador: Voltar e Avançar restauram a posição do histórico em vez de forçar o topo. - -### Identidade visual - -O header usa `--site-header-height`: 60px abaixo de 1280px e 68px a partir desse breakpoint. A mesma variável define a posição do menu, o respiro inicial do hero e a compensação das âncoras. O menu mobile tem fundo opaco; a barra fica levemente translúcida ao rolar. Header e hero usam Space Grotesk para leitura comercial, reservando a fonte monoespaçada aos detalhes técnicos das demais seções. - -Os quatro arquivos recebidos estão preservados, com nomes descritivos, em `imagens/originais/`. As versões prontas para uso ficam em `public/brand/`: - -| Arquivo | Aplicação | +| Área | Pacotes usados | | --- | --- | -| `wordmark-dark.webp` | Logo horizontal transparente, branca e lima, no cabeçalho escuro. | -| `wordmark-light.webp` | Logo horizontal transparente, escura e lima, no rodapé claro. | -| `stacked-light.webp` | Logo empilhada transparente, reservada para composições maiores em fundo claro. | -| `mark-dark.svg` / `mark-light.svg` | Símbolo vetorial para fundos escuros e claros, traçado a partir do original limpo. | -| `mark-dark.png` | Símbolo de 512px incorporado aos cards Open Graph de todas as rotas. | -| `icon-192.png` | Ícone de instalação do manifesto. | +| Aplicação | Next.js 16.3.8, React 19.1.0, React DOM 19.1.0 e TypeScript 5.9.2 | +| Estilos | Tailwind CSS 4 | +| Movimento | CSS (`animation-timeline`) e Lenis | +| 3D | Three.js | +| Interface | Lucide React | +| Qualidade | Biome, Jest, Testing Library (JSDOM), Playwright e axe-core | -`app/icon.png` (512px), `app/apple-icon.png` (180px) e `app/favicon.ico` (16–256px) usam o símbolo sobre fundo escuro. As variantes mantêm as formas originais; o recorte transparente com bordas danificadas permanece arquivado, sem uso na interface. - -### Contato - -O formulário da homepage valida os campos nativos de nome, e-mail, tipo e contexto. Ao enviar, ele abre `wa.me` em uma nova aba com o briefing preenchido; não existe backend de formulário. O rodapé mantém contato direto por e-mail. - -### Consentimento de analytics - -Google Analytics só é carregado depois de aceite explícito. A decisão fica em `evandro.analytics-consent.v1`; recusar não monta o loader. As preferências permanecem acessíveis e a revogação envia consentimento negado, ativa a flag de desabilitação, remove o loader e expira apenas cookies `_ga`/`_ga_*` acessíveis. - -O único loader `gtag.js` configura GA4 `G-QTT9QT0T3S` e Google Ads `AW-18432694647`. Os IDs `GT-M3K4CKQT` e `GT-NBBR3PC2` informados no painel são identificadores das tags, não instalações adicionais. A integração segue https://developers.google.com/tag-platform/gtagjs/configure. `ad_storage`, `ad_user_data` e `ad_personalization` permanecem negados, com `ads_data_redaction` ativo. A tag só é montada em produção. - -### Medição do contato e Google Ads - -`lib/analytics.ts` envia `whatsapp_open` exclusivamente ao GA4 após o acionamento de um link ou a submissão válida do formulário. O evento representa **tentativa de abrir uma conversa**, não mensagem recebida, lead confirmado ou venda. O contato não espera a resposta do Analytics. Eventos recusados ou ocorridos antes do consentimento não são guardados para envio posterior. - -Parâmetros enviados: `service` (rótulo estático), `placement` (posição do CTA) e `page_path`. A URL do evento não inclui query, fragmento ou referenciador; rotas dinâmicas são agrupadas como `/servicos/[slug]` e `/cases/[slug]`. Nome, e-mail, mensagem digitada e URL de destino do WhatsApp não entram nesse evento. A distinção do serviço usa o parâmetro `service`. - -Posições: `service_hub_hero`, `service_hub_intent`, `service_hub_footer`, `service_detail_hero`, `service_detail_footer`, `contact_direct` e `contact_form`. Isso permite comparar entradas de contato pelo tipo de serviço e pela posição na página. Cliques pelo menu de contexto, bloqueadores e falhas de rede podem não ser medidos. - -**Configuração externa ainda necessária:** verificar `whatsapp_open` no GA4, registrá-lo como evento principal, vincular a propriedade à conta Ads correta e importar essa ação de conversão. Conferir a ação existente antes para evitar dupla contagem; não importar simultaneamente um clique automático como a mesma conversão. Para analisar os parâmetros em Explorações, cadastrar dimensões personalizadas de escopo de evento para `service` e `placement`. Nenhum rótulo de conversão Ads foi inventado e nenhuma configuração da conta foi alterada. A medição otimiza intenção de contato, não vendas. - -Também revisar as opções de medição otimizada da propriedade, sobretudo eventos automáticos de cliques externos e formulários, pois essa configuração não é controlada por este código. Referência dos eventos: https://developers.google.com/analytics/devguides/collection/ga4/events. - -Verificação local: build de produção, navegação responsiva, redirecionamento com parâmetros e requisição real `whatsapp_open` gerada pela tag. Requisições de coleta foram interceptadas antes do envio para não contaminar os relatórios; isso **não comprova recebimento no GA4 nem atribuição no Ads**. - -## Estrutura +Todas as dependências diretas estão fixadas em versões exatas no manifesto; o +lockfile é instalado sem atualização implícita. ```text app/ App Router, rotas e componentes da interface lib/ Dados derivados, navegação, JSON-LD e project health -public/ Conteúdo JSON, PDF, fontes, imagens e modelo 3D +public/ Conteúdo JSON, PDF, fontes, imagens e capturas scripts/check-projects.mjs Atualização manual do snapshot de demos data/project-health.json @@ -190,16 +144,47 @@ data/project-health.json docs/project-health.md Contrato operacional do snapshot .github/workflows/ci.yml Gate único de integração contínua -nixpacks.toml Build e start para Nixpacks +nixpacks.toml Build e start para Nixpacks ``` -## CI e deploy +
-O workflow único em `.github/workflows/ci.yml` roda em cada `push` e `pull_request` com permissão `contents: read`. Ele instala Node 22 e pnpm conforme `packageManager`, restaura o cache do pnpm e executa, nesta ordem: frozen install, lint, typecheck, Jest serial, build e auditorias de produção e desenvolvimento com limite high. O passo de typecheck gera os tipos de rota antes do `tsc`: `next-env.d.ts` não é versionado e, em uma árvore recém-clonada, é ele que declara os módulos de imagem importados pelo código. +## Limitações conhecidas -O Nixpacks usa `nodejs_22`, `pnpm-9_x`, `pnpm install --frozen-lockfile`, `pnpm run build` e `pnpm run start`. Não há etapa de deploy nem secret de CI neste repositório. +- **Sem backend.** O formulário de contato não grava nada: abre `wa.me` com o + briefing preenchido. +- **Demos externas podem aparecer como indisponíveis.** O CTA de cada case reflete + o último snapshot versionado em `data/project-health.json`, não uma consulta em + tempo real (em produção o render não faz fetch externo). +- **Sem WebGL em telas pequenas, com ponteiro coarse ou movimento reduzido.** Nesses + casos o hero e o notebook entregam a versão HTML. -## Validação local +## Execução local + +> [!WARNING] +> Use **Node.js `22.x`** e **pnpm `9.15.9`** (fixado em `package.json#packageManager`). +> A instalação é sempre com `--frozen-lockfile`; o CI e o Nixpacks fazem o mesmo. + +```bash +corepack pnpm@9.15.9 install --frozen-lockfile +corepack pnpm@9.15.9 dev # Next.js com Turbopack +corepack pnpm@9.15.9 build # build de produção com Turbopack +corepack pnpm@9.15.9 start # serve um build já gerado +``` + +| Script | Comportamento | +| --- | --- | +| `pnpm dev` | Inicia o Next.js em desenvolvimento com Turbopack | +| `pnpm build` | Gera o build de produção com Turbopack | +| `pnpm start` | Serve um build já gerado | +| `pnpm lint` | Executa `biome check` | +| `pnpm typecheck` | Executa TypeScript sem emitir arquivos | +| `pnpm format` | Formata o repositório com Biome | +| `pnpm test` | Executa a suíte Jest | +| `pnpm test:watch` | Executa Jest em modo watch | +| `pnpm check:projects` | Verifica URLs de demonstração e substitui atomicamente `data/project-health.json` | + +Validação completa, na mesma ordem do CI: ```bash corepack pnpm@9.15.9 install --frozen-lockfile @@ -210,3 +195,21 @@ corepack pnpm@9.15.9 build corepack pnpm@9.15.9 audit --prod --audit-level=high corepack pnpm@9.15.9 audit --dev --audit-level=high ``` + +## CI/CD + +O workflow único [`.github/workflows/ci.yml`](.github/workflows/ci.yml) roda em +cada `push` e `pull_request`, com permissão `contents: read`. No job `verify` +(`ubuntu-latest`) ele instala Node 22 e pnpm conforme `packageManager`, restaura o +cache do pnpm e executa, nesta ordem: frozen install, lint, typecheck, Jest +serial, build e auditorias de produção e desenvolvimento com limite high. + +O Nixpacks ([`nixpacks.toml`](nixpacks.toml)) usa `nodejs_22`, `pnpm-9_x`, +`pnpm install --frozen-lockfile`, `pnpm run build` e `pnpm run start`. Não há +etapa de deploy nem secret de CI neste repositório. + +## Avisos + +- As capturas em `public/assets/prints/*.png` foram feitas com Chrome headless a + partir de deploys de produção do próprio autor (evandro.dev.br) e aparecem como + conteúdo da tela do notebook. Detalhes em [`CREDITS.md`](CREDITS.md). diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..a667db8 --- /dev/null +++ b/vercel.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs" +}