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.
This commit is contained in:
1 parent
fc44a69fb3
commit
9e2d65a926
51 files changed
+1730
-420
No files matched your search
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -12,11 +13,17 @@ 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, a miniatura é o cartão de checklist. */
|
||||
project?: { id: string; title: 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;
|
||||
}
|
||||
@@ -24,17 +31,28 @@ interface Offer {
|
||||
/**
|
||||
* 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:
|
||||
"Catálogo ou tabela de preços, mensagem pronta no WhatsApp e um painel para você editar preços, textos e fotos sozinho, como na Lavação 047.",
|
||||
"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",
|
||||
project: { id: "lavacao-047", title: "Lavação 047" },
|
||||
moreLabel: "Ver o case Lavação 047",
|
||||
poster: { id: "lavacao-047", title: "Lavação 047" },
|
||||
moreLabel: "Ver case Lavação 047",
|
||||
moreHref: "/cases/lavacao-047",
|
||||
},
|
||||
{
|
||||
@@ -42,32 +60,41 @@ const OFFERS: Offer[] = [
|
||||
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",
|
||||
project: { id: "sev-modaintima", title: "SEV Moda Íntima" },
|
||||
moreLabel: "Ver o case SEV Moda Íntima",
|
||||
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:
|
||||
"Avalio o que já existe, testo as falhas de acesso a dados e separo o que dá para aproveitar do que é melhor refazer, antes de o sistema receber mais clientes.",
|
||||
"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",
|
||||
},
|
||||
];
|
||||
|
||||
/** O que a revisão entrega: os mesmos itens da página /ia, em versão curta para a miniatura da oferta. */
|
||||
const REVIEW_CHECKLIST = [
|
||||
"As falhas de acesso a dados",
|
||||
"O que dá para aproveitar",
|
||||
"O que é melhor refazer",
|
||||
"Um orçamento para pôr em produção",
|
||||
];
|
||||
|
||||
const PROCESS = [
|
||||
{
|
||||
title: "Entender a operação",
|
||||
@@ -91,59 +118,40 @@ const PROCESS = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Miniatura da revisão, sem case para mostrar: o que o relatório traz, em um cartão escuro do mesmo tamanho dos pôsteres. */
|
||||
function ReviewChecklist() {
|
||||
/** 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 className="flex h-full flex-col justify-center gap-sp-3 px-sp-5 py-sp-4 text-white">
|
||||
<p className="font-display text-[15px] font-semibold leading-6 text-white">
|
||||
O que você recebe
|
||||
</p>
|
||||
<ul className="flex flex-col gap-sp-2">
|
||||
{REVIEW_CHECKLIST.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-sp-3 font-display text-[15px] leading-6 text-white/85"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
className="mt-1 h-4 w-4 shrink-0 text-white/70"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="m3 8.5 3.2 3L13 4.5" />
|
||||
</svg>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<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: miniatura, título com altura mínima, texto, botão e link do case. */
|
||||
/** 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.project
|
||||
? getShowcaseAssets(offer.project.id, "desktop")
|
||||
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.project ? (
|
||||
{poster && offer.poster ? (
|
||||
<Image
|
||||
src={poster.poster}
|
||||
alt={`Site ${offer.project.title} aberto no computador`}
|
||||
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"
|
||||
/>
|
||||
) : (
|
||||
<ReviewChecklist />
|
||||
)}
|
||||
) : 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 */}
|
||||
@@ -152,15 +160,51 @@ function OfferCard({ offer }: Readonly<{ offer: Offer }>) {
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className="ds-copy !mt-sp-3 !text-[15px] !leading-[1.7] [overflow-wrap:break-word] [word-break:normal]"
|
||||
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-6 w-full ${primaryButton}`}
|
||||
className={`mt-sp-5 w-full ${primaryButton}`}
|
||||
>
|
||||
{offer.ctaLabel}
|
||||
</ContactOfferLink>
|
||||
@@ -197,6 +241,13 @@ export function WorkModalities() {
|
||||
))}
|
||||
</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"
|
||||
|
||||
Reference in new issue
Block a user