"use client"; import { ArrowUpRight, Mail, MapPin } from "lucide-react"; import type { FormEvent, MouseEvent, ReactNode } from "react"; import { useEffect, useState } from "react"; import { trackEmailClick, trackWhatsAppOpen } from "@/lib/analytics"; import { CONTACT_EMAIL } from "@/lib/site-navigation"; import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp"; import "./conversion.css"; import WhatsAppLink from "./WhatsAppLink"; import { WhatsAppMark } from "./WhatsAppMark"; type ContactField = "name" | "email" | "message"; type FieldErrors = Partial>; const FIELD_ERRORS: Record = { name: "Informe seu nome para eu saber com quem falo.", email: "Confira o e-mail: ele precisa ter o formato nome@empresa.com.br.", message: "Conte em poucas palavras o que você precisa resolver.", }; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const FIELD_ORDER: ContactField[] = ["name", "email", "message"]; /** E-mail é opcional: só é erro se vier preenchido e fora do formato. */ function validate(data: { name: string; email: string; message: string; }): FieldErrors { const errors: FieldErrors = {}; if (!data.name.trim()) errors.name = FIELD_ERRORS.name; const email = data.email.trim(); if (email && !EMAIL_PATTERN.test(email)) errors.email = FIELD_ERRORS.email; if (!data.message.trim()) errors.message = FIELD_ERRORS.message; return errors; } /** Mesmo texto para o WhatsApp e para o e-mail; a linha do e-mail só entra se a pessoa informou um. */ function buildProjectMessage(data: { name: string; email: string; subject: string; message: string; }) { const email = data.email.trim(); return `Olá, Evandro! Quero conversar sobre um projeto.\n\nNome: ${data.name.trim()}\n${email ? `E-mail: ${email}\n` : ""}Tipo de projeto: ${data.subject}\n\nContexto, objetivo e prazo:\n${data.message.trim()}\n\nContato iniciado pelo site evandro.dev.br.`; } function buildMailtoUrl(message: string) { return `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent("Projeto pelo site")}&body=${encodeURIComponent(message)}`; } const fieldClassName = "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)] aria-[invalid=true]:border-[#ff8a80]"; const labelClassName = "block font-display text-[15px] md:text-sm font-medium text-white/85"; const errorClassName = "mt-sp-2 font-display text-[15px] md:text-sm leading-6 text-[#ff9d94]"; /** "Ainda preciso definir" vem primeiro e é o padrão: quem chega sem saber o que pedir não é empurrado para um tipo. */ const SUBJECT_OPTIONS = [ "Ainda preciso definir", "Site para empresa", "Sistemas web sob medida", "Aplicativos", "Integrações e automações", "Evolução de um sistema existente", ]; /** Mensagem dos links diretos de quem está sem JavaScript (o formulário não funciona sem ele). */ const DIRECT_MESSAGE = "Olá, Evandro! Quero conversar sobre um projeto. Contato iniciado pelo site evandro.dev.br."; /** Abre a URL em outra aba, ou devolve null se não deu (sem `window.open`, bloqueado ou com erro). */ function tryOpenTab(url: string): Window | null { try { return window.open(url, "_blank"); } catch { return null; } } /** Corta o vínculo da aba aberta com esta página; devolve false se o navegador não deixou. */ function detachOpener(tab: Window): boolean { try { tab.opener = null; return true; } catch { return false; } } /** * Abre o WhatsApp em outra aba. Sem "noopener" na chamada, porque com ele o window.open sempre devolve null * e não dá para saber se abriu; o vínculo com a página (`opener`) é cortado logo em seguida. A página vai para o * WhatsApp na própria aba quando não há aba nova (pop-up bloqueado, navegador interno de aplicativo, sem * `window.open`) e também quando o vínculo não pôde ser cortado: nesse caso a aba aberta é fechada, para * nenhuma aba do WhatsApp ficar com acesso a esta página. */ function openWhatsApp(url: string) { const tab = tryOpenTab(url); if (tab) { if (detachOpener(tab)) return; tab.close(); } window.location.href = url; } /** Atributos que o campo entrega ao controle (input, textarea ou select) para o erro ser anunciado. */ interface ControlProps { id: string; "aria-invalid"?: true; "aria-describedby"?: string; } /** Rótulo, controle e mensagem de erro de um campo; o controle recebe o id e os atributos de acessibilidade. */ function Field({ id, label, error, describedBy, children, }: { id: string; label: string; error?: string; /** Outros textos que descrevem o campo, depois do erro. */ describedBy?: string; children: (control: ControlProps) => ReactNode; }) { const errorId = `${id}-error`; const description = [error ? errorId : "", describedBy ?? ""] .filter(Boolean) .join(" "); return (
{children({ id, "aria-invalid": error ? true : undefined, "aria-describedby": description || undefined, })} {error && (

{error}

)}
); } export default function Contact() { const [whatsAppDisplay, setWhatsAppDisplay] = useState("WhatsApp"); useEffect(() => { setWhatsAppDisplay(getWhatsAppDisplayNumber()); }, []); const [formData, setFormData] = useState({ name: "", email: "", subject: "Ainda preciso definir", message: "", }); const [errors, setErrors] = useState({}); const updateField = (field: ContactField | "subject", value: string) => { setFormData((current) => ({ ...current, [field]: value })); if (field !== "subject" && errors[field]) { setErrors((current) => ({ ...current, [field]: undefined })); } }; /** Mostra os erros, leva o foco ao primeiro campo inválido e diz se há algum. */ const hasInvalidField = (form: HTMLFormElement | null) => { const found = validate(formData); setErrors(found); const firstInvalid = FIELD_ORDER.find((field) => found[field]); if (firstInvalid) { form?.querySelector(`[name="${firstInvalid}"]`)?.focus(); } return firstInvalid !== undefined; }; const handleSubmit = (e: FormEvent) => { e.preventDefault(); if (hasInvalidField(e.currentTarget)) return; openWhatsApp(buildWhatsAppUrl(buildProjectMessage(formData))); trackWhatsAppOpen({ service: "Contato", placement: "contact_form" }); }; /** O rascunho por e-mail passa pela mesma validação do WhatsApp: sem ela o e-mail sairia vazio. */ const handleEmailClick = (e: MouseEvent) => { if (hasInvalidField(e.currentTarget.closest("form"))) { e.preventDefault(); return; } trackEmailClick({ placement: "contact_form" }); }; return (

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.

Você fala direto com quem desenvolve.

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

Prepare sua mensagem

Você revisa no WhatsApp antes de enviar.

{/* js-only: app/layout.tsx esconde o que depende de JavaScript quando ele está desligado, e o