Files
portifolio-evandrodevbr/app/components/Hero.tsx
T
Evandro Fonseca Junior 9e2d65a926 fix(home): oferta leva ao formulário, ofertas logo depois da vitrine
O botão das ofertas rola até o contato com o tipo marcado e não quebra mais as âncoras; ofertas com o que você recebe e primeiro passo, logo depois da vitrine; bloco de IA fora da home; cartão largo da SEV no hero do celular; separador da faixa de clientes só entre itens; favicon de 13 para 1,8 KB; pré-carregamento só no hover e nunca de /#... fora da home; X do menu e cabeçalho opaco; WhatsApp com "o case"; /ia com 3 passos e modelo de relatório ilustrativo; Sobre alinhado ao currículo; CSP Report-Only com os domínios do Ads.
2026-10-14 19:14:47 -03:00

198 lines
8.7 KiB
TypeScript

"use client";
import { type CSSProperties, Fragment } from "react";
import { trackEmailClick, trackSocialClick } from "@/lib/analytics";
import { CONTACT_EMAIL, LINKEDIN_URL } from "@/lib/site-navigation";
import { HOME_SUBJECT } from "@/lib/whatsapp";
import ClientStrip, { type ClientLink } from "./ClientStrip";
import HeroField from "./cinematic/HeroField";
import { HERO_CTA_ID } from "./FloatingWhatsApp";
import HeroWork, { type HeroWorkUrl } from "./HeroWork";
import HoverPrefetchLink from "./HoverPrefetchLink";
import { HERO_CAPTIONS } from "./hero-captions";
import WhatsAppLink from "./WhatsAppLink";
const SOCIAL_LINKS = [
{
href: LINKEDIN_URL,
label: "LinkedIn",
onClick: () => trackSocialClick({ network: "linkedin" }),
},
{
href: `mailto:${CONTACT_EMAIL}`,
label: "Email",
onClick: () => trackEmailClick({ placement: "hero" }),
},
];
/**
* Números reais, calculados em app/page.tsx (nada digitado à mão): os anos vêm de public/portfolio-data.json
* e aparecem na mesma linha de "Já entregue para" (ClientStrip).
*/
export type HeroStats = {
years?: number;
};
/** O destaque em limão fica num trecho curto: "pedidos pelo WhatsApp" (palavras 5 a 7). */
const ACCENT = new Set([5, 6, 7]);
/** Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. */
const TITLE =
"Sites e sistemas que trazem pedidos pelo WhatsApp e tiram sua operação da planilha."
.split(" ")
.map((word, index) => ({
word,
// a mesma palavra se repete ("e"), então a posição entra na chave
key: `${word}-${index}`,
accent: ACCENT.has(index),
}));
export default function Hero({
stats,
clients,
workUrl,
}: {
stats?: HeroStats;
clients?: ClientLink[];
/** Endereço mostrado na barra do navegador de HeroWork (o domínio próprio do site da SEV Moda Íntima). */
workUrl?: HeroWorkUrl;
}) {
return (
<section
id="home"
className="hero relative w-full bg-[var(--ink)] font-display text-[#f4f2ed]"
>
<div className="hero-stage">
<div className="hero-glow" aria-hidden="true" />
<HeroField />
<div className="hero-scrim" aria-hidden="true" />
{/* Legendas do mergulho: aparecem enquanto a câmera atravessa cada camada (só no hero fixo, telas grandes). */}
<ol className="hero-captions">
{HERO_CAPTIONS.map(({ title, text, from, to }, index) => (
<li
key={title}
style={
{ "--i": index, "--from": from, "--to": to } as CSSProperties
}
>
<span className="hero-caption-title">{title}</span>
{text}
</li>
))}
</ol>
{/* Os espaços verticais acompanham a altura da tela (svh): subtítulo, botões e faixa de clientes precisam caber no palco fixo. */}
<div className="hero-content relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-1 flex-col justify-center px-sp-6 pt-[calc(var(--site-header-height)+clamp(8px,2.5svh,32px))] pb-[clamp(16px,3svh,32px)] lg:px-sp-12">
{/* Space Grotesk (a fonte do título, já carregada), e não a Geist Mono: assim nada de fonte nova é baixado antes do LCP. */}
<p className="hero-in mb-sp-4 flex items-center gap-sp-3 font-display text-[12px] uppercase leading-[1.5] tracking-[0.14em] text-[rgba(244,242,237,.72)]">
<span aria-hidden="true" className="h-2 w-2 bg-[var(--slash)]" />
Evandro Fonseca Junior · Engenharia de software
</p>
{/* Sem opacity:0: as palavras pintam no primeiro quadro (LCP); a entrada anima só posição e foco. */}
{/* No desktop o título ocupa até 54% da largura; a coluna HeroWork fica com 40%, à direita. */}
<h1 className="hero-title max-w-[16em] text-[clamp(34px,min(8.4vw,5.2svh+14px),84px)] font-semibold leading-[1.02] tracking-[-0.045em] lg:max-w-[54%]">
{TITLE.map(({ word, key, accent }, index) => (
<Fragment key={key}>
<span
className={`hero-word${accent ? " text-[var(--slash)]" : ""}`}
style={{ "--i": index } as CSSProperties}
>
{word}
</span>{" "}
</Fragment>
))}
</h1>
<p className="hero-in mt-sp-4 max-w-[34em] text-[clamp(16px,0.95rem+0.4vw,22px)] leading-[1.55] text-[rgba(244,242,237,.8)] [--d:.55s] lg:max-w-[min(36em,54%)]">
Do catálogo com pedido no WhatsApp ao painel da sua equipe. Você
fala com quem desenvolve.
</p>
<div className="hero-in mt-sp-5 flex flex-col gap-sp-3 [--d:.65s] sm:flex-row sm:flex-wrap sm:items-center">
{/* Abre o WhatsApp direto, com o assunto da home: o formulário continua na seção de contato, ao fim da página. */}
<WhatsAppLink
id={HERO_CTA_ID}
service={HOME_SUBJECT}
placement="hero"
className="group inline-flex min-h-12 items-center justify-center gap-sp-3 bg-[var(--slash)] px-sp-4 py-sp-3 text-center text-[15px] font-semibold leading-6 text-[var(--ink)] no-underline transition-colors hover:bg-[#d9ff40] motion-reduce:transition-none sm:px-sp-6 sm:text-[16px]"
>
Falar sobre meu projeto
<svg
aria-hidden="true"
viewBox="0 0 16 16"
className="h-4 w-4 transition-transform group-hover:translate-x-1 motion-reduce:transition-none"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M2 8h11M9 3.5 13.5 8 9 12.5" />
</svg>
</WhatsAppLink>
<HoverPrefetchLink
href="/cases"
className="inline-flex min-h-12 items-center justify-center border border-white/25 bg-transparent px-sp-5 py-sp-3 text-center text-[15px] font-medium leading-6 text-[#f4f2ed] no-underline transition-colors hover:border-white/55 motion-reduce:transition-none sm:text-[16px]"
>
Ver projetos entregues
</HoverPrefetchLink>
</div>
<ClientStrip
clients={clients}
years={stats?.years}
className="hero-in mt-sp-3 [--d:.75s]"
/>
<HeroWork url={workUrl} />
</div>
<div className="hero-foot relative mx-auto flex w-full max-w-[max(1400px,var(--shell))] flex-wrap items-end justify-between gap-sp-5 px-sp-6 pb-sp-5 lg:px-sp-12">
<div className="flex flex-wrap items-end gap-x-sp-8 gap-y-sp-4 border-t border-white/10 pt-sp-4">
{/* no celular, serviços e redes ficam no menu e no rodapé: assim o hero cabe numa tela e pode ficar fixo */}
<div className="max-lg:hidden">
<ul className="flex flex-wrap gap-x-sp-5 gap-y-sp-2 text-[14px] leading-[1.5] text-[rgba(244,242,237,.8)]">
{["Sites", "Sistemas web", "Aplicativos", "Integrações"].map(
(service) => (
<li key={service} className="flex items-center gap-sp-2">
<span
aria-hidden="true"
className="h-1 w-1 bg-[var(--slash)]"
/>
{service}
</li>
),
)}
</ul>
<div className="flex flex-wrap items-center gap-x-sp-5">
{SOCIAL_LINKS.map((link) => (
<a
key={link.label}
href={link.href}
target={link.href.startsWith("http") ? "_blank" : undefined}
rel={
link.href.startsWith("http")
? "noopener noreferrer"
: undefined
}
onClick={link.onClick}
className="inline-flex min-h-11 items-center text-[13px] text-[rgba(244,242,237,.64)] no-underline underline-offset-4 hover:text-[#f4f2ed] hover:underline"
>
{link.label}
</a>
))}
<span className="text-[13px] text-[rgba(244,242,237,.55)]">
Santa Catarina · Atendimento remoto
</span>
</div>
</div>
</div>
<p
aria-hidden="true"
className="hero-cue hidden items-center gap-sp-3 font-display text-[12px] uppercase tracking-[0.14em] text-[rgba(244,242,237,.55)] lg:flex"
>
Role para mergulhar
<span className="hero-cue-line" />
</p>
</div>
</div>
</section>
);
}