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:
1 parent
e2b20bc699
commit
8b12e51bc5
35 files changed
+2359
-1299
No files matched your search
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user