diff --git a/.gitignore b/.gitignore index a0d7728..faad045 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ next-env.d.ts .superpowers/ /.worktrees/ /.impeccable/ +/.serena/ # generated analysis artifacts /graphify-out/ diff --git a/CREDITS.md b/CREDITS.md index a72b3c3..91ddc85 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -4,3 +4,8 @@ - `public/assets/prints/*.png` are captures taken with headless Chrome from production deployments owned by the site author (evandro.dev.br), used as on-screen content inside the notebook browser surface. + +## WhatsApp icon +- The WhatsApp silhouette in `app/components/Contact.tsx` comes from + [Simple Icons](https://github.com/simple-icons/simple-icons/blob/develop/icons/whatsapp.svg). + It identifies the contact channel; it does not imply endorsement by WhatsApp. diff --git a/README.md b/README.md index a86d8de..0520413 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
-# Evandro.dev.br: portfólio +# Evandro.dev.br: software sob medida -Portfólio profissional de Evandro Fonseca Junior, com projetos, cases, serviços, currículo e contato por WhatsApp. +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.
@@ -11,7 +11,7 @@ Portfólio profissional de Evandro Fonseca Junior, com projetos, cases, serviço - Node.js `22.x` - pnpm `9.15.9`, fixado em `package.json#packageManager` -O projeto usa Next.js `16.3.0`. Essa versão é intencional: `16.2.11` ainda mantinha três advisories transitivos de severidade alta; `16.3.0` foi a menor versão estável da mesma major que deixou as auditorias atuais sem vulnerabilidades high. +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 @@ -45,10 +45,10 @@ corepack pnpm@9.15.9 start | Área | Pacotes usados | | --- | --- | -| Aplicação | Next.js 16.3.0, React 19.1.0, React DOM 19.1.0 e TypeScript 5.9.2. | +| 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, Drei e Cobe. | +| 3D e gráficos | Three.js, React Three Fiber e Drei. | | Interface | Lucide React. | | Qualidade | Biome, Jest, Testing Library e ambiente JSDOM. | @@ -58,7 +58,7 @@ Todas as dependências diretas estão fixadas em versões exatas no manifesto; o | Rota | Conteúdo | | --- | --- | -| `/` | Homepage com hero, notebook de projetos, perfil, competências, experiência, projetos, modalidades de trabalho e contato. | +| `/` | 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. | @@ -66,18 +66,19 @@ Todas as dependências diretas estão fixadas em versões exatas no manifesto; o | `/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` | 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`. | -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. +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. -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. +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é. ## Comportamento da aplicação @@ -87,12 +88,51 @@ O conteúdo principal vem de `public/portfolio-data.json` e `lib/site-content.ts 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). +### Posicionamento comercial e uso de IA + +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. + +`app/components/EngineeringWithAI.tsx` explica arquitetura, permissões, validação e recuperação de dados, com três relatos históricos de 2025 e links junto de cada afirmação: + +- 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 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. + +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:`. + +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. + ### 3D progressivo e movimento reduzido -O hero 3D é carregado no cliente a partir de `1024px`, sem `prefers-reduced-motion: reduce` e sem ponteiro coarse. O notebook 3D exige mais de `1024px`, hover disponível e as mesmas condições de movimento e ponteiro. Nos demais casos, o notebook entrega a navegação HTML completa sem WebGL ou movimento. No desktop, tampa, tela, abas e cursor seguem o mesmo progresso de scroll; a cena renderiza sob demanda e pausa fora do viewport ou com a página oculta. +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. + +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. + +### 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 | +| --- | --- | +| `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. | + +`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. @@ -101,6 +141,22 @@ O formulário da homepage valida os campos nativos de nome, e-mail, tipo e conte 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 ```text diff --git a/app/cases/[slug]/page.test.tsx b/app/cases/[slug]/page.test.tsx index b683ae0..02f0285 100644 --- a/app/cases/[slug]/page.test.tsx +++ b/app/cases/[slug]/page.test.tsx @@ -58,16 +58,12 @@ function readJsonLdEntities(container: HTMLElement): Record[] { } function expectCaseCtas() { - expect( - screen.getByRole("link", { - name: "Avaliar perfil para contratação", - }), - ).toHaveAttribute("href", "/curriculo"); - expect( - screen.getByRole("link", { - name: "Solicitar consultoria técnica", - }), - ).toHaveAttribute("href", "/#contact"); + const destinations = screen + .getAllByRole("link") + .map((link) => link.getAttribute("href")); + expect(destinations).toContain("/#contact"); + expect(destinations).toContain("/servicos"); + expect(destinations).not.toContain("/curriculo"); } it("keeps the six public params and factual case metadata", async () => { diff --git a/app/cases/[slug]/page.tsx b/app/cases/[slug]/page.tsx index 409310a..5c6c558 100644 --- a/app/cases/[slug]/page.tsx +++ b/app/cases/[slug]/page.tsx @@ -236,9 +236,9 @@ export default async function CasePage({ params }: CasePageProps) {
- {intentCtas.hiring.label} + {intentCtas.project.label} - {intentCtas.hiring.label} + {intentCtas.project.label} - Garuva, SC | Remote worldwide + Base em Garuva, SC | Atendimento remoto
} > @@ -140,14 +140,14 @@ export default function About() { className="mb-sp-6 aspect-square w-full overflow-hidden rounded-[6px] border border-[var(--border)]" imageClassName="object-contain" /> -
Technical profile
+
Da arquitetura à implementação
-

Base técnica e postura de entrega

+

Uma visão do sistema inteiro

- Sou engenheiro da computação e desenvolvedor de software full-stack, - atuando ha 5 anos com foco em integrar camadas que normalmente ficam - separadas: hardware, servicos, operacao e interface. Meu trabalho e - dar forma clara ao que costuma nascer fragmentado. + Meu trabalho conecta interface, serviços, dados e, quando + necessário, equipamentos. Entender essas partes em conjunto ajuda a + definir o que construir, o que integrar e o que pode ser + aproveitado.

{PROFILE_POINTS.map((point) => ( diff --git a/app/components/Contact.test.tsx b/app/components/Contact.test.tsx new file mode 100644 index 0000000..bfdbfb1 --- /dev/null +++ b/app/components/Contact.test.tsx @@ -0,0 +1,105 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import Contact from "./Contact"; + +beforeEach(() => { + jest.replaceProperty(process.env, "NODE_ENV", "production"); + localStorage.setItem("evandro.analytics-consent.v1", "granted"); + delete window["ga-disable-G-QTT9QT0T3S"]; + window.gtag = jest.fn(); + jest.spyOn(window, "open").mockImplementation(() => null); +}); + +afterEach(() => { + jest.restoreAllMocks(); + localStorage.clear(); + delete window.gtag; +}); + +it("prepares an isolated WhatsApp draft with the entered project details only after submission", async () => { + const user = userEvent.setup(); + render(); + + const name = "Ana & João"; + const email = "ana+projeto@example.com"; + const context = + "Integrar pedidos & estoque? Objetivo: evitar retrabalho.\nPrazo: outubro #2026"; + await user.type(screen.getByRole("textbox", { name: /nome/i }), name); + await user.type(screen.getByRole("textbox", { name: /e-mail/i }), email); + const subject = screen.getByRole("combobox", { name: /tipo de projeto/i }); + await user.selectOptions(subject, "Integrações e automações"); + await user.type(screen.getByRole("textbox", { name: /contexto/i }), context); + + expect(window.open).not.toHaveBeenCalled(); + expect(window.gtag).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: /whatsapp/i })); + + expect(window.open).toHaveBeenCalledTimes(1); + const [destination, target, features] = jest.mocked(window.open).mock + .calls[0]; + const url = new URL(String(destination)); + expect(url.origin).toBe("https://wa.me"); + expect(url.pathname).toMatch(/^\/\d+$/); + expect([...url.searchParams.keys()]).toEqual(["text"]); + const message = url.searchParams.get("text"); + expect(message).toContain(name); + expect(message).toContain(email); + expect(message).toContain("Integrações e automações"); + expect(message).toContain(context); + expect(target).toBe("_blank"); + expect(features).toBe("noopener,noreferrer"); + expect(window.gtag).toHaveBeenCalledTimes(1); + expect(window.gtag).toHaveBeenCalledWith("event", "whatsapp_open", { + send_to: "G-QTT9QT0T3S", + service: "Contato", + placement: "contact_form", + page_path: "/", + page_location: "https://evandro.dev.br/", + page_referrer: "", + }); +}); + +it("does not open WhatsApp while required fields are empty or the email is invalid", async () => { + const user = userEvent.setup(); + render(); + + const submit = screen.getByRole("button", { name: /whatsapp/i }); + await user.click(submit); + expect(window.open).not.toHaveBeenCalled(); + + await user.type(screen.getByRole("textbox", { name: /nome/i }), "Ana"); + await user.type( + screen.getByRole("textbox", { name: /e-mail/i }), + "email-invalido", + ); + await user.type( + screen.getByRole("textbox", { name: /contexto/i }), + "Organizar pedidos", + ); + await user.click(submit); + expect(window.open).not.toHaveBeenCalled(); + expect(window.gtag).not.toHaveBeenCalled(); +}); + +it("opens the project draft with analytics unavailable and consent denied", async () => { + localStorage.setItem("evandro.analytics-consent.v1", "denied"); + delete window.gtag; + const user = userEvent.setup(); + render(); + await user.type(screen.getByRole("textbox", { name: /nome/i }), "Ana"); + await user.type( + screen.getByRole("textbox", { name: /e-mail/i }), + "ana@example.com", + ); + await user.type( + screen.getByRole("textbox", { name: /contexto/i }), + "Organizar pedidos", + ); + await user.click(screen.getByRole("button", { name: /whatsapp/i })); + expect(window.open).toHaveBeenCalledTimes(1); + const destination = new URL( + String(jest.mocked(window.open).mock.calls[0][0]), + ); + expect(destination.origin).toBe("https://wa.me"); + expect(destination.searchParams.get("text")).toContain("Organizar pedidos"); +}); diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 2d34959..3559cae 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -1,231 +1,252 @@ "use client"; -import { motion, type Variants } from "motion/react"; -import type React from "react"; +import { ArrowUpRight, Mail, MapPin } from "lucide-react"; +import type { FormEvent } from "react"; import { useEffect, useState } from "react"; -import { useReducedMotion } from "@/app/hooks/useReducedMotion"; +import { trackWhatsAppOpen } from "@/lib/analytics"; import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp"; -import { DrawLine } from "./animations/DrawLine"; -import MagneticButton from "./cinematic/MagneticButton"; -import { Section } from "./Section"; +import WhatsAppLink from "./WhatsAppLink"; + +// WhatsApp brand silhouette from Simple Icons (simple-icons/simple-icons). +function WhatsAppMark({ className }: { className: string }) { + return ( + + ); +} export default function Contact() { - const shouldReduceMotion = useReducedMotion(); const [whatsAppDisplay, setWhatsAppDisplay] = useState("WhatsApp"); - useEffect(() => { setWhatsAppDisplay(getWhatsAppDisplayNumber()); }, []); const fieldClassName = - "mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[13px] leading-6 text-white outline-none transition-[border-color,box-shadow] focus-visible:border-[var(--slash)] focus-visible:ring-2 focus-visible:ring-[rgba(204,255,0,0.34)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"; + "mt-sp-2 w-full min-w-0 rounded-lg border border-white/15 bg-black/25 px-sp-4 py-sp-3 font-display text-base leading-6 text-white placeholder:text-white/40 focus-visible:border-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"; + const labelClassName = "block font-display text-sm font-medium text-white/85"; const [formData, setFormData] = useState({ name: "", email: "", - subject: "full-stack", + subject: "Sistemas web sob medida", message: "", }); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault(); - - const messageTemplate = `*NOVA SOLICITACAO DE PROJETO*\n\n*Origem:* Portfolio Profissional\n*Prioridade:* Alta\n\n---\n*CLIENTE:* ${formData.name}\n*EMAIL:* ${formData.email}\n*AREA:* ${formData.subject.toUpperCase()}\n\n*DESCRICAO:*\n"${formData.message}"\n---\n_Mensagem gerada via Evandro.dev | Garuva, SC_`; - - const whatsappUrl = buildWhatsAppUrl(messageTemplate); - - window.open(whatsappUrl, "_blank", "noopener,noreferrer"); - }; - - const gridVariants: Variants = { - hidden: {}, - visible: { - transition: { - delayChildren: 0.05, - staggerChildren: 0.08, - }, - }, - }; - - const cardVariants: Variants = { - hidden: shouldReduceMotion - ? { opacity: 0 } - : { opacity: 0, y: 18, clipPath: "inset(100% 0 0 0)" }, - visible: { - opacity: 1, - y: 0, - clipPath: "inset(0% 0 0 0)", - transition: shouldReduceMotion - ? { duration: 0.25 } - : { duration: 0.75, ease: [0.16, 1, 0.3, 1] }, - }, + if (!e.currentTarget.reportValidity()) return; + const messageTemplate = `Olá, Evandro! Quero conversar sobre um projeto.\n\nNome: ${formData.name}\nE-mail: ${formData.email}\nTipo de projeto: ${formData.subject}\n\nContexto, objetivo e prazo:\n${formData.message}\n\nContato iniciado pelo site evandro.dev.br.`; + window.open( + buildWhatsAppUrl(messageTemplate), + "_blank", + "noopener,noreferrer", + ); + trackWhatsAppOpen({ service: "Contato", placement: "contact_form" }); }; return ( -
- - -
- Availability -
- -

- Resposta inicial em ate 24h -

-

- Ideal para discovery tecnico, revisao de arquitetura, rescue de - projeto, performance ou construcao de produto com ownership real. +

+
+

Contato

+

+ Vamos conversar sobre o que sua empresa precisa. +

+

+ Conte o que você quer resolver. Pode ser um projeto novo, uma + melhoria ou uma integração. Não precisa chegar com a solução + definida.

-
-
WhatsApp: {whatsAppDisplay}
-
E-mail: evandrofonsecajunior@gmail.com
-
Base: Garuva, Santa Catarina | atuacao remota
+
+

+ Você fala direto com quem desenvolve. +

+

+ Sou Evandro. A primeira conversa é para entender sua necessidade; + escopo, prazo e proposta vêm depois. +

-
- Envie contexto, prazo e objetivo. Eu cuido de transformar isso em - conversa tecnica objetiva. +
+ + + + + + + Prefere conversar direto? + + + WhatsApp · {whatsAppDisplay} + + + + +
- +

+

+
- -
- - - setFormData({ ...formData, name: e.target.value }) - } - className={fieldClassName} - /> +
+ + + +
+

+ Prepare sua mensagem +

+

+ Você revisa no WhatsApp antes de enviar. +

+
-
-