feat: refine portfolio showcase, services and quality lab
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.
This commit is contained in:
Evandro Fonseca Junior committed 2026-09-05 14:20:54 -03:00
1 parent e2b20bc699
commit 8b12e51bc5
35 files changed
+2359 -1299

No files matched your search

+2 -2
View File
@@ -40,8 +40,8 @@ export default async function Image({
return new ImageResponse(
<OgCard
eyebrow={service.eyebrow}
title={service.headline}
description={service.answer}
title={service.navLabel}
description={service.intro}
/>,
OG_SIZE,
);
+14 -9
View File
@@ -91,10 +91,18 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
});
const { container } = render(ui);
const heading = screen.getByRole("heading", { level: 1 });
const answer = screen.getByText(service.answer);
expect(answer).toBeVisible();
expect(heading.nextElementSibling).toBe(answer);
for (const item of [
...service.outcomes,
...service.examples,
...service.process,
]) {
expect(screen.getByText(item)).toBeVisible();
}
for (const detail of service.technicalDetails) {
expect(screen.getByText(detail)).not.toBeVisible();
}
const primaryCtas = screen.getAllByRole("link", {
name: service.ctaLabel,
@@ -201,11 +209,6 @@ it("renders answer-first consulting, reciprocal case links and minimal schema",
it("keeps hub cards and structured items aligned", () => {
const { container } = render(<ServicesIndexPage />);
const heading = screen.getByRole("heading", { level: 1 });
expect(heading.nextElementSibling).toHaveTextContent(
/desenvolvimento.+consultoria.+engenharia de software/i,
);
const visibleBreadcrumb = screen.getByRole("navigation", {
name: "Breadcrumb",
});
@@ -225,7 +228,7 @@ it("keeps hub cards and structured items aligned", () => {
"BreadcrumbList",
]);
const itemList = graph[1] as {
itemListElement: Array<{ position: number; url: string }>;
itemListElement: Array<{ position: number; url: string; name: string }>;
};
expect(itemList.itemListElement.map(({ position }) => position)).toEqual([
1, 2, 3, 4, 5, 6,
@@ -236,7 +239,9 @@ it("keeps hub cards and structured items aligned", () => {
expect([...structuredUrls].sort()).toEqual(
publicServiceHrefs.map((href) => `${siteUrl}${href}`).sort(),
);
const visibleCards = screen.getAllByRole("link", { name: "Abrir página" });
const visibleCards = itemList.itemListElement.map(({ name }) =>
screen.getByRole("link", { name }),
);
const visibleHrefs = visibleCards.map((link) => link.getAttribute("href"));
expect(visibleHrefs).toHaveLength(6);
expect(new Set(visibleHrefs).size).toBe(6);
+56 -49
View File
@@ -147,11 +147,8 @@ export default async function ServicePage({ params }: ServicePageProps) {
<div className="ds-section-shell">
<div className="ds-grid-2">
<article className="ds-card">
<div className="ds-kicker">Para quem faz sentido</div>
<h2 className="ds-card-title">
Contextos em que esse serviço encaixa melhor
</h2>
<ul className="ds-list mt-sp-5 pl-[1.15rem]">
<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>
))}
@@ -159,11 +156,8 @@ export default async function ServicePage({ params }: ServicePageProps) {
</article>
<article className="ds-card">
<div className="ds-kicker">O que muda</div>
<h2 className="ds-card-title">
Resultados que eu busco destravar
</h2>
<ul className="ds-list mt-sp-5 pl-[1.15rem]">
<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>
))}
@@ -176,48 +170,59 @@ export default async function ServicePage({ params }: ServicePageProps) {
<section className="ds-section">
<div className="ds-section-shell">
<div className="ds-grid-2">
<article className="ds-card">
<div className="ds-kicker">Como eu estruturo</div>
<h2 className="ds-card-title">Processo de trabalho</h2>
<ol className="list-none mt-sp-5 space-y-sp-4 text-[14px] leading-8 text-[var(--ink-80)]">
{service.process.map((step, index) => (
<li
key={step}
className="border-t border-[var(--border)] pt-sp-4 first:border-t-0 first:pt-0"
>
<span className="mr-sp-3 inline-flex h-7 min-w-7 items-center justify-center rounded-full border border-[var(--border)] text-[11px] font-semibold text-[var(--meta)]">
{index + 1}
</span>
<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="ds-card">
<div className="ds-kicker">Stack e repertório</div>
<h2 className="ds-card-title">
Tecnologias e capacidades envolvidas
</h2>
<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">
A stack entra como meio para resolver o problema certo, com
critério de manutenção, observabilidade e evolução do produto.
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-tags">
{service.stack.map((item) => (
<MagneticButton
key={item}
as="li"
className="ds-tag"
strength={0.15}
>
{item}
</MagneticButton>
<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">
@@ -226,11 +231,12 @@ export default async function ServicePage({ params }: ServicePageProps) {
<div className="ds-section-meta">
<p className="ds-eyebrow">Casos relacionados</p>
<h2 className="ds-section-title">
Projetos que reforçam esse tipo de entrega
Projetos para conhecer meu trabalho
</h2>
<p className="ds-section-description">
Alguns exemplos de contexto técnico em que esse serviço se
conecta a entregas reais.
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>
@@ -246,7 +252,7 @@ export default async function ServicePage({ params }: ServicePageProps) {
</p>
<ul className="ds-tags mt-sp-6">
{caseStudy.stack.map((item) => (
<li key={item} className="ds-tag">
<li key={item} className="ds-tag !whitespace-normal">
{item}
</li>
))}
@@ -265,7 +271,7 @@ export default async function ServicePage({ params }: ServicePageProps) {
target="_blank"
rel="noreferrer"
>
Abrir projeto
Abrir projeto: {caseStudy.title}
</a>
) : null}
{caseStudy.githubUrl ? (
@@ -275,7 +281,7 @@ export default async function ServicePage({ params }: ServicePageProps) {
target="_blank"
rel="noreferrer"
>
Ver código
Ver código: {caseStudy.title}
</a>
) : null}
</div>
@@ -290,12 +296,13 @@ export default async function ServicePage({ params }: ServicePageProps) {
<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">
Se esse serviço parece aderente ao seu contexto, a conversa
inicial já pode ser objetiva.
Conte o que você precisa resolver.
</h2>
<p className="ds-copy !text-[rgba(255,255,255,0.76)]">
Traga o sistema, o gargalo e o objetivo de negócio. Eu estruturo a
leitura técnica e indico o caminho mais seguro para avançar.
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
+3 -3
View File
@@ -2,7 +2,7 @@ import { ImageResponse } from "next/og";
import { OG_CONTENT_TYPE, OG_SIZE, OgCard } from "@/lib/og-card";
export const alt =
"Serviços de engenharia de software e consultoria técnica — Evandro Fonseca Junior";
"Sites, sistemas web, aplicativos mobile e desktop — Evandro Fonseca Junior";
export const size = OG_SIZE;
export const contentType = OG_CONTENT_TYPE;
@@ -10,8 +10,8 @@ export default async function Image() {
return new ImageResponse(
<OgCard
eyebrow="Serviços"
title="Engenharia de software e consultoria técnica"
description="Base em Garuva, Norte de Santa Catarina e remoto."
title="Sites, sistemas e aplicativos"
description="Web, mobile e desktop para Windows e Linux. Desenvolvimento, integrações e melhorias para o seu negócio."
/>,
OG_SIZE,
);
+49 -23
View File
@@ -1,15 +1,14 @@
import type { Metadata } from "next";
import Link from "next/link";
import HeroBackground from "@/app/components/cinematic/HeroBackground";
import MagneticButton from "@/app/components/cinematic/MagneticButton";
import { safeJsonLd } from "@/lib/jsonld";
import { buildBreadcrumbList } from "@/lib/seo";
import { serviceHubLinks, siteName, siteUrl } from "@/lib/site-content";
const pageUrl = `${siteUrl}/servicos`;
const pageTitle = "Serviços de Engenharia de Software e Consultoria Técnica";
const pageTitle = "Sites, Sistemas Web e Aplicativos Mobile e Desktop";
const pageDescription =
"Desenvolvimento e consultoria em engenharia de software, Next.js, Tauri, Rust, arquitetura e integração hardware + software para produtos e sistemas críticos.";
"Desenvolvimento de sites, sistemas web, aplicativos mobile e desktop para Windows e Linux. Integrações, melhorias e consultoria para o seu negócio.";
export const metadata: Metadata = {
title: pageTitle,
@@ -80,13 +79,15 @@ export default function ServicesIndexPage() {
</li>
</ol>
</nav>
<p className="ds-eyebrow">Hub de serviços</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
Serviços de engenharia de software para contextos técnicos de alta
exigência.
<p className="ds-eyebrow">Serviços de desenvolvimento</p>
<h1 className="ds-section-title max-w-[18ch] text-balance text-white">
Da presença na internet ao sistema que move seu negócio.
</h1>
<p className="ds-copy max-w-[64ch] !text-[rgba(255,255,255,0.78)]">
{pageDescription}
Sites e sistemas web, aplicativos mobile e aplicativos desktop
para Windows e Linux. Também integro ferramentas e melhoro
software que já está em uso. Começamos pelo que você precisa
resolver, não pela tecnologia.
</p>
</div>
</div>
@@ -94,25 +95,50 @@ export default function ServicesIndexPage() {
<section className="ds-section">
<div className="ds-section-shell">
<div className="ds-grid-2">
{serviceHubLinks.map((service) => (
<article key={service.href} className="ds-card">
<div className="ds-kicker">Serviço especializado</div>
<h2 className="ds-card-title">{service.label}</h2>
<p className="ds-copy">{service.description}</p>
<div className="mt-sp-6">
<MagneticButton
as="a"
href={service.href}
className="ds-link ds-link-accent"
strength={0.25}
>
Abrir página
</MagneticButton>
<div className="max-w-[72ch]">
<h2 className="ds-card-title !mt-0">O que você precisa fazer?</h2>
<p className="ds-copy">
Conheça as possibilidades abaixo. Cada serviço explica para quem
faz sentido, exemplos de uso, escopo e etapas de trabalho. Os
detalhes técnicos ficam disponíveis para aprofundar a conversa.
</p>
</div>
<div className="mt-sp-8 divide-y divide-[var(--border)] border-y border-[var(--border)]">
{serviceHubLinks.map((service, index) => (
<article
key={service.href}
className="grid gap-sp-4 py-sp-6 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] md:gap-sp-8"
>
<div className="min-w-0">
<p className="ds-kicker" aria-hidden="true">
{String(index + 1).padStart(2, "0")}
</p>
<h3 className="ds-card-title">
<Link
href={service.href}
className="underline decoration-[var(--slash)] decoration-2 underline-offset-4 hover:decoration-[var(--ink)]"
>
{service.label}
</Link>
</h3>
</div>
<p className="ds-copy !mt-0">{service.description}</p>
</article>
))}
</div>
<div className="mt-sp-8 max-w-[72ch]">
<h2 className="ds-card-title">
Ainda não sabe qual caminho seguir?
</h2>
<p className="ds-copy">
Conte como funciona sua rotina e o que gostaria de mudar.
Definimos juntos a primeira entrega, as plataformas atendidas e os
limites do projeto antes de fechar a proposta.
</p>
<Link className="ds-link ds-link-accent mt-sp-5" href="/#contact">
Conversar sobre minha ideia
</Link>
</div>
</div>
</section>
</main>