feat(home): prova no topo, contato sem atrito e conversão medida

Home mais curta (hero, vitrine de clientes, sobre, serviços, IA resumida, depoimentos e contato), faixa de clientes e telas reais no hero, sem números do GitHub. Barra de cookies fina, WhatsApp fixo no celular, formulário com e-mail opcional e alternativa por e-mail, página /ia, eventos de e-mail, redes e currículo e conversão do Ads opcional por variável. Rodapé em 3 colunas. Remove componentes órfãos (LighthouseMetrics, Skills, Experience, contagem do GitHub).
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 03:32:23 -03:00
1 parent 6e1022b58c
commit 8d07b8c6bb
60 files changed
+4392 -2124

No files matched your search

+46 -54
View File
@@ -1,61 +1,42 @@
"use client";
import Link from "next/link";
import { type CSSProperties, Fragment } from "react";
import { trackEmailClick, trackSocialClick } from "@/lib/analytics";
import { CONTACT_EMAIL, LINKEDIN_URL } from "@/lib/site-navigation";
import ClientStrip, { type ClientLink } from "./ClientStrip";
import HeroField from "./cinematic/HeroField";
import { HERO_CTA_ID } from "./FloatingWhatsApp";
import HeroWork, { type HeroWorkUrl } from "./HeroWork";
const SOCIAL_LINKS = [
{
href: "https://github.com/evandrodevbr",
label: "GitHub",
},
{
href: "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
href: LINKEDIN_URL,
label: "LinkedIn",
onClick: () => trackSocialClick({ network: "linkedin" }),
},
{
href: "mailto:[email protected]",
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;
* repos vem da API do GitHub (lib/github-stats.ts) e some quando nem a consulta nem o último retrato estão disponíveis.
* Números reais, calculados em app/page.tsx (nada digitado à mão): os anos vêm de public/portfolio-data.json.
*/
export type HeroStats = {
years?: number;
repos?: { total: number; public: number; private: number };
};
type StatItem = {
value: string;
label: string;
prefix?: string;
detail?: string;
};
/** "Mais de N" só é verdade se N for estritamente menor que o total: arredonda para baixo, em múltiplos de 5. */
export function roundedRepoFloor(total: number) {
return Math.floor((total - 1) / 5) * 5;
}
function statItems(stats?: HeroStats): StatItem[] {
if (!stats) return [];
const { repos } = stats;
const repoFloor = repos ? roundedRepoFloor(repos.total) : 0;
const items: Array<StatItem | null> = [
stats.years
? { value: String(stats.years), label: "anos de experiência" }
: null,
repos && repoFloor > 0
? {
prefix: "Mais de",
value: String(repoFloor),
label: "projetos desenvolvidos",
detail: `${repos.public} públicos · ${repos.private} privados`,
}
: null,
];
return items.filter((item): item is StatItem => item !== null);
if (!stats?.years) return [];
return [{ value: String(stats.years), label: "anos de experiência" }];
}
/** Título em palavras para a entrada em cascata; o texto segue inteiro para leitores de tela e buscadores. */
@@ -75,7 +56,16 @@ const CAPTIONS = [
["Os dados", "Suas informações guardadas, organizadas e seguras."],
] as const;
export default function Hero({ stats }: { stats?: HeroStats }) {
export default function Hero({
stats,
clients,
workUrl,
}: {
stats?: HeroStats;
clients?: ClientLink[];
/** Endereço mostrado na barra do navegador de HeroWork (o do site da Lavação 047). */
workUrl?: HeroWorkUrl;
}) {
const items = statItems(stats);
return (
@@ -97,13 +87,15 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
))}
</ol>
<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)+32px)] pb-sp-8 lg:px-sp-12">
<p className="hero-in mb-sp-5 flex items-center gap-sp-3 font-mono text-[11px] uppercase leading-[1.5] tracking-[0.14em] text-[rgba(244,242,237,.72)] sm:text-[12px]">
{/* 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">
<p className="hero-in mb-sp-4 flex items-center gap-sp-3 font-mono 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. */}
<h1 className="hero-title max-w-[12.5em] lg:max-w-[min(11em,54vw)] text-[clamp(38px,min(6vw,10.5svh),124px)] font-semibold leading-[0.98] tracking-[-0.05em]">
{/* No desktop o título para antes da coluna HeroWork (largura da coluna: clamp(300px,24vw,400px)). */}
<h1 className="hero-title max-w-[12.5em] lg:max-w-[min(11em,54vw,calc(100%-clamp(300px,24vw,400px)-24px))] text-[clamp(38px,min(6vw,9svh),124px)] font-semibold leading-[0.98] tracking-[-0.05em]">
{TITLE.map((word, index) => (
<Fragment key={word}>
<span
@@ -115,13 +107,15 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
</Fragment>
))}
</h1>
<p className="hero-in mt-sp-5 max-w-[34em] text-[clamp(16px,0.95rem+0.4vw,22px)] leading-[1.55] text-[rgba(244,242,237,.8)] [--d:.55s]">
Projeto e desenvolvo sistemas web, aplicativos e integrações, do
entendimento do negócio à arquitetura e à entrega em produção.
<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(40em,calc(100vw-clamp(300px,24vw,400px)-9rem))]">
Para empresas que cresceram além das planilhas e dos sistemas
prontos: sites, sistemas web, aplicativos e integrações, com contato
direto com quem desenvolve.
</p>
<div className="hero-in mt-sp-6 flex flex-col gap-sp-3 [--d:.65s] sm:flex-row sm:flex-wrap sm:items-center">
<div className="hero-in mt-sp-5 flex flex-col gap-sp-3 [--d:.65s] sm:flex-row sm:flex-wrap sm:items-center">
<Link
id={HERO_CTA_ID}
href="/#contact"
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]"
>
@@ -139,33 +133,30 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
</Link>
<Link
href="/cases"
className="inline-flex min-h-12 items-center justify-center border border-white/15 bg-[rgba(10,10,10,.35)] px-sp-5 py-sp-3 text-center text-[15px] font-medium leading-6 text-[#f4f2ed] no-underline backdrop-blur-sm transition-colors hover:border-white/40 motion-reduce:transition-none sm:text-[16px]"
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
</Link>
</div>
<ClientStrip
clients={clients}
className="hero-in mt-sp-4 [--d:.75s] lg:max-w-[min(44rem,calc(100vw-clamp(300px,24vw,400px)-9rem))]"
/>
<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">
{items.length > 0 && (
<dl className="flex gap-x-sp-5 gap-y-sp-4 sm:flex-wrap sm:gap-x-sp-8">
{items.map(({ prefix, value, label, detail }) => (
{items.map(({ value, label }) => (
<div key={label} className="flex flex-col-reverse">
<dt className="text-[13px] leading-[1.4] text-[rgba(244,242,237,.64)]">
{label}
{detail && (
<span className="block text-[12px] text-[rgba(244,242,237,.55)]">
{detail}
</span>
)}
</dt>
<dd className="font-mono text-[clamp(22px,1.1rem+0.8vw,36px)] font-semibold leading-none tracking-[-0.03em] text-[var(--slash)]">
{prefix && (
<span className="mr-sp-2 text-[13px] font-normal tracking-normal text-[rgba(244,242,237,.64)]">
{prefix}
</span>
)}
{value}
</dd>
</div>
@@ -198,6 +189,7 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
? "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}
@@ -211,7 +203,7 @@ export default function Hero({ stats }: { stats?: HeroStats }) {
</div>
<p
aria-hidden="true"
className="hero-cue hidden items-center gap-sp-3 font-mono text-[11px] uppercase tracking-[0.14em] text-[rgba(244,242,237,.55)] lg:flex"
className="hero-cue hidden items-center gap-sp-3 font-mono text-[12px] uppercase tracking-[0.14em] text-[rgba(244,242,237,.55)] lg:flex"
>
Role para mergulhar
<span className="hero-cue-line" />