Files
portifolio-evandrodevbr/app/components/WorkModalities.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

282 lines
10 KiB
TypeScript

import { LayoutDashboard, type LucideIcon, ShieldCheck } from "lucide-react";
import Image from "next/image";
import { getShowcaseAssets } from "@/lib/project-showcase";
import { primaryButton, textLink } from "./button-styles";
import ContactOfferLink from "./ContactOfferLink";
import type { ContactKind } from "./contact-subject";
import "./conversion.css";
import HoverPrefetchLink from "./HoverPrefetchLink";
import { Reveal } from "./Reveal";
import { Section } from "./Section";
interface Offer {
id: string;
title: string;
description: string;
/** O que a pessoa recebe: de três a quatro itens, só o que o case ou a página /ia citados já sustentam. */
deliverables: string[];
/** Primeira etapa: uma conversa e, depois dela, o escopo e a proposta por escrito (sem prazo nem preço). */
firstStep: string;
/** Tipo de projeto que o botão deixa marcado no formulário de contato. */
kind: ContactKind;
ctaLabel: string;
/** Case cujo pôster ilustra a oferta; sem ele, o cartão não tem foto e mostra só o ícone. */
poster?: { id: string; title: string };
/** Ícone do cartão sem foto. */
icon?: LucideIcon;
moreLabel: string;
moreHref: string;
}
/**
* As três ofertas da home, na ordem do H1: site que traz pedidos, sistema que tira a operação da planilha e revisão
* de sistema que já existe. Cada texto só diz o que o case ou a página de /ia citados já sustentam; sem prazo nem preço.
* Só o site tem foto: a Lavação 047 é um site de catálogo e tabela de preços, o que o texto descreve. O sistema e a
* revisão não têm um case que mostre o que descrevem (a vitrine da SEV é uma loja, não um painel), então o cartão
* traz só um ícone, e os entregáveis dizem o que a pessoa recebe.
*/
const OFFERS: Offer[] = [
{
id: "site",
title: "Site que traz pedidos pelo WhatsApp",
description:
"O cliente vê o que você oferece e vai para a conversa no WhatsApp, como na Lavação 047.",
deliverables: [
"Catálogo ou tabela de preços",
"Mensagem pronta no WhatsApp",
"Painel para editar preços e fotos",
"Publicação do site",
],
firstStep:
"Primeira conversa para entender o problema. Depois, escopo e proposta por escrito.",
kind: "site",
ctaLabel: "Começar pelo site",
poster: { id: "lavacao-047", title: "Lavação 047" },
moreLabel: "Ver case Lavação 047",
moreHref: "/cases/lavacao-047",
},
{
id: "sistema",
title: "Sistema para tirar a operação da planilha",
description:
"Pedidos, estoque e rotinas que hoje vivem em planilhas e mensagens passam para um painel próprio, feito para a sua equipe. Um exemplo é a SEV Moda Íntima, com cadastro de peças, pedidos e estoque por tamanho.",
deliverables: [
"Cadastro de clientes e produtos",
"Pedidos e acompanhamento",
"Relatórios",
"Acesso por perfil",
],
firstStep:
"Primeira conversa para entender a operação. Depois, escopo e proposta por escrito.",
kind: "sistema",
ctaLabel: "Começar pelo sistema",
icon: LayoutDashboard,
moreLabel: "Ver case SEV Moda Íntima",
moreHref: "/cases/sev-modaintima",
},
{
id: "revisao",
title: "Revisão de sistema que já existe, inclusive feito com IA",
description:
"Para quem já tem um sistema, como um app feito no Lovable, no Bolt ou no Replit, e quer saber se ele está pronto para receber clientes.",
deliverables: [
"As falhas de acesso a dados",
"O que dá para aproveitar",
"O que é melhor refazer",
"Um orçamento para pôr em produção",
],
firstStep:
"Primeira conversa para entender o app e o que ele precisa fazer. Depois, escopo e proposta por escrito.",
kind: "revisao",
ctaLabel: "Começar pela revisão",
icon: ShieldCheck,
moreLabel: "Ver como funciona a revisão",
moreHref: "/ia",
},
];
const PROCESS = [
{
title: "Entender a operação",
description:
"Quem usa, qual problema precisa resolver e quais dados e ferramentas já existem.",
},
{
title: "Definir a entrega",
description:
"Escopo, arquitetura, riscos, investimento e critérios de aceite antes de desenvolver.",
},
{
title: "Construir e validar",
description:
"Entregas por etapa, demonstrações e testes dos fluxos, das permissões e das integrações.",
},
{
title: "Publicar e dar continuidade",
description:
"Implantação, documentação e condições de manutenção combinadas para a operação.",
},
];
/** Miniatura sem foto: um painel escuro do mesmo tamanho dos pôsteres, só com o ícone da oferta. */
function OfferIcon({ icon: Icon }: Readonly<{ icon: LucideIcon }>) {
return (
<div
data-testid="offer-icon"
className="flex h-full items-center justify-center text-white/85"
>
<Icon aria-hidden="true" className="h-14 w-14" strokeWidth={1.25} />
</div>
);
}
/** Um único estilo de cartão para as três ofertas: foto ou ícone, título com altura mínima, texto, o que você recebe, primeiro passo, botão e link do case. */
function OfferCard({ offer }: Readonly<{ offer: Offer }>) {
const poster = offer.poster
? getShowcaseAssets(offer.poster.id, "desktop")
: null;
return (
<article className="ds-card cv-press cv-lift relative h-full min-w-0 overflow-hidden">
<div className="aspect-[16/10] overflow-hidden border border-[var(--border)] bg-[var(--ink)] md:aspect-[21/9] lg:aspect-[16/10]">
{poster && offer.poster ? (
<Image
src={poster.poster}
alt={`Site ${offer.poster.title} aberto no computador`}
width={poster.width}
height={poster.height}
sizes="(min-width: 1024px) 33vw, 100vw"
loading="lazy"
className="block h-full w-full object-cover object-top"
/>
) : offer.icon ? (
<OfferIcon icon={offer.icon} />
) : null}
</div>
{/* min-h de 3 linhas no desktop: os títulos de 2 e 3 linhas terminam na mesma altura e os textos se alinham */}
<h3 className="ds-card-title !mt-sp-5 text-balance !text-[clamp(20px,5vw,26px)] !leading-[1.1] !tracking-[-0.03em] [overflow-wrap:break-word] [word-break:normal] lg:min-h-[3.3em] lg:!text-[22px] xl:min-h-[2.2em] xl:!text-[24px]">
{offer.title}
</h3>
<p
className="ds-copy !mt-sp-3 !grow-0 !text-[15px] !leading-[1.7] [overflow-wrap:break-word] [word-break:normal]"
data-reel="right"
>
{offer.description}
</p>
<div className="my-sp-5">
<p className="font-display text-[15px] font-semibold leading-6 text-[var(--ink)]">
O que você recebe
</p>
<ul className="mt-sp-2 flex flex-col gap-sp-2">
{offer.deliverables.map((item) => (
<li
key={item}
className="flex items-start gap-sp-3 font-display text-[15px] leading-6 text-[var(--ink-80)]"
>
<svg
aria-hidden="true"
viewBox="0 0 16 16"
className="mt-1 h-4 w-4 shrink-0 text-[var(--ink)]"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="m3 8.5 3.2 3L13 4.5" />
</svg>
{item}
</li>
))}
</ul>
</div>
{/* mt-auto: com cartões de alturas diferentes, o primeiro passo e o botão descem juntos até o pé do cartão */}
<div className="mt-auto border-t border-[var(--border)] pt-sp-4">
<p className="font-display text-[15px] font-semibold leading-6 text-[var(--ink)]">
Primeiro passo
</p>
<p className="mt-sp-1 font-display text-[15px] leading-6 text-[var(--ink-80)]">
{offer.firstStep}
</p>
</div>
<ContactOfferLink
kind={offer.kind}
className={`mt-sp-5 w-full ${primaryButton}`}
>
{offer.ctaLabel}
</ContactOfferLink>
<HoverPrefetchLink
href={offer.moreHref}
className={`${textLink} mt-sp-2 self-start text-[var(--ink-80)]`}
>
{offer.moreLabel}
</HoverPrefetchLink>
</article>
);
}
export function WorkModalities() {
return (
<Section
id="modalities"
eyebrow="Soluções"
title="Escolha por onde começar"
description="Três pontos de partida. Escolha o que mais se parece com o seu caso: o formulário de contato já abre com o tipo de projeto marcado."
disableChildrenAnimation
>
<div className="grid grid-cols-1 gap-sp-5 lg:grid-cols-3 lg:gap-sp-6">
{OFFERS.map((offer, index) => (
<Reveal
key={offer.id}
y={20}
duration={0.75}
delay={0.08 * (index + 1)}
className="min-w-0"
>
<OfferCard offer={offer} />
</Reveal>
))}
</div>
<HoverPrefetchLink
href="/servicos"
className={`${textLink} mt-sp-6 text-[var(--ink-80)]`}
>
Ver todos os serviços
</HoverPrefetchLink>
{/* Lista compacta: o título e a descrição de cada etapa numa linha só, sem a grade de quatro cartões. */}
<div
id="process"
className="mt-sp-12 grid scroll-mt-24 gap-sp-6 border-t border-[var(--border)] pt-sp-8 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:gap-sp-12"
>
<div>
<h3 className="ds-card-title !mt-0">Como eu trabalho</h3>
<p className="ds-copy max-w-[40ch] !text-[15px]">
Você acompanha as decisões e valida cada etapa. Prazo e investimento
dependem do escopo, das integrações e dos riscos identificados.
</p>
</div>
<ol className="divide-y divide-[var(--border)] border-y border-[var(--border)]">
{PROCESS.map((step, index) => (
<li key={step.title} className="flex gap-sp-4 py-sp-4">
<span className="w-7 shrink-0 font-mono text-[15px] leading-6 text-[var(--meta)]">
{String(index + 1).padStart(2, "0")}
</span>
<p className="min-w-0 font-display text-[15px] leading-6 text-[var(--ink-80)]">
<strong className="font-semibold text-[var(--ink)]">
{step.title}.
</strong>{" "}
{step.description}
</p>
</li>
))}
</ol>
</div>
</Section>
);
}