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

+57 -5
View File
@@ -10,17 +10,24 @@ it("uses a short search title: with the layout suffix it fits in 44 characters",
description: expect.any(String),
alternates: { canonical: "https://evandro.dev.br/ia" },
});
// as prévias de compartilhamento não passam pelo template: o título delas já leva o sufixo
expect(metadata.openGraph?.title).toBe(
`Revisão de app feito com IA${suffix}`,
);
// como nas outras páginas, o título da prévia de compartilhamento é o mesmo da busca, sem o sufixo
expect(metadata.openGraph?.title).toBe("Revisão de app feito com IA");
expect(metadata.twitter?.title).toBe("Revisão de app feito com IA");
});
it("uses no lime outside the contact actions (the quote on the paper background is ink)", () => {
const { container } = render(<AiPage />);
expect(
container.querySelector('[class*="border-[var(--slash)]"]'),
).toBeNull();
});
it("keeps the full AI content, with the cited incidents summarized only here", () => {
render(<AiPage />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"Revisão de app feito com IA (Lovable, Bolt, Replit)",
"Seu app feito com IA está pronto para receber clientes? Eu testo antes deles.",
);
for (const heading of [
"Se a IA cria um sistema, por que contratar um desenvolvedor?",
@@ -91,6 +98,51 @@ it("presents the review as an offer: what the person receives, with no price or
expect(container.textContent).not.toMatch(/R\$|\d+ dias|em até \d+/);
});
it("shows the report list in a card on the right half of the hero, next to the headline", () => {
render(<AiPage />);
const card = screen.getByTestId("ia-review-card");
expect(within(card).getByText("O que você recebe")).toBeInTheDocument();
expect(
within(card)
.getAllByRole("listitem")
.map((item) => item.textContent?.replace(/^\d+/, "")),
).toEqual([
"As falhas de acesso a dados",
"O que dá para aproveitar",
"O que é melhor refazer",
"Um orçamento para pôr em produção",
]);
// o cartão divide a grade do hero com o título (metade direita no desktop) e não cria uma região de página
const hero = screen.getByRole("heading", { level: 1 }).closest("section");
expect(hero).toContainElement(card);
expect(card.parentElement).toHaveClass(
"lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]",
);
expect(screen.queryByRole("complementary")).toBeNull();
// o título do cartão não repete o h2 "O que você recebe na revisão" da seção seguinte
expect(
screen.getAllByRole("heading", { name: /O que você recebe/ }),
).toHaveLength(1);
});
it("keeps the secondary hero button inside the page: the portfolio cases are not reviews of AI-built apps", () => {
render(<AiPage />);
const hero = screen.getByRole("heading", { level: 1 }).closest("section");
const secondary = within(hero as HTMLElement).getByRole("link", {
name: "Ver como a revisão funciona",
});
expect(secondary).toHaveAttribute("href", "#ia-receber");
expect(document.querySelector(`section[id="ia-receber"]`)).not.toBeNull();
for (const link of within(hero as HTMLElement).getAllByRole("link")) {
expect(link.getAttribute("href")).not.toMatch(/^\/cases/);
}
expect(
screen.queryByRole("link", { name: "Ver projetos entregues" }),
).toBeNull();
});
it("keeps the incident news collapsed in a <details>, closed by default", () => {
const { container } = render(<AiPage />);
+64 -27
View File
@@ -1,5 +1,4 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Breadcrumb } from "@/app/components/Breadcrumb";
import {
pageTop,
@@ -25,8 +24,7 @@ export const metadata: Metadata = {
description: pageDescription,
...buildPageSocialMetadata({
url: pageUrl,
// as prévias de compartilhamento não passam pelo template do título
title: `${pageTitle} | Evandro.dev.br`,
title: pageTitle,
description: pageDescription,
type: "website",
}),
@@ -140,32 +138,71 @@ export default function AiPage() {
{ label: "Revisão de app feito com IA", href: "/ia" },
]}
/>
<p className="ds-eyebrow">IA no desenvolvimento</p>
<h1 className="ds-section-title max-w-[22ch] text-balance">
Revisão de app feito com IA (Lovable, Bolt, Replit)
</h1>
<p className="ds-copy max-w-[62ch]">
A IA permite testar ideias e construir protótipos mesmo sem
experiência em programação. Quando o sistema passa a cuidar de
clientes, dinheiro ou dados pessoais, alguém precisa avaliar o que
foi gerado e validar como ele se comporta fora da demonstração.
</p>
<div className="mt-sp-6 flex flex-wrap items-center gap-x-sp-6 gap-y-sp-3">
<WhatsAppLink
className={primaryButton}
service={WHATSAPP_SERVICE}
placement="ia_hero"
<div className="grid items-center gap-sp-8 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] lg:gap-sp-12">
<div className="min-w-0">
<p className="ds-eyebrow">IA no desenvolvimento</p>
<h1 className="ds-section-title max-w-[24ch] text-balance">
Seu app feito com IA está pronto para receber clientes? Eu testo
antes deles.
</h1>
<p className="ds-copy max-w-[62ch]">
A IA permite testar ideias e construir protótipos mesmo sem
experiência em programação. Quando o sistema passa a cuidar de
clientes, dinheiro ou dados pessoais, alguém precisa avaliar o
que foi gerado e validar como ele se comporta fora da
demonstração. Revisão para apps criados com Lovable, Bolt,
Replit e ferramentas parecidas.
</p>
<div className="mt-sp-6 flex flex-wrap items-center gap-x-sp-6 gap-y-sp-3">
<WhatsAppLink
className={primaryButton}
service={WHATSAPP_SERVICE}
placement="ia_hero"
>
Pedir a revisão do meu app
</WhatsAppLink>
{/* Segue na própria página: os cases do portfólio não são revisões de app feito com IA. */}
<a href="#ia-receber" className={textLinkDark}>
Ver como a revisão funciona
</a>
</div>
</div>
<div
data-testid="ia-review-card"
className="min-w-0 border border-white/15 bg-white/[0.04] p-sp-6 sm:p-sp-8"
>
Pedir a revisão do meu app
</WhatsAppLink>
<Link href="/cases" className={textLinkDark}>
Ver projetos entregues
</Link>
<p className="font-display text-xl font-semibold leading-7 text-white">
O que você recebe
</p>
<ol className="mt-sp-5 flex flex-col">
{DELIVERABLES.map((item, index) => (
<li
key={item.title}
className="flex items-start gap-sp-4 border-t border-white/12 py-sp-4 first:border-t-0 first:pt-0 last:pb-0"
>
<span
aria-hidden="true"
className="w-7 shrink-0 font-mono text-[15px] leading-6 text-white/60"
>
{String(index + 1).padStart(2, "0")}
</span>
<span className="min-w-0 font-display text-base leading-6 text-white/90">
{item.title}
</span>
</li>
))}
</ol>
</div>
</div>
</div>
</section>
<section className="ds-section" aria-labelledby="ia-receber-title">
<section
id="ia-receber"
className="ds-section"
aria-labelledby="ia-receber-title"
>
<div className="ds-section-shell">
<h2 id="ia-receber-title" className="ds-section-title max-w-[26ch]">
O que você recebe na revisão
@@ -198,7 +235,7 @@ export default function AiPage() {
<h2 id="ia-contratar-title" className="ds-section-title max-w-[26ch]">
Se a IA cria um sistema, por que contratar um desenvolvedor?
</h2>
<div className="mt-sp-6 border-l-2 border-[var(--slash)] pl-sp-5">
<div className="mt-sp-6 border-l-2 border-[var(--ink)] pl-sp-5">
<p className="max-w-[75ch] text-lg leading-8 text-[var(--ink)]">
Você contrata engenharia para transformar as regras do seu negócio
em um sistema que possa ser testado, operado e mantido. A IA pode
@@ -259,7 +296,7 @@ export default function AiPage() {
{incident.links.map((link) => (
<li key={link.href}>
<a
className="inline-flex min-h-11 items-center text-[15px] md:text-sm underline underline-offset-4"
className="inline-flex min-h-11 items-center text-[15px] underline underline-offset-4"
href={link.href}
target="_blank"
rel="noopener noreferrer"
@@ -298,7 +335,7 @@ export default function AiPage() {
adequação.
</p>
<a
className="mt-sp-4 inline-flex min-h-11 items-center text-[15px] md:text-sm underline underline-offset-4"
className="mt-sp-4 inline-flex min-h-11 items-center text-[15px] underline underline-offset-4"
href="https://www.gov.br/anpd/pt-br/acesso-a-informacao/institucional/atos-normativos/regulamentacoes_anpd/resolucao-cd-anpd-no-2-de-27-de-janeiro-de-2022"
target="_blank"
rel="noopener noreferrer"