CI / verify (push) Failing after 5m9s
Align the notebook browser with the physical LCD, refresh real project captures and navigation, simplify service content, and stabilize hero hydration.
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import HeroBackground from "@/app/components/cinematic/HeroBackground";
|
|
import MagneticButton from "@/app/components/cinematic/MagneticButton";
|
|
import { safeJsonLd } from "@/lib/jsonld";
|
|
import { buildBreadcrumbList, buildServiceJsonLd } from "@/lib/seo";
|
|
import {
|
|
getRelatedCases,
|
|
getServicePage,
|
|
servicePages,
|
|
siteName,
|
|
siteUrl,
|
|
} from "@/lib/site-content";
|
|
|
|
interface ServicePageProps {
|
|
params: Promise<{
|
|
slug: string;
|
|
}>;
|
|
}
|
|
|
|
export function generateStaticParams() {
|
|
return servicePages.map((service) => ({ slug: service.slug }));
|
|
}
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: ServicePageProps): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const service = getServicePage(slug);
|
|
|
|
if (!service) {
|
|
return {
|
|
title: `Serviço não encontrado`,
|
|
};
|
|
}
|
|
|
|
const url = `${siteUrl}/servicos/${service.slug}`;
|
|
|
|
return {
|
|
title: service.seoTitle,
|
|
description: service.metaDescription,
|
|
alternates: {
|
|
canonical: url,
|
|
},
|
|
openGraph: {
|
|
title: service.seoTitle,
|
|
description: service.metaDescription,
|
|
url,
|
|
siteName,
|
|
type: "article",
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: service.seoTitle,
|
|
description: service.metaDescription,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function ServicePage({ params }: ServicePageProps) {
|
|
const { slug } = await params;
|
|
const service = getServicePage(slug);
|
|
|
|
if (!service) {
|
|
notFound();
|
|
}
|
|
|
|
const relatedCases = getRelatedCases(service.relatedCaseIds);
|
|
const pageUrl = `${siteUrl}/servicos/${service.slug}`;
|
|
|
|
const jsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@graph": [
|
|
buildServiceJsonLd(service),
|
|
buildBreadcrumbList(pageUrl, [
|
|
["Início", siteUrl],
|
|
["Serviços", `${siteUrl}/servicos`],
|
|
[service.navLabel, pageUrl],
|
|
] as const),
|
|
],
|
|
};
|
|
|
|
return (
|
|
<main>
|
|
<script type="application/ld+json">{safeJsonLd(jsonLd)}</script>
|
|
|
|
<section className="ds-section ds-section-dark relative min-h-[340px] overflow-hidden">
|
|
<HeroBackground />
|
|
<div className="ds-section-shell relative z-10">
|
|
<div className="max-w-[860px]">
|
|
<nav aria-label="Breadcrumb" className="mb-sp-6">
|
|
<ol className="flex flex-wrap items-center gap-sp-2 text-sm">
|
|
<li>
|
|
<Link className="ds-link ds-link-muted" href="/">
|
|
Início
|
|
</Link>
|
|
</li>
|
|
<li className="flex items-center gap-sp-2">
|
|
<span aria-hidden="true">/</span>
|
|
<Link className="ds-link ds-link-muted" href="/servicos">
|
|
Serviços
|
|
</Link>
|
|
</li>
|
|
<li className="flex items-center gap-sp-2">
|
|
<span aria-hidden="true">/</span>
|
|
<Link
|
|
aria-current="page"
|
|
className="ds-link ds-link-muted"
|
|
href={`/servicos/${service.slug}`}
|
|
>
|
|
{service.navLabel}
|
|
</Link>
|
|
</li>
|
|
</ol>
|
|
</nav>
|
|
<p className="ds-eyebrow">{service.eyebrow}</p>
|
|
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
|
|
{service.headline}
|
|
</h1>
|
|
<p className="ds-copy max-w-[64ch] !text-[rgba(255,255,255,0.78)]">
|
|
{service.answer}
|
|
</p>
|
|
<div className="mt-sp-8 flex flex-wrap gap-sp-3">
|
|
<MagneticButton
|
|
as="a"
|
|
href={service.ctaHref}
|
|
className="ds-link ds-link-accent"
|
|
strength={0.25}
|
|
>
|
|
{service.ctaLabel}
|
|
</MagneticButton>
|
|
<MagneticButton
|
|
as="a"
|
|
href="/cases"
|
|
className="ds-link ds-link-muted"
|
|
strength={0.2}
|
|
>
|
|
Ver casos relacionados
|
|
</MagneticButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="ds-section">
|
|
<div className="ds-section-shell">
|
|
<div className="ds-grid-2">
|
|
<article className="ds-card">
|
|
<h2 className="ds-card-title !mt-0">Para quem faz sentido</h2>
|
|
<ul className="ds-list">
|
|
{service.audience.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</article>
|
|
|
|
<article className="ds-card">
|
|
<h2 className="ds-card-title !mt-0">O que podemos entregar</h2>
|
|
<ul className="ds-list">
|
|
{service.outcomes.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ul>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="ds-section">
|
|
<div className="ds-section-shell">
|
|
<div className="ds-grid-2">
|
|
<article className="min-w-0">
|
|
<div className="ds-kicker">Da conversa à entrega</div>
|
|
<h2 className="ds-card-title">Como trabalhamos</h2>
|
|
<ol className="mt-sp-5 list-decimal space-y-sp-4 pl-sp-5 text-sm leading-8 text-[var(--ink-80)]">
|
|
{service.process.map((step) => (
|
|
<li key={step} className="pl-sp-2">
|
|
{step}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</article>
|
|
|
|
<article className="min-w-0">
|
|
<div className="ds-kicker">Possibilidades de uso</div>
|
|
<h2 className="ds-card-title">Imagine na sua rotina</h2>
|
|
<p className="ds-copy">
|
|
Exemplos ilustrativos para ajudar a explicar o serviço, não
|
|
projetos já entregues. O seu escopo será definido na conversa.
|
|
</p>
|
|
<ul className="ds-list">
|
|
{service.examples.map((example) => (
|
|
<li key={example}>{example}</li>
|
|
))}
|
|
</ul>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
<div className="ds-section-shell mt-sp-8">
|
|
<details className="border border-[var(--border)] bg-white/70 p-sp-5 md:p-sp-6">
|
|
<summary className="cursor-pointer py-sp-2 text-base font-semibold leading-relaxed focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--ink)]">
|
|
Detalhes técnicos e tecnologias
|
|
</summary>
|
|
<div className="max-w-[80ch]">
|
|
<ul className="ds-list">
|
|
{service.technicalDetails.map((detail) => (
|
|
<li key={detail}>{detail}</li>
|
|
))}
|
|
</ul>
|
|
<p className="ds-copy">
|
|
Tecnologias consideradas neste serviço. A seleção depende do
|
|
problema e do ambiente; não é uma lista obrigatória para todo
|
|
projeto.
|
|
</p>
|
|
<ul className="ds-tags" aria-label="Tecnologias do serviço">
|
|
{service.stack.map((item) => (
|
|
<li key={item} className="ds-tag !whitespace-normal">
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="ds-section">
|
|
<div className="ds-section-shell">
|
|
<div className="ds-section-header">
|
|
<div className="ds-section-meta">
|
|
<p className="ds-eyebrow">Casos relacionados</p>
|
|
<h2 className="ds-section-title">
|
|
Projetos para conhecer meu trabalho
|
|
</h2>
|
|
<p className="ds-section-description">
|
|
Referências de problemas e soluções relacionados. Cada projeto
|
|
usa as tecnologias indicadas no próprio case; não representa
|
|
todas as plataformas ou tecnologias oferecidas neste serviço.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ds-grid-2">
|
|
{relatedCases.map((caseStudy) => (
|
|
<article key={caseStudy.id} className="ds-card">
|
|
<div className="ds-kicker">{caseStudy.title}</div>
|
|
<h3 className="ds-card-title">{caseStudy.subtitle}</h3>
|
|
<p className="ds-copy">{caseStudy.summary}</p>
|
|
<p className="mt-sp-4 text-[13px] leading-7 text-[var(--meta)]">
|
|
{caseStudy.impact}
|
|
</p>
|
|
<ul className="ds-tags mt-sp-6">
|
|
{caseStudy.stack.map((item) => (
|
|
<li key={item} className="ds-tag !whitespace-normal">
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
|
|
<Link
|
|
href={`/cases/${caseStudy.id}`}
|
|
className="ds-link ds-link-accent"
|
|
>
|
|
Ler case: {caseStudy.title}
|
|
</Link>
|
|
{caseStudy.demoUrl ? (
|
|
<a
|
|
href={caseStudy.demoUrl}
|
|
className="ds-link ds-link-accent"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Abrir projeto: {caseStudy.title}
|
|
</a>
|
|
) : null}
|
|
{caseStudy.githubUrl ? (
|
|
<a
|
|
href={caseStudy.githubUrl}
|
|
className="ds-link ds-link-muted"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Ver código: {caseStudy.title}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="ds-section ds-section-dark pb-sp-6">
|
|
<div className="ds-section-shell">
|
|
<div className="ds-card border-white/10 bg-[rgba(255,255,255,0.03)]">
|
|
<div className="ds-kicker">Próximo passo</div>
|
|
<h2 className="ds-card-title text-white">
|
|
Conte o que você precisa resolver.
|
|
</h2>
|
|
<p className="ds-copy !text-[rgba(255,255,255,0.76)]">
|
|
Pode ser uma ideia, uma tarefa que toma tempo ou um sistema que
|
|
precisa melhorar. Não é preciso chegar com uma solução técnica.
|
|
Atendo a partir de Garuva, no Norte de Santa Catarina, e
|
|
remotamente.
|
|
</p>
|
|
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
|
|
<MagneticButton
|
|
as="a"
|
|
href={service.ctaHref}
|
|
className="ds-link ds-link-accent"
|
|
strength={0.25}
|
|
>
|
|
{service.ctaLabel}
|
|
</MagneticButton>
|
|
<MagneticButton
|
|
as="a"
|
|
href="/servicos"
|
|
className="ds-link ds-link-muted"
|
|
strength={0.2}
|
|
>
|
|
Ver todos os serviços
|
|
</MagneticButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{service.faq && service.faq.length > 0 ? (
|
|
<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">
|
|
{service.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>
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|