Files
portifolio-evandrodevbr/app/layout.tsx
T
evandro c7d09d63d4
CI / audit (push) Successful in 10m38s
CI / verify (push) Successful in 13m10s
CI / deploy (push) Successful in 1m50s
CI / e2e (push) Successful in 18m48s
feat(site): blocos 3D da marca, novas ofertas e hero mais abrangente
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.
2026-11-10 03:48:25 -03:00

180 lines
6.3 KiB
TypeScript

import type { Metadata, Viewport } from "next";
import { Geist_Mono, Space_Grotesk } from "next/font/google";
import type { ReactNode } from "react";
import { safeJsonLd } from "@/lib/jsonld";
import { buildPersonJsonLd, buildWebSiteJsonLd } from "@/lib/seo";
import {
caseStudies,
serviceHubLinks,
servicePages,
siteUrl,
} from "@/lib/site-content";
import type { WhatsAppContexts } from "@/lib/whatsapp";
import CinematicOverlays from "./components/cinematic/CinematicOverlays";
import { FRAGMENT_NAV_BOOT_SCRIPT } from "./components/cinematic/fragment-nav";
import FloatingWhatsApp from "./components/FloatingWhatsApp";
import Footer from "./components/Footer";
import GoogleAnalytics from "./components/GoogleAnalytics";
import { Header } from "./components/Header";
import "./globals.css";
// As molduras de navegador e de celular (o hero e os cases também as usam) vêm uma vez só, aqui, depois do global.
// A lista da vitrine (reel-list.css) já vem no globals.css, por @import; só a vitrine fixa (pinned-reel.css) e a janela
// ampliada (lightbox.css) viajam com os componentes deles.
import "./components/showcase/showcase.css";
const titleSuffix = " | Evandro.dev.br";
/** Título da home: o `title.default` não passa pelo template, então leva o sufixo. Com ele, no máximo 60 caracteres. */
const defaultTitle = `Sites e sistemas em Joinville e SC${titleSuffix}`;
const seoDescription =
"Sites e sistemas sob medida para o jeito que a sua empresa trabalha. Base em Garuva, SC; atendimento em Joinville e remoto em todo o Brasil.";
/**
* Para o WhatsApp falar do serviço ou do case da página aberta: só os assuntos e nomes, sem o conteúdo pesado das
* páginas. O serviço entra pelo `whatsappSubject` (uma frase em minúsculas, que cabe em "Quero conversar sobre
* {assunto}."), e não pelo nome: o nome daria "sobre Sites profissionais.".
*/
const whatsAppContexts: WhatsAppContexts = {
services: Object.fromEntries(
servicePages.map((service) => [service.slug, service.whatsappSubject]),
),
cases: Object.fromEntries(
caseStudies.map((caseStudy) => [caseStudy.id, caseStudy.title]),
),
};
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
display: "swap",
variable: "--font-space-grotesk",
});
const geistMono = Geist_Mono({
subsets: ["latin"],
weight: ["400", "500", "600"],
display: "swap",
// só rótulos pequenos e kickers: não disputa a rede com a fonte do título, o arquivo é baixado quando o texto usa
preload: false,
variable: "--font-geist-mono",
});
export const metadata: Metadata = {
title: {
default: defaultTitle,
template: `%s${titleSuffix}`,
},
description: seoDescription,
authors: [{ name: "Evandro Fonseca Junior" }],
creator: "Evandro Fonseca Junior",
publisher: "Evandro Fonseca Junior",
applicationName: "Evandro.dev.br",
category: "Technology",
metadataBase: new URL(siteUrl),
formatDetection: {
email: false,
address: false,
telephone: false,
},
openGraph: {
title: defaultTitle,
description: seoDescription,
url: siteUrl,
siteName: "Evandro.dev.br",
locale: "pt_BR",
type: "website",
images: [
{
url: "/opengraph-image",
width: 1200,
height: 630,
alt: "Evandro.dev.br | Sites e sistemas para empresas",
},
],
},
twitter: {
card: "summary_large_image",
title: defaultTitle,
description: seoDescription,
creator: "@evandrodevbr",
images: [
{
url: "/opengraph-image",
alt: "Evandro.dev.br | Sites e sistemas para empresas",
},
],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
export const viewport: Viewport = {
themeColor: "#000000",
width: "device-width",
initialScale: 1,
maximumScale: 5,
};
interface RootLayoutProps {
children: ReactNode;
}
export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [buildWebSiteJsonLd(), buildPersonJsonLd()],
};
return (
// data-scroll-behavior: com `scroll-behavior: smooth` no CSS global, o Next desliga a suavização só durante a troca
// de página (a página nova abre no topo, sem deslizar) e a restaura depois; as âncoras da mesma página seguem suaves.
<html
lang="pt-BR"
data-scroll-behavior="smooth"
className={`${spaceGrotesk.variable} ${geistMono.variable}`}
>
<head>
{/* Set arrival policy before native restoration and client-side layout changes. */}
<style id="arrival-scroll-policy">{`html { scroll-behavior: auto !important; }`}</style>
{/* Sem JavaScript as animações de entrada (opacity:0 inline vindo do servidor e palavras de título em CSS) não rodam: tudo precisa aparecer. */}
<noscript>
<style>{`[style*="opacity:0"],[style*="clip-path"]{opacity:1!important;transform:none!important;clip-path:none!important}.text-reveal-word{opacity:1!important;transform:none!important}.js-only{display:none!important}`}</style>
</noscript>
<script>{`
addEventListener("load", () => {
const sheet = document.getElementById("arrival-scroll-policy")?.sheet;
if (sheet) sheet.disabled = true;
}, { once: true });
${FRAGMENT_NAV_BOOT_SCRIPT}
`}</script>
</head>
<body>
<script type="application/ld+json">{safeJsonLd(jsonLd)}</script>
<a className="skip-link" href="#main-content">
Pular para o conteúdo
</a>
{/* Aviso de cookies logo depois do "pular": fica cedo na ordem do Tab, ainda que fixo no rodapé da tela. */}
<GoogleAnalytics />
<Header whatsAppContexts={whatsAppContexts} />
<CinematicOverlays />
<div id="main-content" tabIndex={-1}>
{children}
</div>
{/* Rodapé de todas as páginas; z-10 o mantém acima das cenas da home (relative z-0). O id deixa o menu do celular torná-lo inerte. */}
<div id="site-footer" className="relative z-10">
<Footer serviceLinks={serviceHubLinks} />
</div>
<FloatingWhatsApp contexts={whatsAppContexts} />
</body>
</html>
);
}