feat(home): ofertas que vendem o título, WhatsApp no hero e menu do celular acessível

Três ofertas (site que traz pedidos pelo WhatsApp, sistema para tirar a operação da planilha, revisão de sistema existente) com miniatura e formulário com o tipo escolhido; botão principal do hero abre o WhatsApp; moldura da SEV no hero do celular; menu fecha ao navegar, Esc, foco preso incluindo a barra de cookies e espaço para ela; CinematicHome como componente de servidor com ilha cliente; /ia com H1 de oferta e cartão do que você recebe; botão flutuante em aside com transform; AVIF antes de WebP; copy sem repetir contato direto.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 17:01:36 -03:00
1 parent de6af74a0c
commit f8545cd442
44 files changed
+2286 -571

No files matched your search

+34 -23
View File
@@ -6,6 +6,13 @@ 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 {
CONTACT_KIND_EVENT,
CONTACT_KIND_PARAM,
CONTACT_SUBJECT_OPTIONS,
contactSubjectForKind,
DEFAULT_CONTACT_SUBJECT,
} from "./contact-subject";
import "./conversion.css";
import WhatsAppLink from "./WhatsAppLink";
import { WhatsAppMark } from "./WhatsAppMark";
@@ -54,19 +61,9 @@ function buildMailtoUrl(message: string) {
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";
"block font-display text-[15px] 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",
];
"mt-sp-2 font-display text-[15px] leading-6 text-[#ff9d94]";
/** Mensagem dos links diretos de quem está sem JavaScript (o formulário não funciona sem ele). */
const DIRECT_MESSAGE =
@@ -161,11 +158,25 @@ export default function Contact() {
const [formData, setFormData] = useState({
name: "",
email: "",
subject: "Ainda preciso definir",
subject: DEFAULT_CONTACT_SUBJECT as string,
message: "",
});
const [errors, setErrors] = useState<FieldErrors>({});
// O tipo de projeto pode vir da oferta escolhida na home: pelo endereço (`/?tipo=site#contact`, ao chegar de outra
// página) ou pelo evento que o botão da oferta dispara na própria página. Tipo desconhecido não muda nada.
useEffect(() => {
const choose = (kind: string | null | undefined) => {
const subject = contactSubjectForKind(kind);
if (subject) setFormData((current) => ({ ...current, subject }));
};
choose(new URLSearchParams(window.location.search).get(CONTACT_KIND_PARAM));
const onChoose = (event: Event) =>
choose((event as CustomEvent<string | undefined>).detail);
window.addEventListener(CONTACT_KIND_EVENT, onChoose);
return () => window.removeEventListener(CONTACT_KIND_EVENT, onChoose);
}, []);
const updateField = (field: ContactField | "subject", value: string) => {
setFormData((current) => ({ ...current, [field]: value }));
if (field !== "subject" && errors[field]) {
@@ -222,9 +233,9 @@ export default function Contact() {
</p>
<div className="mt-sp-8 border-l-2 border-[var(--slash)] pl-sp-5">
<h3 className="font-display text-lg font-semibold text-white">
Você fala direto com quem desenvolve.
Como a conversa começa.
</h3>
<p className="mt-sp-2 max-w-[44ch] font-display text-[15px] md:text-sm leading-7 text-white/65">
<p className="mt-sp-2 max-w-[44ch] font-display text-[15px] leading-7 text-white/65">
Sou Evandro. A primeira conversa é para entender sua necessidade;
escopo, prazo e proposta vêm depois.
</p>
@@ -238,10 +249,10 @@ export default function Contact() {
<WhatsAppMark className="h-6 w-6" />
</span>
<span className="min-w-0 flex-1 font-display">
<span className="block text-[15px] md:text-sm font-semibold text-white group-hover:text-[#25d366]">
<span className="block text-[15px] font-semibold text-white group-hover:text-[#25d366]">
Prefere conversar direto?
</span>
<span className="mt-1 block text-[15px] md:text-sm text-white/65">
<span className="mt-1 block text-[15px] text-white/65">
WhatsApp · {whatsAppDisplay}
</span>
</span>
@@ -256,7 +267,7 @@ export default function Contact() {
className="group flex min-h-16 items-center gap-sp-4 rounded-sm py-sp-5 text-white/65 no-underline hover:text-white focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--slash)]"
>
<Mail className="h-5 w-5 shrink-0" aria-hidden="true" />
<span className="min-w-0 break-all font-display text-[15px] md:text-sm">
<span className="min-w-0 break-all font-display text-[15px]">
{CONTACT_EMAIL}
</span>
<ArrowUpRight
@@ -265,7 +276,7 @@ export default function Contact() {
/>
</a>
</div>
<p className="mt-sp-5 flex items-start gap-sp-3 font-display text-[15px] md:text-sm leading-6 text-white/55">
<p className="mt-sp-5 flex items-start gap-sp-3 font-display text-[15px] leading-6 text-white/55">
<MapPin className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
Garuva, Santa Catarina. Atendimento remoto em todo o Brasil.
</p>
@@ -288,7 +299,7 @@ export default function Contact() {
>
Prepare sua mensagem
</h3>
<p className="mt-1 font-display text-[15px] md:text-sm leading-6 text-white/65">
<p className="mt-1 font-display text-[15px] leading-6 text-white/65">
Você revisa no WhatsApp antes de enviar.
</p>
</div>
@@ -339,7 +350,7 @@ export default function Contact() {
onChange={(e) => updateField("subject", e.target.value)}
className={`${fieldClassName} [color-scheme:dark]`}
>
{SUBJECT_OPTIONS.map((option) => (
{CONTACT_SUBJECT_OPTIONS.map((option) => (
<option key={option} value={option}>
{option}
</option>
@@ -386,7 +397,7 @@ export default function Contact() {
</p>
<p
id="contact-privacy-note"
className="font-display text-[15px] md:text-xs leading-6 text-white/55"
className="font-display text-[15px] leading-6 text-white/55"
>
Nada é enviado automaticamente. O WhatsApp abre com a mensagem
para você revisar. Não inclua senhas nem dados pessoais de
@@ -396,7 +407,7 @@ export default function Contact() {
{/* Sem JavaScript o formulário não envia nada: no lugar do botão, links diretos que funcionam sem script. */}
<noscript>
<p className="font-display text-base leading-7 text-white/80">
Para enviar sua mensagem, fale direto comigo:
Para enviar sua mensagem, use um destes contatos:
</p>
<p className="mt-sp-3 flex flex-col gap-sp-3 font-display text-base">
<a