feat(cases): cases de clientes, serviços ligados à prova e SEO por página

Cases da SEV Moda Íntima e da Lavação 047; case no formato cliente, problema, entrega, mídia e contato, com detalhes técnicos recolhidos; sem GitHub, ressalvas e fontes de commit. /cases separa clientes e Laboratório. Serviços mostram o case de cliente certo e o bloco de investimento e prazo só aparece preenchido. Metadata social própria por página, not-found com noindex, sitemap com /ia.
This commit is contained in:
Evandro Fonseca Junior committed 2026-10-14 03:31:57 -03:00
1 parent 3756905a1b
commit 6e1022b58c
44 files changed
+2691 -1424

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import type { ServicePageContent } from "@/lib/site-content";
/** "Perguntas frequentes" do serviço. Sem perguntas, a seção não aparece. */
export function ServiceFaq({ faq }: { faq: ServicePageContent["faq"] }) {
if (!faq || faq.length === 0) return null;
return (
<section className="ds-section">
<div className="ds-section-shell">
<div className="ds-section-header">
<div className="ds-section-meta">
<p className="ds-eyebrow">Dúvidas comuns</p>
<h2 className="ds-section-title">Perguntas frequentes</h2>
<p className="ds-section-description">
Respostas diretas às perguntas que costumam aparecer antes de cada
projeto começar.
</p>
</div>
</div>
<div className="ds-grid-2">
{faq.map((item) => (
<article key={item.question} className="ds-card">
<h3 className="ds-card-title">{item.question}</h3>
<p className="ds-copy">{item.answer}</p>
</article>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,25 @@
import Link from "next/link";
import { CaseCover } from "@/app/cases/_components/CaseCover";
import type { CaseStudyContent } from "@/lib/site-content";
/** Capa do case de destaque ao lado do título do serviço. Sem case com captura, não mostra nada. */
export function ServiceHeroCover({
caseStudy,
}: {
caseStudy?: CaseStudyContent;
}) {
if (!caseStudy) return null;
return (
<Link href={`/cases/${caseStudy.id}`} className="block min-w-0">
<span className="sr-only">Ver o case {caseStudy.title}</span>
<CaseCover
id={caseStudy.id}
title={caseStudy.title}
demoUrl={caseStudy.demoUrl}
sizes="(min-width: 1024px) 520px, 100vw"
priority
/>
</Link>
);
}
+41
View File
@@ -0,0 +1,41 @@
import type { ServicePageContent } from "@/lib/site-content";
/** "Investimento e prazo": só mostra os campos preenchidos com valor real; sem nenhum, a seção não aparece. */
export function ServiceOffer({
service,
}: {
service: Pick<
ServicePageContent,
"startingPrice" | "typicalTimeline" | "guarantee"
>;
}) {
const offer = [
{ label: "A partir de", value: service.startingPrice },
{ label: "Prazo típico", value: service.typicalTimeline },
{ label: "Garantia", value: service.guarantee },
].filter((item): item is { label: string; value: string } =>
Boolean(item.value),
);
if (offer.length === 0) return null;
return (
<section className="ds-section" aria-labelledby="investimento-prazo">
<div className="ds-section-shell">
<h2 id="investimento-prazo" className="ds-card-title !mt-0">
Investimento e prazo
</h2>
<dl className="mt-sp-6 grid gap-sp-6 md:grid-cols-3">
{offer.map((item) => (
<div
key={item.label}
className="border-t border-[var(--border)] pt-sp-4"
>
<dt className="ds-kicker">{item.label}</dt>
<dd className="ds-copy !mt-sp-2">{item.value}</dd>
</div>
))}
</dl>
</div>
</section>
);
}
@@ -0,0 +1,84 @@
import Link from "next/link";
import { CaseCover } from "@/app/cases/_components/CaseCover";
import { getCaseMedia } from "@/app/cases/_components/case-media";
import { secondaryButtonLight, textLink } from "@/app/components/button-styles";
import { type CaseStudyContent, isClientCase } from "@/lib/site-content";
/** "Veja projetos parecidos": os cases ligados ao serviço. Sem case relacionado, não mostra nada. */
export function ServiceRelatedCases({
cases,
}: {
cases: readonly CaseStudyContent[];
}) {
if (cases.length === 0) return null;
return (
<section
id="casos-relacionados"
className="ds-section"
aria-labelledby="casos-relacionados-titulo"
>
<div className="ds-section-shell">
<div className="ds-section-header">
<div className="ds-section-meta">
<p className="ds-eyebrow">Trabalhos parecidos</p>
<h2 id="casos-relacionados-titulo" className="ds-section-title">
Veja projetos parecidos, já prontos
</h2>
<p className="ds-section-description">
Projetos reais nesta área. Abra o case para ver o que foi feito e
como ficou.
</p>
</div>
</div>
<div className="ds-grid-2">
{cases.map((caseStudy) => (
<article key={caseStudy.id} className="ds-card">
{getCaseMedia(caseStudy.id, "desktop") ? (
<Link
href={`/cases/${caseStudy.id}`}
aria-hidden="true"
tabIndex={-1}
className="mb-sp-5 block"
>
<CaseCover
id={caseStudy.id}
title={caseStudy.title}
demoUrl={caseStudy.demoUrl}
sizes="(min-width: 1024px) 560px, 100vw"
/>
</Link>
) : null}
<div className="ds-kicker">
{isClientCase(caseStudy)
? caseStudy.title
: `${caseStudy.title} · Protótipo de laboratório`}
</div>
<h3 className="ds-card-title">{caseStudy.headline}</h3>
<p className="ds-copy">{caseStudy.summary}</p>
<div className="mt-sp-6 flex flex-wrap items-center gap-sp-3">
<Link
href={`/cases/${caseStudy.id}`}
className={secondaryButtonLight}
>
Ler case: {caseStudy.title}
</Link>
{caseStudy.demoUrl ? (
<a
href={caseStudy.demoUrl}
className={textLink}
target="_blank"
rel="noreferrer"
>
Abrir o site: {caseStudy.title}
</a>
) : null}
</div>
</article>
))}
</div>
</div>
</section>
);
}