Hero e movimento - cena 3D em tempo real com os blocos da marca (kit feito no Blender, 3d/blocos.py): marca, tela, integrações e dados; segue a rolagem, tem vida em repouso, reage ao cursor e, parada no topo, passa sozinha pelos estados com legenda - três níveis de aparelho (full, lite, static); sem WebGL fica o pôster - título novo: "Sites e sistemas sob medida para o jeito que a sua empresa trabalha." - faixa de clientes com o ramo, linha do primeiro passo, hero alinhado às seções - rótulos digitados, entrada dos blocos, cortina de blocos entre páginas e capa compartilhada entre a lista de cases e o case Ofertas - atendimento no WhatsApp com IA e nota fiscal para a reforma tributária (páginas de serviço, cartões da home, formulário e aviso com data de validade) - página /ia atualizada, com fontes - peças 3D em loop nos cartões e topos de página (3d/pecas.py) Design - hover por papel (bloco nos botões, barra nos links), rótulos sem caixa alta - um destino por rótulo: "Falar sobre meu projeto" leva ao formulário, "Chamar no WhatsApp" abre o WhatsApp - logo montado em blocos, 404 redesenhado, grades sem célula vazia - ícones do PWA e manifesto novos Specs em specs/, contexto de design em DESIGN.md e PRODUCT.md.
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
import { CaseCoverLink } from "@/app/cases/_components/CaseCoverLink";
|
|
import { secondaryButtonLight, textLink } from "@/app/components/button-styles";
|
|
import HoverPrefetchLink from "@/app/components/HoverPrefetchLink";
|
|
import {
|
|
buttonLabels,
|
|
type CaseStudyContent,
|
|
isClientCase,
|
|
} from "@/lib/site-content";
|
|
|
|
function ServiceCaseCard({
|
|
caseStudy,
|
|
relevance,
|
|
}: {
|
|
caseStudy: CaseStudyContent;
|
|
/** Por que este case interessa ao serviço; sem frase própria, vale o resumo do case. */
|
|
relevance?: string;
|
|
}) {
|
|
return (
|
|
<article className="ds-card">
|
|
<CaseCoverLink
|
|
id={caseStudy.id}
|
|
title={caseStudy.title}
|
|
demoUrl={caseStudy.demoUrl}
|
|
/>
|
|
<div className="ds-kicker">
|
|
{isClientCase(caseStudy)
|
|
? caseStudy.title
|
|
: `${caseStudy.title} · Projeto próprio`}
|
|
</div>
|
|
<h3 className="ds-card-title">{caseStudy.headline}</h3>
|
|
<p className="ds-kicker mt-sp-5">Relação com este serviço</p>
|
|
<p className="ds-copy !mt-sp-2">{relevance ?? caseStudy.summary}</p>
|
|
<div className="mt-sp-6 flex flex-wrap items-center gap-sp-3">
|
|
<HoverPrefetchLink
|
|
href={`/cases/${caseStudy.id}`}
|
|
className={secondaryButtonLight}
|
|
>
|
|
{buttonLabels.case}
|
|
<span className="sr-only"> {caseStudy.title}</span>
|
|
</HoverPrefetchLink>
|
|
{caseStudy.demoUrl ? (
|
|
<a
|
|
href={caseStudy.demoUrl}
|
|
className={textLink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{buttonLabels.site}
|
|
<span className="sr-only"> {caseStudy.title}</span>
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Prova do serviço. Com algum case de cliente, é "Onde já apliquei isso": os cases ligados ao serviço, cada um com uma
|
|
* frase sobre o que ele tem a ver com ele, e a experiência profissional citada em texto (sem link). Sem case de cliente
|
|
* (só projeto próprio, só experiência ou nada), o título é "Como eu trabalho nisso", porque não há cliente para citar.
|
|
* Sem case, fica só a nota de experiência, sem a frase que manda abrir o case; sem case nem experiência, não mostra nada.
|
|
* O case cuja capa abre a página (`heroCaseId`) não ganha cartão aqui: a legenda da capa já o cita, com a mesma frase de
|
|
* relação, e a imagem e o nome dele não aparecem duas vezes na mesma página.
|
|
*/
|
|
export function ServiceRelatedCases({
|
|
cases: allCases,
|
|
relevance,
|
|
experienceNote,
|
|
heroCaseId,
|
|
}: {
|
|
cases: readonly CaseStudyContent[];
|
|
/** Por que cada case interessa a este serviço (chave: id do case). */
|
|
relevance: Readonly<Record<string, string>>;
|
|
experienceNote?: string;
|
|
/** Case cuja capa já está no topo da página, com legenda: fica de fora dos cartões. */
|
|
heroCaseId?: string;
|
|
}) {
|
|
const cases = allCases.filter(({ id }) => id !== heroCaseId);
|
|
const hasCases = cases.length > 0;
|
|
if (!hasCases && !experienceNote) return null;
|
|
const hasClientCase = cases.some(isClientCase);
|
|
|
|
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">
|
|
{hasClientCase ? "Trabalhos parecidos" : "Experiência"}
|
|
</p>
|
|
<h2 id="casos-relacionados-titulo" className="ds-section-title">
|
|
{hasClientCase
|
|
? "Onde já apliquei isso"
|
|
: "Como eu trabalho nisso"}
|
|
</h2>
|
|
{hasCases ? (
|
|
<p className="ds-section-description">
|
|
Cada projeto tem a ver com este serviço de um jeito. Abra o case
|
|
para ver o que foi feito e como ficou.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{experienceNote ? (
|
|
<p className="ds-copy mb-sp-8 max-w-[72ch]">
|
|
<strong className="font-semibold text-[var(--ink)]">
|
|
Na prática:
|
|
</strong>{" "}
|
|
{experienceNote}
|
|
</p>
|
|
) : null}
|
|
|
|
{hasCases ? (
|
|
<div className="ds-grid-2">
|
|
{cases.map((caseStudy) => (
|
|
<ServiceCaseCard
|
|
key={caseStudy.id}
|
|
caseStudy={caseStudy}
|
|
relevance={relevance[caseStudy.id]}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|