feat: initialize portfolio site with core components, pages, and content structure, and update Next.js configuration.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-17 20:08:00 -03:00
1 parent 223746b898
commit d00258dcd1
22 files changed
+1512 -8038

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
import type { Metadata } from "next";
import Link from "next/link";
import { caseStudies, siteName, siteUrl } from "@/lib/site-content";
export const metadata: Metadata = {
title: `Cases de Engenharia de Software, Next.js e IoT | ${siteName}`,
description:
"Cases selecionados em engenharia de software, Next.js, IoT, integracoes e software para contextos criticos. Portfolio tecnico com foco em clareza, performance e execucao.",
alternates: {
canonical: `${siteUrl}/cases`,
},
openGraph: {
title: `Cases de Engenharia de Software, Next.js e IoT | ${siteName}`,
description:
"Cases selecionados em engenharia de software, Next.js, IoT, integracoes e software para contextos criticos.",
url: `${siteUrl}/cases`,
siteName,
type: "website",
},
};
export default function CasesPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "CollectionPage",
"@id": `${siteUrl}/cases#page`,
name: "Cases de engenharia de software",
url: `${siteUrl}/cases`,
description:
"Colecao de cases em Next.js, IoT, integracoes e software para operacoes criticas.",
},
{
"@type": "ItemList",
itemListElement: caseStudies.map((caseStudy, index) => ({
"@type": "ListItem",
position: index + 1,
name: caseStudy.title,
url: caseStudy.demoUrl ?? caseStudy.githubUrl ?? `${siteUrl}/cases`,
})),
},
],
};
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<div className="max-w-[860px]">
<p className="ds-eyebrow">Selected work</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
Cases em software, Next.js, integracoes e produtos conectados.
</h1>
<p className="ds-copy max-w-[64ch] !text-[rgba(255,255,255,0.78)]">
Esta pagina organiza projetos com leitura mais indexavel e comercial, conectando stack,
contexto tecnico e tipo de problema resolvido.
</p>
</div>
</div>
</section>
<section className="ds-section">
<div className="ds-section-shell">
<div className="ds-grid-2">
{caseStudies.map((caseStudy) => (
<article key={caseStudy.id} className="ds-card">
<div className="ds-kicker">{caseStudy.title}</div>
<h2 className="ds-card-title">{caseStudy.subtitle}</h2>
<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">
{item}
</li>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{caseStudy.demoUrl ? (
<a href={caseStudy.demoUrl} className="ds-link ds-link-accent" target="_blank" rel="noreferrer">
Abrir projeto
</a>
) : null}
{caseStudy.githubUrl ? (
<a href={caseStudy.githubUrl} className="ds-link ds-link-muted" target="_blank" rel="noreferrer">
Ver codigo
</a>
) : null}
</div>
</article>
))}
</div>
<div className="mt-sp-10 flex flex-wrap gap-sp-3">
<Link href="/servicos" className="ds-link ds-link-accent">
Ver servicos
</Link>
<Link href="/#contact" className="ds-link ds-link-muted">
Ir para contato
</Link>
</div>
</div>
</section>
</main>
);
}
+31 -34
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import profilePhoto from "@/public/edev.jpeg";
@@ -8,39 +8,34 @@ import { LazyRevealImage } from "./animations/LazyRevealImage";
const STORY_DATA = [
{
id: "HARDWARE_BRIDGE",
title: "Do físico ao digital",
label: "Hardware + software",
title: "Do sinal fisico ao produto digital",
content:
"Construo a ponte entre hardware e software. Projeto sistemas capazes de orquestrar dispositivos físicos, protocolos de baixo nível e interfaces web modernas com baixa latência e alta resiliência.",
"Construo a ponte entre dispositivos, protocolos de baixo nivel, backend e interface para que o sistema inteiro pareca uma unica experiencia coerente.",
},
{
id: "RUNTIME_EXEC",
title: "Entrega sob restrições",
label: "Execution",
title: "Entrega clara mesmo sob restricao",
content:
"Atuo com naturalidade em cenários críticos envolvendo integrações de hardware, APIs complexas e infraestrutura legada, sempre priorizando a estabilidade e a legibilidade técnica.",
"Trabalho bem em contextos com legado, integracoes complexas e pouca margem para erro, priorizando estabilidade, leitura tecnica e evolucao sustentavel.",
},
];
const VALUES_DATA = [
const PROFILE_POINTS = [
{
title: "Pragmatismo arquitetural",
title: "Arquitetura pragmatica",
description:
"Decisões guiadas por contexto real e operabilidade. A melhor tecnologia é aquela que resolve o problema sem adicionar complexidade inútil ao sistema.",
"Escolhas guiadas por contexto, operabilidade e impacto real no produto.",
},
{
title: "Fundação & Integração",
title: "Integracao profunda",
description:
"Domínio de protocolos de rede e arquiteturas distribuídas para conectar desde microcontroladores até sistemas robustos na nuvem.",
"Capacidade de atravessar camada fisica, servicos e interface sem perder consistencia.",
},
{
title: "Ownership ponta a ponta",
description:
"Responsabilidade que atravessa camadas: do pulso elétrico no hardware até a renderização do último componente na tela do usuário.",
},
{
title: "Security & Safety",
description:
"Segurança e resiliência tratadas como base. Sistemas desenhados para falhar graciosamente e proteger dados do dispositivo à nuvem.",
"Responsabilidade que cobre discovery, implementacao, qualidade e operacao.",
},
];
@@ -84,9 +79,9 @@ export default function About() {
return (
<Section
id="about"
eyebrow="WHOAMI.sys"
title="Engenharia orientada por clareza, nao por ruido"
description="Transformo complexidade tecnica em sistemas estaveis, operaveis e alinhados ao negocio."
eyebrow="Operating context"
title="Engenharia de software, arquitetura e integracoes complexas"
description="Atuo na intersecao entre arquitetura de software, integracoes de sistemas, hardware + software e entrega full-stack para produtos que precisam funcionar com confiabilidade."
disableChildrenAnimation
aside={
<div className="mt-sp-4 ds-metric">
@@ -108,14 +103,14 @@ export default function About() {
>
{STORY_DATA.map((step) => (
<motion.article
key={step.id}
key={step.title}
className="ds-card"
variants={cardVariants}
whileHover={cardHover}
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
transition={{ type: "spring", stiffness: 180, damping: 18 }}
>
<div className="ds-kicker">{step.id}</div>
<div className="ds-kicker">{step.label}</div>
<DrawLine delay={0.03} />
<h3 className="ds-card-title">{step.title}</h3>
<p className="ds-copy">{step.content}</p>
@@ -136,25 +131,25 @@ export default function About() {
className="mb-sp-6 h-[220px] w-full overflow-hidden rounded-[6px] border border-[var(--border)]"
imageClassName="object-cover"
/>
<div className="ds-kicker">System Signature</div>
<div className="ds-kicker">Technical profile</div>
<DrawLine delay={0.05} />
<h3 className="ds-card-title">Base tecnica e postura de entrega</h3>
<p className="ds-copy">
Evandro Fonseca Junior. Engenheiro de Software Full-Stack com foco
na convergência entre hardware e aplicações web modernas.
Especialista em TypeScript, Node.js, ecossistema React e automação
de dispositivos físicos.
Sou engenheiro de software full-stack com foco em integrar camadas
que normalmente ficam separadas: hardware, servicos, operacao e
interface. Meu trabalho e dar forma clara ao que costuma nascer
fragmentado.
</p>
<div className="ds-grid-2 mt-sp-6">
{VALUES_DATA.map((value) => (
<div className="mt-sp-6 space-y-sp-4">
{PROFILE_POINTS.map((point) => (
<div
key={value.title}
key={point.title}
className="border-t border-[var(--border)] pt-sp-4"
>
<div className="text-[11px] font-medium uppercase tracking-[0.08em]">
{value.title}
<div className="text-[12px] font-medium tracking-[0.02em]">
{point.title}
</div>
<p className="ds-copy !mt-sp-2">{value.description}</p>
<p className="ds-copy !mt-sp-2">{point.description}</p>
</div>
))}
</div>
@@ -163,3 +158,5 @@ export default function About() {
</Section>
);
}
+44 -31
View File
@@ -10,6 +10,8 @@ const WHATSAPP_NUMBER = "5547992742365";
export default function Contact() {
const shouldReduceMotion = useReducedMotion();
const fieldClassName =
"mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[13px] leading-6 text-white outline-none transition-[border-color,box-shadow] focus-visible:border-[var(--slash)] focus-visible:ring-2 focus-visible:ring-[rgba(204,255,0,0.34)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]";
const [formData, setFormData] = useState({
name: "",
email: "",
@@ -55,9 +57,9 @@ export default function Contact() {
return (
<Section
id="contact"
eyebrow="CONTACT_PROTOCOL.init"
title="Vamos discutir o proximo sistema"
description="Descreva o contexto, o gargalo ou o produto. O contato abre direto no WhatsApp com briefing estruturado."
eyebrow="Contact"
title="Consultoria tecnica para software, arquitetura e integracoes criticas"
description="Fale comigo se voce precisa de engenharia de software, consultoria tecnica, arquitetura de software ou apoio em projetos com Next.js, Tauri, Rust e IoT."
dark
disableChildrenAnimation
>
@@ -82,18 +84,22 @@ export default function Contact() {
whileTap={shouldReduceMotion ? undefined : { scale: 0.995 }}
transition={{ type: "spring", stiffness: 180, damping: 18 }}
>
<div className="ds-kicker">Response SLA</div>
<div className="ds-kicker">Availability</div>
<DrawLine delay={0.04} />
<h3 className="ds-card-title">Retorno inicial em ate 24h</h3>
<h3 className="ds-card-title">Resposta inicial em ate 24h</h3>
<p className="ds-copy">
Ideal para discovery tecnico, revisao de arquitetura, rescue de
projeto, otimizacao de performance ou construcao de produto do zero.
projeto, performance ou construcao de produto com ownership real.
</p>
<div className="mt-sp-6 space-y-sp-4 text-[12px] leading-7 text-[rgba(255,255,255,.7)]">
<div className="mt-sp-6 space-y-sp-4 text-[13px] leading-7 text-[rgba(255,255,255,.85)]">
<div>WhatsApp: +55 47 99274-2365</div>
<div>E-mail: evandrofonsecajunior@gmail.com</div>
<div>Base: Garuva, Santa Catarina | atuacao remota</div>
</div>
<div className="mt-sp-6 border-t border-[rgba(255,255,255,.08)] pt-sp-4 text-[12px] leading-6 text-[rgba(255,255,255,.75)]">
Envie contexto, prazo e objetivo. Eu cuido de transformar isso em
conversa tecnica objetiva.
</div>
</motion.div>
<motion.form
@@ -111,54 +117,60 @@ export default function Contact() {
transition={{ type: "spring", stiffness: 180, damping: 18 }}
>
<div>
<label htmlFor="contact-name" className="ds-kicker block">
<label
htmlFor="contact-name"
className="block text-[12px] font-medium text-[rgba(255,255,255,.85)]"
>
Nome completo
</label>
<input
id="contact-name"
name="name"
required
type="text"
autoComplete="name"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{
color: "#fff",
backgroundColor: "rgba(255,255,255,0.06)",
}}
className={fieldClassName}
/>
</div>
<div>
<label htmlFor="contact-email" className="ds-kicker block">
E-mail corporativo
<label
htmlFor="contact-email"
className="block text-[12px] font-medium text-[rgba(255,255,255,.85)]"
>
E-mail
</label>
<input
id="contact-email"
name="email"
required
type="email"
autoComplete="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{
color: "#fff",
backgroundColor: "rgba(255,255,255,0.06)",
}}
className={fieldClassName}
/>
</div>
<div>
<label htmlFor="contact-subject" className="ds-kicker block">
<label
htmlFor="contact-subject"
className="block text-[12px] font-medium text-[rgba(255,255,255,.85)]"
>
Tipo de projeto
</label>
<select
id="contact-subject"
name="subject"
value={formData.subject}
onChange={(e) =>
setFormData({ ...formData, subject: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
className={fieldClassName}
style={{ color: "#fff", backgroundColor: "#1a1a1a" }}
>
<option value="full-stack">Desenvolvimento Full-Stack</option>
@@ -168,33 +180,34 @@ export default function Contact() {
</select>
</div>
<div>
<label htmlFor="contact-message" className="ds-kicker block">
Detalhes do projeto
<label
htmlFor="contact-message"
className="block text-[12px] font-medium text-[rgba(255,255,255,.85)]"
>
Contexto do projeto
</label>
<textarea
id="contact-message"
name="message"
required
rows={6}
autoComplete="off"
value={formData.message}
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
className="mt-sp-2 w-full resize-none border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{
color: "#fff",
backgroundColor: "rgba(255,255,255,0.06)",
}}
className={`${fieldClassName} resize-none`}
/>
</div>
<motion.button
type="submit"
className="ds-link ds-link-accent border-0"
className="ds-link ds-link-accent min-h-12 border-0"
whileHover={shouldReduceMotion ? undefined : { scale: 1.03 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
Iniciar protocolo de contato
Enviar briefing pelo WhatsApp
</motion.button>
</motion.form>
</motion.div>
+18 -22
View File
@@ -1,11 +1,10 @@
"use client";
"use client";
import { useMemo } from "react";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import { CounterValue } from "./animations/CounterValue";
import { SectionLoading } from "./SectionLoading";
export default function Experience() {
@@ -24,11 +23,11 @@ export default function Experience() {
}, [data]);
if (loading) {
return <SectionLoading message="Loading system logs" />;
return <SectionLoading message="Loading selected experience" />;
}
if (error || !data) {
return <div className="ds-error">Error loading system logs</div>;
return <div className="ds-error">Error loading selected experience</div>;
}
const listVariants: Variants = {
@@ -58,9 +57,9 @@ export default function Experience() {
return (
<Section
id="experience"
eyebrow="SYSTEM_LOGS.history"
title="Trajetoria focada em produto, legado e crescimento sustentavel"
description="Experiencias recentes onde combinei entrega rapida com responsabilidade arquitetural."
eyebrow="Selected experience"
title="Experiencia em engenharia de software e sistemas criticos"
description="Projetos e contextos em que combinei arquitetura de software, integracoes complexas e entrega com responsabilidade operacional."
disableChildrenAnimation
>
<motion.div
@@ -86,33 +85,28 @@ export default function Experience() {
whileTap={shouldReduceMotion ? undefined : { scale: 0.995 }}
transition={{ type: "spring", stiffness: 170, damping: 20 }}
>
<div className="flex flex-col lg:grid lg:grid-cols-[1fr_auto] gap-sp-4 items-start">
<div>
<div className="flex flex-col gap-sp-4 lg:flex-row lg:items-start lg:justify-between">
<div className="max-w-[720px]">
<div className="ds-kicker">{exp.period}</div>
<DrawLine delay={0.04} />
<h3 className="ds-card-title">{exp.title}</h3>
<div className="mt-sp-3 text-[11px] uppercase tracking-[0.1em] text-meta">
{exp.company} | {exp.type}
</div>
</div>
<div className="flex flex-wrap items-start justify-start gap-sp-3 lg:justify-end">
<span className="ds-tag">{exp.location}</span>
<span className="ds-tag">
<CounterValue
value={exp.technologies.length}
suffix=" tecnologias"
/>
</span>
<p className="mt-sp-3 text-[13px] leading-6 text-[rgba(10,10,10,.64)] dark:text-[rgba(255,255,255,.6)]">
{exp.company} • {exp.type}
</p>
</div>
<div className="ds-metric">{exp.location}</div>
</div>
<p className="ds-copy">{exp.description}</p>
<ul className="ds-list">
{exp.achievements?.map((achievement) => (
<li key={achievement}>{achievement}</li>
))}
</ul>
<ul className="ds-tags mt-sp-6">
{exp.technologies?.map((tech) => (
{exp.technologies?.slice(0, 6).map((tech) => (
<li key={tech} className="ds-tag">
{tech}
</li>
@@ -124,3 +118,5 @@ export default function Experience() {
</Section>
);
}
+48 -33
View File
@@ -1,23 +1,10 @@
"use client";
/**
* Footer Component
*
* Displays contact information, social links, and copyright in a consistent footer layout.
* Follows the minimalist design system with Monument Extended typography and #ccff00 accent color.
*
* Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7
*/
// TypeScript interface for social media links
export interface SocialLink {
name: string; // Display name (e.g., "GitHub")
href: string; // Full URL
icon: string; // SVG path or icon identifier
ariaLabel: string; // Accessibility label
name: string;
href: string;
icon: string;
ariaLabel: string;
}
// Social links data
const socialLinks: SocialLink[] = [
{
name: "GitHub",
@@ -39,6 +26,16 @@ const socialLinks: SocialLink[] = [
},
];
const quickLinks = [
{ label: "Hub de servicos", href: "/servicos" },
{ label: "Cases", href: "/cases" },
{ label: "Consultoria Next.js", href: "/servicos/consultoria-nextjs" },
{
label: "Integracao hardware + software",
href: "/servicos/integracao-hardware-software-iot",
},
];
export default function Footer() {
const currentYear = new Date().getFullYear();
@@ -46,22 +43,26 @@ export default function Footer() {
<footer className="ds-section">
<div className="ds-section-shell">
<div className="ds-grid-2">
{/* Contact & Legal */}
<div className="ds-card flex flex-col justify-between">
<div>
<div className="ds-kicker">SYSTEM_INFO</div>
<div className="ds-kicker">Contato</div>
<span className="ds-draw-line" />
<h3 className="ds-card-title">
Transformando complexidade técnica em sistemas estáveis
Engenharia com menos ruido e mais clareza de execucao
</h3>
<p className="ds-copy max-w-[48ch]">
Se voce precisa de alguem para conectar produto, integracao e
entrega com mais criterio tecnico, este portfolio e o ponto de
partida.
</p>
<div className="mt-sp-6">
<p className="text-[12px] uppercase tracking-wider text-[var(--meta)] mb-2">
Contato
<p className="mb-2 text-[12px] tracking-[0.02em] text-[var(--meta)]">
E-mail direto
</p>
<a
href="mailto:[email protected]"
className="inline-block text-[18px] font-bold text-[var(--ink)] hover:text-[var(--slash)] transition-colors duration-200"
className="inline-block text-[18px] font-bold text-[var(--ink)] transition-colors duration-200 hover:text-[var(--slash)]"
>
evandro@dev.br
</a>
@@ -70,21 +71,20 @@ export default function Footer() {
<div className="mt-sp-8">
<div className="ds-metric">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-slash animate-pulse" />
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-slash" />
Garuva, SC | Remote worldwide
</div>
<p className="mt-sp-4 text-[12px] text-[var(--meta)]">
© {currentYear} Evandro Fonseca Junior. All rights reserved.
Copyright {currentYear} Evandro Fonseca Junior. All rights reserved.
</p>
</div>
</div>
{/* Social Links */}
<div className="ds-card flex flex-col justify-between">
<div>
<div className="ds-kicker">SOCIAL_LINKS</div>
<div className="ds-kicker">Presenca digital</div>
<span className="ds-draw-line" />
<h3 className="ds-card-title">Redes profissionais</h3>
<h3 className="ds-card-title">Canais profissionais</h3>
<div className="mt-sp-6 flex flex-col gap-sp-4">
{socialLinks.map((link) => (
@@ -94,27 +94,42 @@ export default function Footer() {
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
className="group flex items-center justify-between p-sp-4 border border-[var(--border)] bg-[rgba(10,10,10,0.02)] rounded-[8px] hover:bg-[rgba(10,10,10,0.04)] hover:border-[var(--slash)] transition-all duration-300"
className="group flex items-center justify-between rounded-[8px] border border-[var(--border)] bg-[rgba(10,10,10,0.02)] p-sp-4 transition-all duration-300 hover:border-[var(--slash)] hover:bg-[rgba(10,10,10,0.04)]"
>
<div className="flex items-center gap-sp-3">
<svg
className="w-5 h-5 text-[var(--meta)] group-hover:text-[var(--ink)] transition-colors"
className="h-5 w-5 text-[var(--meta)] transition-colors group-hover:text-[var(--ink)]"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d={link.icon} />
</svg>
<span className="text-[14px] font-medium text-[var(--ink-80)] group-hover:text-[var(--ink)] transition-colors">
<span className="text-[14px] font-medium text-[var(--ink-80)] transition-colors group-hover:text-[var(--ink)]">
{link.name}
</span>
</div>
<span className="text-[var(--slash)] opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300 font-bold">
→
<span className="text-[12px] tracking-[0.08em] text-[var(--meta)] transition-colors group-hover:text-[var(--slash)]">
abrir
</span>
</a>
))}
</div>
<div className="mt-sp-8 border-t border-[var(--border)] pt-sp-5">
<div className="ds-kicker">Explorar</div>
<div className="mt-sp-4 grid gap-sp-3 md:grid-cols-2">
{quickLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="rounded-[8px] border border-[var(--border)] bg-[rgba(10,10,10,0.02)] px-sp-4 py-sp-3 text-[13px] leading-6 text-[var(--ink-80)] transition-colors duration-200 hover:border-[var(--slash)] hover:text-[var(--ink)]"
>
{link.label}
</a>
))}
</div>
</div>
</div>
<div className="mt-sp-8 ds-metric">Open to collaboration</div>
+6 -2
View File
@@ -5,10 +5,14 @@ import Script from "next/script";
const GA_MEASUREMENT_ID = "G-QTT9QT0T3S";
export default function GoogleAnalytics() {
if (process.env.NODE_ENV !== "production") {
return null;
}
return (
<>
<Script src="/sys/libs/vendor-utils.js" strategy="afterInteractive" />
<Script id="google-analytics" strategy="afterInteractive">
<Script src="/sys/libs/vendor-utils.js" strategy="lazyOnload" />
<Script id="google-analytics" strategy="lazyOnload">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
+107 -117
View File
@@ -30,21 +30,27 @@ const SOCIAL_LINKS = [
},
];
const HERO_POINTS = [
"Produtos conectados, integracoes complexas e operacoes criticas.",
"Arquitetura mais clara para evoluir sem aumentar o caos tecnico.",
"Execucao ponta a ponta, do discovery a entrega em producao.",
];
const PROOF_ITEMS = [
{
label: "Especialidade",
label: "Quem eu ajudo",
value:
"Produtos conectados, interfaces de alta performance e sistemas criticos.",
"Empresas que dependem de integracoes, hardware ou fluxos criticos para operar com seguranca e previsibilidade.",
},
{
label: "Escopo",
label: "Como entrego",
value:
"Da integracao com hardware e APIs complexas ao frontend e operacao.",
"Traduzo complexidade tecnica em backlog claro, arquitetura legivel e decisao mais rapida para o time.",
},
{
label: "Formato",
label: "Por que conversar",
value:
"Consultoria, execucao ponta a ponta e colaboracao remota para squads.",
"Voce ganha um parceiro tecnico capaz de conectar produto, backend, interface e operacao sem perder contexto.",
},
];
@@ -73,39 +79,44 @@ export default function Hero() {
/>
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-[var(--ink)] to-transparent" />
<div className="relative z-10 mx-auto flex w-full max-w-[1240px] flex-col px-sp-6 py-sp-16 lg:px-sp-16 lg:py-0 pointer-events-none">
<div className="relative z-10 mx-auto flex w-full max-w-[1240px] flex-col px-sp-6 py-sp-16 lg:px-sp-16 lg:py-sp-12">
<motion.div
initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
className="grid items-end gap-sp-10 lg:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)] pointer-events-auto"
className="grid items-start gap-sp-8 lg:grid-cols-[minmax(0,1.1fr)_minmax(340px,0.9fr)] lg:gap-sp-10"
>
<div className="max-w-[720px]">
<div className="mb-sp-6 inline-flex items-center gap-sp-3 border border-[rgba(255,255,255,0.12)] bg-[rgba(10,10,10,0.4)] px-4 py-2 backdrop-blur-xl">
<motion.span
className="h-2 w-2 bg-[var(--slash)]"
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: [1, 0.25, 1] }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 1.5,
ease: "easeInOut",
repeat: Number.POSITIVE_INFINITY,
}
}
/>
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-[rgba(244,242,237,.82)] drop-shadow-md">
Available for selected builds {"//"} Remote worldwide
</span>
<div className="max-w-[760px]">
<div className="mb-sp-5 flex flex-wrap items-center gap-sp-3">
<div className="inline-flex items-center gap-sp-3 border border-[rgba(255,255,255,0.12)] bg-[rgba(10,10,10,0.42)] px-4 py-2 backdrop-blur-xl">
<motion.span
className="h-2 w-2 bg-[var(--slash)]"
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: [1, 0.25, 1] }
}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: 1.5,
ease: "easeInOut",
repeat: Number.POSITIVE_INFINITY,
}
}
/>
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.82)]">
Available for selected builds
</span>
</div>
<div className="inline-flex items-center gap-sp-2 border border-[rgba(255,255,255,0.08)] bg-[rgba(255,255,255,0.02)] px-4 py-2 text-[11px] text-[rgba(244,242,237,.72)] backdrop-blur-xl">
Produtos conectados, integracoes criticas e entrega ponta a ponta
</div>
</div>
<motion.p
className="mb-sp-4 max-w-[220px] font-mono text-[10px] uppercase tracking-[0.22em] text-[var(--slash)] drop-shadow-lg"
className="mb-sp-4 max-w-[540px] font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--slash)]"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 12 }
}
@@ -118,19 +129,18 @@ export default function Hero() {
ease: [0.16, 1, 0.3, 1],
}}
>
Software engineering for products, operations & critical flows
Evandro Fonseca Junior | Engenheiro de software full-stack
</motion.p>
<h1
className="max-w-[920px] text-balance font-display text-[clamp(2.9rem,7vw,6.4rem)] font-extrabold leading-[0.92] tracking-[-0.04em] text-transparent bg-clip-text bg-gradient-to-br from-[#ffffff] to-[#a3a3a3]"
aria-label="evandro.dev.br"
className="max-w-[920px] text-balance font-display text-[clamp(3rem,7vw,6.2rem)] font-extrabold leading-[0.9] tracking-[-0.055em] text-transparent bg-clip-text bg-gradient-to-br from-[#ffffff] to-[#a7a7a7]"
aria-label="Engenharia para sistemas que nao podem falhar"
>
Engenharia de software para produtos conectados, interfaces de
alta performance e sistemas criticos.
Engenharia de software para sistemas que nao podem falhar.
</h1>
<motion.p
className="mt-sp-6 max-w-[640px] text-[15px] leading-7 text-[rgba(244,242,237,.9)] lg:text-[16px] drop-shadow-md"
className="mt-sp-6 max-w-[680px] text-[16px] leading-8 text-[rgba(244,242,237,.88)] lg:text-[17px]"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 16 }
}
@@ -139,15 +149,40 @@ export default function Hero() {
}
transition={{
duration: 0.8,
delay: 0.16,
delay: 0.14,
ease: [0.16, 1, 0.3, 1],
}}
>
Da integracao com hardware e APIs complexas ao frontend moderno,
construo sistemas estaveis, operaveis e prontos para crescer sem
sacrificar clareza tecnica.
Ajudo empresas com produtos conectados, integracoes complexas e
operacoes criticas a transformar arquitetura confusa em software
estavel, mais rapido de evoluir e muito mais facil de operar.
</motion.p>
<motion.ul
className="mt-sp-8 grid gap-sp-3 sm:grid-cols-3"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 16 }
}
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
transition={{
duration: 0.8,
delay: 0.22,
ease: [0.16, 1, 0.3, 1],
}}
>
{HERO_POINTS.map((point) => (
<li
key={point}
className="flex min-w-0 gap-sp-3 rounded-[8px] border border-[rgba(255,255,255,0.08)] bg-[rgba(255,255,255,0.02)] px-sp-4 py-sp-4 text-[13px] leading-6 text-[rgba(244,242,237,.82)] backdrop-blur-sm"
>
<span className="mt-[8px] h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--slash)]" />
<span>{point}</span>
</li>
))}
</motion.ul>
<motion.div
className="mt-sp-8 flex flex-wrap items-center gap-sp-4"
initial={
@@ -158,32 +193,32 @@ export default function Hero() {
}
transition={{
duration: 0.8,
delay: 0.24,
delay: 0.3,
ease: [0.16, 1, 0.3, 1],
}}
>
<motion.a
href={`https://wa.me/5547992742365?text=${encodeURIComponent(greeting)}`}
className="inline-flex min-h-12 items-center justify-center bg-[var(--slash)] px-sp-8 font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-[#0A0A0A] no-underline transition-[transform,background-color,box-shadow] duration-300 hover:bg-white hover:shadow-[0_0_20px_rgba(204,255,0,0.4)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
className="inline-flex min-h-12 items-center justify-center bg-[var(--slash)] px-sp-8 font-mono text-[11px] font-bold uppercase tracking-[0.1em] text-[#0A0A0A] no-underline transition-[transform,background-color,box-shadow] duration-300 hover:bg-white hover:shadow-[0_0_20px_rgba(204,255,0,0.4)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
whileHover={shouldReduceMotion ? undefined : { scale: 1.02 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.98 }}
>
Iniciar projeto
Agendar conversa tecnica
</motion.a>
<motion.a
href="#projects"
className="inline-flex min-h-12 items-center justify-center border border-[rgba(244,242,237,.28)] bg-[rgba(10,10,10,0.4)] backdrop-blur-md px-sp-8 font-mono text-[11px] font-bold uppercase tracking-[0.12em] text-[#f4f2ed] no-underline transition-[transform,border-color,background-color,color] duration-300 hover:border-[var(--slash)] hover:bg-[rgba(204,255,0,0.08)] hover:text-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
href="/cases"
className="inline-flex min-h-12 items-center justify-center border border-[rgba(244,242,237,.24)] bg-[rgba(10,10,10,0.4)] px-sp-8 font-mono text-[11px] font-bold uppercase tracking-[0.1em] text-[#f4f2ed] no-underline transition-[transform,border-color,background-color,color] duration-300 hover:border-[var(--slash)] hover:bg-[rgba(204,255,0,0.08)] hover:text-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
whileHover={shouldReduceMotion ? undefined : { scale: 1.02 }}
whileTap={shouldReduceMotion ? undefined : { scale: 0.98 }}
>
Ver projetos
Ver casos selecionados
</motion.a>
<a
href="/assets/Evandro_Fonseca_Junior_CV.pdf"
download
className="inline-flex min-h-12 items-center justify-center px-sp-2 text-[12px] font-medium text-[rgba(244,242,237,.9)] no-underline transition-colors duration-300 hover:text-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)] drop-shadow-md"
className="inline-flex min-h-12 items-center justify-center px-sp-2 text-[12px] font-medium text-[rgba(244,242,237,.76)] no-underline transition-colors duration-300 hover:text-[var(--slash)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
>
Download CV
</a>
@@ -199,15 +234,15 @@ export default function Hero() {
}
transition={{
duration: 0.8,
delay: 0.32,
delay: 0.38,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="inline-flex items-center gap-sp-3 text-[11px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.72)]">
<div className="inline-flex items-center gap-sp-3 text-[11px] uppercase tracking-[0.08em] text-[rgba(244,242,237,.68)]">
<span className="h-2 w-2 rounded-full bg-[var(--slash)] shadow-[0_0_8px_var(--slash)]" />
Garuva, SC / Brasil
</div>
<div className="inline-flex items-center gap-sp-3 text-[11px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.72)]">
<div className="inline-flex items-center gap-sp-3 text-[11px] uppercase tracking-[0.08em] text-[rgba(244,242,237,.68)]">
<span className="h-[1px] w-6 bg-[rgba(255,255,255,.2)]" />
Next.js, Tauri, Rust, Node.js
</div>
@@ -222,7 +257,7 @@ export default function Hero() {
? "noopener noreferrer"
: undefined
}
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-[rgba(244,242,237,0.18)] bg-[rgba(10,10,10,0.4)] backdrop-blur-md text-[rgba(244,242,237,.88)] transition-[transform,border-color,color,background-color] duration-300 hover:border-[var(--slash)] hover:bg-[rgba(204,255,0,0.1)] hover:text-[var(--slash)] hover:shadow-[0_0_15px_rgba(204,255,0,0.2)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-[rgba(244,242,237,0.16)] bg-[rgba(10,10,10,0.34)] text-[rgba(244,242,237,.86)] transition-[transform,border-color,color,background-color] duration-300 hover:border-[var(--slash)] hover:bg-[rgba(204,255,0,0.1)] hover:text-[var(--slash)] hover:shadow-[0_0_15px_rgba(204,255,0,0.18)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
aria-label={link.label}
>
<svg
@@ -232,8 +267,7 @@ export default function Hero() {
fill={link.stroke ? "none" : "currentColor"}
stroke={link.stroke ? "currentColor" : "none"}
strokeWidth={link.stroke ? "2" : undefined}
role="img"
aria-label={link.label}
aria-hidden="true"
>
{link.icon}
</svg>
@@ -244,28 +278,26 @@ export default function Hero() {
</div>
<motion.aside
className="relative overflow-hidden border border-[rgba(255,255,255,0.15)] bg-[linear-gradient(180deg,rgba(10,10,10,0.6),rgba(10,10,10,0.3))] p-sp-6 backdrop-blur-xl lg:p-sp-8"
className="relative overflow-hidden border border-[rgba(255,255,255,0.12)] bg-[linear-gradient(180deg,rgba(10,10,10,0.58),rgba(10,10,10,0.36))] p-sp-6 backdrop-blur-xl lg:p-sp-8"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, x: 24 }
}
animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}
transition={{ duration: 0.9, delay: 0.22, ease: [0.16, 1, 0.3, 1] }}
>
<div className="absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(204,255,0,0.9),transparent)]" />
<div className="absolute inset-y-0 left-0 w-px bg-[linear-gradient(180deg,rgba(204,255,0,0.4),transparent)]" />
<div className="absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(204,255,0,0.8),transparent)]" />
<div className="flex items-start justify-between gap-sp-4 border-b border-[rgba(255,255,255,0.08)] pb-sp-5">
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--slash)]">
Execution Brief
</div>
<h2 className="mt-sp-2 font-display text-[clamp(1.35rem,3vw,2rem)] font-extrabold leading-[1.02] tracking-[-0.03em] text-[#f7f4ee]">
Clareza de arquitetura antes de complexidade.
</h2>
</div>
<div className="shrink-0 border border-[rgba(204,255,0,0.3)] bg-[rgba(204,255,0,0.05)] px-3 py-2 font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--slash)]">
2026
<div className="border-b border-[rgba(255,255,255,0.08)] pb-sp-5">
<div className="font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--slash)]">
Why this positioning works
</div>
<h2 className="mt-sp-3 font-display text-[clamp(1.35rem,3vw,2rem)] font-extrabold leading-[1.04] tracking-[-0.03em] text-[#f7f4ee]">
Menos atrito entre produto, engenharia e operacao.
</h2>
<p className="mt-sp-4 text-[14px] leading-7 text-[rgba(244,242,237,.78)]">
Entro onde a complexidade cresce e ajudo a devolver clareza,
ritmo de entrega e confiabilidade para o sistema e para o time.
</p>
</div>
<div className="mt-sp-6 space-y-sp-5">
@@ -281,68 +313,26 @@ export default function Hero() {
}
transition={{
duration: 0.65,
delay: 0.28 + index * 0.08,
delay: 0.3 + index * 0.08,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[rgba(244,242,237,.7)]">
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.56)]">
{item.label}
</div>
<p className="mt-sp-2 text-[14px] leading-6 text-[rgba(244,242,237,.94)]">
<p className="mt-sp-2 text-[14px] leading-7 text-[rgba(244,242,237,.92)]">
{item.value}
</p>
</motion.article>
))}
</div>
<motion.div
className="mt-sp-8 grid gap-sp-4 sm:grid-cols-2"
initial={
shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 16 }
}
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
transition={{
duration: 0.75,
delay: 0.48,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="border border-[rgba(255,255,255,0.1)] bg-[rgba(10,10,10,0.4)] p-sp-4 hover:border-[var(--slash)] transition-colors duration-300">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--slash)]">
Focus
</div>
<p className="mt-sp-2 text-[13px] leading-6 text-[rgba(244,242,237,.9)]">
Sistemas com baixa latencia, alta previsibilidade e manutencao
sustentavel.
</p>
</div>
<div className="border border-[rgba(255,255,255,0.1)] bg-[rgba(10,10,10,0.4)] p-sp-4 hover:border-[var(--slash)] transition-colors duration-300">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--slash)]">
Availability
</div>
<p className="mt-sp-2 text-[13px] leading-6 text-[rgba(244,242,237,.9)]">
Aberto a novos projetos com ownership tecnico, discovery e
entrega iterativa.
</p>
</div>
</motion.div>
</motion.aside>
</motion.div>
</div>
<motion.div
className="absolute bottom-10 left-1/2 flex -translate-x-1/2 flex-col items-center gap-3 opacity-50 z-10 pointer-events-none"
initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0 }}
animate={shouldReduceMotion ? { opacity: 0.5 } : { opacity: 0.5 }}
transition={{ delay: 1, duration: 1 }}
>
<span className="font-mono text-[9px] uppercase tracking-widest text-[var(--slash)]">
Scroll
</span>
<div className="h-10 w-[1px] bg-gradient-to-b from-[var(--slash)] to-transparent" />
</motion.div>
</section>
);
}
+33 -35
View File
@@ -5,7 +5,6 @@ import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import { CounterValue } from "./animations/CounterValue";
import { SectionLoading } from "./SectionLoading";
export default function Projects() {
@@ -18,11 +17,11 @@ export default function Projects() {
);
if (loading) {
return <SectionLoading dark message="Loading featured projects" />;
return <SectionLoading dark message="Loading selected work" />;
}
if (error) {
return <div className="ds-error">Error loading featured projects</div>;
return <div className="ds-error">Error loading selected work</div>;
}
const gridVariants: Variants = {
@@ -52,9 +51,9 @@ export default function Projects() {
return (
<Section
id="projects"
eyebrow="PRODUCAO_INTELECTUAL"
title="Projetos com recorte tecnico claro e impacto observavel"
description="Selecao de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
eyebrow="Selected work"
title="Projetos em Next.js, Tauri, Rust e integracao de sistemas"
description="Cases e produtos que mostram minha atuacao em engenharia de software, consultoria tecnica e construcao de sistemas de alta exigencia."
dark
disableChildrenAnimation
>
@@ -82,19 +81,29 @@ export default function Projects() {
transition={{ type: "spring", stiffness: 170, damping: 18 }}
>
<div className="flex items-start justify-between gap-sp-6">
<div>
<div className="max-w-[560px]">
<div className="ds-kicker">{project.category}</div>
<DrawLine delay={0.05} />
<h3 className="ds-card-title">{project.title}</h3>
<p className="mt-sp-2 text-[11px] uppercase tracking-[0.1em] text-[rgba(255,255,255,.36)]">
{project.subtitle}
</p>
{project.subtitle && (
<p className="mt-sp-3 text-[13px] leading-6 text-[rgba(255,255,255,.56)]">
{project.subtitle}
</p>
)}
</div>
<span className="ds-tag">{project.status}</span>
</div>
<p className="ds-copy">{project.description}</p>
<ul className="ds-tags mt-sp-6">
{project.technologies.slice(0, 5).map((tech: string) => (
<li key={tech} className="ds-tag">
{tech}
</li>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{project.githubUrl && (
<motion.a
@@ -106,7 +115,7 @@ export default function Projects() {
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
GitHub
Ver codigo
</motion.a>
)}
{project.demoUrl && (
@@ -119,36 +128,25 @@ export default function Projects() {
whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
Live demo
Abrir projeto
</motion.a>
)}
</div>
<ul className="ds-tags mt-sp-6">
{project.technologies.slice(0, 6).map((tech: string) => (
<li key={tech} className="ds-tag">
{tech}
</li>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-4">
<span className="ds-metric">
Complexity{" "}
<CounterValue
value={project.metrics?.complexity || 3}
suffix="/5"
/>
</span>
{project.metrics?.teamSize && (
<span className="ds-metric">
Team {project.metrics.teamSize}
</span>
)}
</div>
</motion.article>
))}
</motion.div>
<div className="mt-sp-8 flex flex-wrap gap-sp-3">
<a href="/cases" className="ds-link ds-link-accent">
Explorar todos os cases
</a>
<a href="/servicos" className="ds-link ds-link-muted">
Ver servicos relacionados
</a>
</div>
</Section>
);
}
+14 -15
View File
@@ -1,10 +1,9 @@
"use client";
"use client";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import { CounterValue } from "./animations/CounterValue";
import { SectionLoading } from "./SectionLoading";
export default function Skills() {
@@ -12,11 +11,11 @@ export default function Skills() {
const shouldReduceMotion = useReducedMotion();
if (loading) {
return <SectionLoading dark message="Loading skill registry" />;
return <SectionLoading dark message="Loading core stack" />;
}
if (error || !data) {
return <div className="ds-error">Error loading skill registry</div>;
return <div className="ds-error">Error loading core stack</div>;
}
const gridVariants: Variants = {
@@ -46,9 +45,9 @@ export default function Skills() {
return (
<Section
id="skills"
eyebrow="MODULE_REGISTRY.lib"
title="Stack modular para produto, operacao e escala"
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutencao."
eyebrow="Core stack"
title="Next.js, Tauri, Rust e stack para produtos criticos"
description="Tecnologias e fundamentos que uso em engenharia de software, arquitetura de software, IoT e integracoes complexas."
dark
disableChildrenAnimation
>
@@ -59,7 +58,7 @@ export default function Skills() {
viewport={{ once: true, amount: 0.2 }}
variants={gridVariants}
>
{data.skills.technical.map((category, index) => (
{data.skills.technical.map((category) => (
<motion.article
key={category.category}
className="ds-card h-full"
@@ -75,14 +74,12 @@ export default function Skills() {
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
transition={{ type: "spring", stiffness: 180, damping: 18 }}
>
<div className="flex items-center justify-between gap-sp-4">
<div className="ds-kicker">ID_0{index + 1}X</div>
<div className="ds-metric">
<CounterValue value={category.skills.length} suffix=" items" />
</div>
</div>
<h3 className="ds-card-title !mt-0">{category.category}</h3>
<DrawLine delay={0.05} />
<h3 className="ds-card-title mb-sp-8">{category.category}</h3>
<p className="ds-copy !mt-sp-4">
Ferramentas que uso para construir com clareza tecnica e ritmo de
entrega.
</p>
<ul className="ds-tags">
{category.skills.map((skill) => (
<li key={skill} className="ds-tag">
@@ -96,3 +93,5 @@ export default function Skills() {
</Section>
);
}
+63 -26
View File
@@ -1,6 +1,8 @@
"use client";
import Link from "next/link";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { serviceHubLinks } from "@/lib/site-content";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
@@ -87,8 +89,7 @@ function TierCard({ tier }: Readonly<{ tier: Tier }>) {
return (
<motion.article
tabIndex={0}
className={`ds-card h-full relative outline-none ${
className={`ds-card relative flex h-full min-w-0 flex-col overflow-hidden outline-none max-md:w-full max-md:max-w-none max-md:px-sp-5 max-md:py-sp-5 ${
tier.featured ? "border-[var(--slash)] shadow-sm" : ""
}`}
style={{
@@ -105,42 +106,50 @@ function TierCard({ tier }: Readonly<{ tier: Tier }>) {
whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}
transition={{ type: "spring", stiffness: 180, damping: 18 }}
>
<div className="flex justify-between items-start">
<span className="ds-kicker">{tier.badge}</span>
<span className="font-display text-[11px] font-extrabold tracking-widest text-[var(--meta)]">
<div className="flex min-w-0 items-start justify-between gap-sp-4">
<span className="ds-kicker max-md:text-[10px] max-md:leading-[1.6]">
{tier.badge}
</span>
<span className="text-[11px] tracking-[0.08em] text-[var(--meta)]">
{tier.level}
</span>
</div>
<DrawLine delay={0.04} />
<div className="mt-sp-4">
<h3 className="ds-card-title">{tier.title}</h3>
<p className="mt-sp-2 text-[11px] font-medium leading-relaxed text-[var(--meta)]">
<div className="mt-sp-4 min-w-0">
<h3 className="ds-card-title !mt-0 text-balance !text-[clamp(22px,5vw,28px)] !leading-[1.02] !tracking-[-0.035em] [overflow-wrap:break-word] [word-break:normal]">
{tier.title}
</h3>
<p className="mt-sp-2 text-[12px] leading-[1.7] text-[var(--meta)] md:text-[11px]">
{tier.tagline}
</p>
</div>
<p className="ds-copy">{tier.description}</p>
<p className="ds-copy !mt-sp-4 !text-[13px] !leading-[1.75] [overflow-wrap:break-word] [word-break:normal] md:!text-[12px]">
{tier.description}
</p>
<ul className="ds-list">
<ul className="ds-list !mt-sp-5 !pl-[1.15rem] !text-[13px] !leading-[1.75] [overflow-wrap:break-word] [word-break:normal] md:!text-[12px]">
{tier.deliverables.map((deliverable) => (
<li key={deliverable}>{deliverable}</li>
<li key={deliverable} className="pl-sp-1">
{deliverable}
</li>
))}
</ul>
<div className="mt-sp-6 border-t border-[var(--border)] pt-sp-4">
<div className="font-display text-[clamp(14px,1.5vw,18px)] font-extrabold tracking-tight">
<div className="mt-sp-6 space-y-sp-2 border-t border-[var(--border)] pt-sp-5">
<div className="font-display text-[clamp(17px,4.6vw,18px)] font-extrabold leading-none tracking-tight">
{tier.pricing}
</div>
<div className="mt-1 text-[10px] tracking-widest text-[var(--meta)] uppercase">
<div className="text-[12px] leading-[1.7] text-[var(--meta)]">
{tier.pricingNote}
</div>
</div>
<motion.a
href={tier.ctaHref}
className={`ds-link mt-sp-6 justify-center w-full ${
className={`ds-link mt-sp-6 min-h-[50px] w-full justify-center px-sp-4 py-sp-3 text-center text-[11px] leading-none ${
tier.featured ? "ds-link-accent" : "ds-link-muted"
}`}
whileHover={shouldReduceMotion ? undefined : { scale: 1.02 }}
@@ -148,7 +157,6 @@ function TierCard({ tier }: Readonly<{ tier: Tier }>) {
transition={{ type: "spring", stiffness: 260, damping: 20 }}
>
{tier.ctaLabel}
{" ->"}
</motion.a>
</motion.article>
);
@@ -184,25 +192,53 @@ export function WorkModalities() {
return (
<Section
id="modalities"
eyebrow="CONTRACT_PROTOCOLS.sys"
title="Engenharia sob demanda. Previsibilidade de entrega sem overhead."
description="Tres niveis de contratacao desenhados como uma esteira de valor, do diagnostico rapido a parceria estrategica de longo prazo."
eyebrow="Working models"
title="Servicos de engenharia de software e consultoria tecnica."
description="Formatos de contratacao para arquitetura de software, integracoes criticas, produtos conectados e evolucao de sistemas em producao."
disableChildrenAnimation
>
<div className="mb-sp-8 ds-card bg-[rgba(10,10,10,0.02)]">
<div className="flex flex-col gap-sp-4 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-[760px]">
<div className="ds-kicker">Paginas de servico</div>
<h3 className="ds-card-title !mt-sp-3">Especialidades organizadas por intencao de busca</h3>
<p className="ds-copy !mt-sp-3 max-w-[62ch]">
Se voce chegou buscando uma stack, um tipo de consultoria ou um contexto tecnico especifico,
aqui esta o caminho mais direto para a pagina certa.
</p>
</div>
<Link href="/servicos" className="ds-link ds-link-accent">
Ver hub de servicos
</Link>
</div>
<div className="mt-sp-6 grid gap-sp-3 md:grid-cols-2 xl:grid-cols-3">
{serviceHubLinks.map((service) => (
<Link
key={service.href}
href={service.href}
className="rounded-[8px] border border-[var(--border)] bg-white/70 px-sp-4 py-sp-4 transition-colors duration-200 hover:border-[var(--slash)] hover:bg-white"
>
<div className="text-[12px] font-semibold tracking-[0.02em] text-[var(--ink)]">
{service.label}
</div>
<div className="mt-sp-2 text-[12px] leading-6 text-[var(--meta)]">
{service.description}
</div>
</Link>
))}
</div>
</div>
<motion.div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "var(--sp-6)",
}}
className="max-md:grid-cols-1 max-md:!flex max-md:!flex-col"
className="grid grid-cols-1 gap-sp-4 md:grid-cols-2 md:gap-sp-5 lg:grid-cols-3 lg:gap-sp-6"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={containerVariants}
>
{TIERS.map((tier) => (
<motion.div key={tier.id} variants={itemVariants}>
<motion.div key={tier.id} variants={itemVariants} className="min-w-0">
<TierCard tier={tier} />
</motion.div>
))}
@@ -210,3 +246,4 @@ export function WorkModalities() {
</Section>
);
}
+5 -13
View File
@@ -21,7 +21,6 @@ export function LazyRevealImage({
}: Readonly<LazyRevealImageProps>) {
const shouldReduceMotion = useReducedMotion();
const [loaded, setLoaded] = useState(false);
const [animating, setAnimating] = useState(!shouldReduceMotion);
return (
<motion.div
@@ -29,28 +28,24 @@ export function LazyRevealImage({
initial={
shouldReduceMotion
? { opacity: 0 }
: { opacity: 0, filter: "blur(20px)", scale: 1.05 }
: { opacity: 0, y: 20, scale: 1.02 }
}
animate={
loaded
? shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, filter: "blur(0px)", scale: 1 }
: { opacity: 1, y: 0, scale: 1 }
: shouldReduceMotion
? { opacity: 0 }
: { opacity: 0, filter: "blur(20px)", scale: 1.05 }
: { opacity: 0, y: 20, scale: 1.02 }
}
transition={{
duration: shouldReduceMotion ? 0.25 : 0.75,
ease: [0.16, 1, 0.3, 1],
}}
style={{
willChange:
animating && !shouldReduceMotion
? "filter, transform, opacity"
: undefined,
willChange: !shouldReduceMotion ? "transform, opacity" : undefined,
}}
onAnimationComplete={() => setAnimating(false)}
>
<Image
src={src}
@@ -59,10 +54,7 @@ export function LazyRevealImage({
loading="lazy"
sizes={sizes}
placeholder={typeof src === "string" ? "empty" : "blur"}
onLoad={() => {
setAnimating(true);
setLoaded(true);
}}
onLoad={() => setLoaded(true)}
className={imageClassName}
/>
</motion.div>
+89 -72
View File
@@ -1,21 +1,3 @@
@import url("https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Geist+Mono:wght@300;400;500;600&display=swap");
@font-face {
font-family: "Monument Extended";
src: url("/fonts/MonumentExtended-Regular.otf") format("opentype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Monument Extended";
src: url("/fonts/MonumentExtended-Ultrabold.otf") format("opentype");
font-weight: 800;
font-style: normal;
font-display: swap;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -46,7 +28,7 @@
--white: #ffffff;
--slash: #ccff00;
--slash-dim: rgba(204, 255, 0, 0.16);
--meta: #777;
--meta: #595959;
--border: rgba(10, 10, 10, 0.1);
--border-light: rgba(10, 10, 10, 0.06);
--sp-1: 4px;
@@ -65,9 +47,9 @@
--r-md: 8px;
--r-lg: 16px;
--r-full: 999px;
--font-display: "Syne", sans-serif;
--font-hero: "Monument Extended", sans-serif;
--font-mono: "Geist Mono", monospace;
--font-display: var(--font-syne), sans-serif;
--font-hero: var(--font-monument-extended), sans-serif;
--font-mono: var(--font-geist-mono), monospace;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
}
@@ -88,8 +70,8 @@ body {
background: var(--paper);
color: var(--ink);
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
font-size: 14px;
line-height: 1.68;
overflow-x: hidden;
}
@@ -124,11 +106,11 @@ textarea {
}
.ds-main {
/* padding-top: 52px; removed since there is no header now */
/* no top padding needed */
}
.ds-section {
padding: var(--sp-20) var(--sp-16);
padding: clamp(72px, 9vw, 112px) clamp(24px, 6vw, 64px);
border-bottom: 1px solid var(--border);
}
@@ -153,18 +135,18 @@ textarea {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--sp-8);
margin-bottom: var(--sp-12);
gap: var(--sp-6);
margin-bottom: var(--sp-10);
}
.ds-section-meta {
max-width: 700px;
max-width: 760px;
}
.ds-eyebrow,
.ds-kicker {
font-size: 9px;
letter-spacing: 0.16em;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--meta);
}
@@ -175,16 +157,17 @@ textarea {
.ds-section-title {
font-family: var(--font-display);
font-size: clamp(24px, 4vw, 40px);
line-height: 1.05;
font-size: clamp(28px, 4vw, 44px);
line-height: 1.02;
font-weight: 800;
letter-spacing: -0.03em;
letter-spacing: -0.035em;
}
.ds-section-description {
color: var(--meta);
font-size: 11px;
line-height: 1.8;
max-width: 58ch;
color: rgba(10, 10, 10, 0.62);
font-size: 14px;
line-height: 1.82;
}
.ds-grid-2,
@@ -209,14 +192,15 @@ textarea {
}
.ds-card {
background: rgba(255, 255, 255, 0.55);
padding: var(--sp-6);
background: rgba(255, 255, 255, 0.74);
padding: clamp(20px, 3vw, 28px);
display: flex;
flex-direction: column;
backdrop-filter: blur(10px);
}
.ds-panel {
background: rgba(10, 10, 10, 0.02);
background: rgba(10, 10, 10, 0.03);
padding: var(--sp-5);
display: flex;
flex-direction: column;
@@ -224,16 +208,16 @@ textarea {
.ds-card-title {
font-family: var(--font-display);
font-size: clamp(18px, 2.4vw, 28px);
line-height: 1.1;
letter-spacing: -0.02em;
margin-top: var(--sp-3);
font-size: clamp(20px, 2.4vw, 30px);
line-height: 1.08;
letter-spacing: -0.025em;
margin-top: var(--sp-4);
}
.ds-copy {
margin-top: var(--sp-4);
color: var(--ink-80);
font-size: 12px;
font-size: 14px;
line-height: 1.8;
flex-grow: 1;
}
@@ -241,27 +225,30 @@ textarea {
.ds-tags {
display: flex;
flex-wrap: wrap;
gap: var(--sp-3);
gap: 10px;
list-style: none;
margin-top: auto;
padding-top: var(--sp-8);
padding-top: var(--sp-6);
}
.ds-tag {
padding: 8px 12px;
border: 1px solid var(--border);
border: 1px solid rgba(10, 10, 10, 0.12);
background: rgba(10, 10, 10, 0.03);
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.04em;
text-transform: none;
font-weight: 500;
white-space: normal;
word-break: normal;
overflow-wrap: break-word;
}
.ds-metric {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
color: var(--meta);
color: rgba(10, 10, 10, 0.65);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
@@ -272,8 +259,8 @@ textarea {
align-items: center;
gap: var(--sp-2);
text-decoration: none;
font-size: 10px;
letter-spacing: 0.12em;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
transition:
transform 0.2s ease,
@@ -297,10 +284,10 @@ textarea {
}
.ds-list {
margin-top: var(--sp-6);
padding-left: var(--sp-5);
font-size: 12px;
line-height: 1.8;
margin-top: var(--sp-5);
padding-left: 18px;
font-size: 14px;
line-height: 1.85;
flex-grow: 1;
}
@@ -310,31 +297,31 @@ textarea {
.ds-loading,
.ds-error {
padding: var(--sp-10) var(--sp-16);
padding: var(--sp-10) clamp(24px, 6vw, 64px);
font-size: 11px;
letter-spacing: 0.14em;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.ds-section-dark .ds-eyebrow,
.ds-section-dark .ds-kicker,
.ds-section-dark .ds-metric {
color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.65);
}
.ds-section-dark .ds-section-description {
color: rgba(255, 255, 255, 0.38);
color: rgba(255, 255, 255, 0.75);
}
.ds-section-dark .ds-card,
.ds-section-dark .ds-panel {
background: rgba(255, 255, 255, 0.02);
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.08);
}
.ds-section-dark .ds-copy,
.ds-section-dark .ds-list {
color: rgba(255, 255, 255, 0.68);
color: rgba(255, 255, 255, 0.74);
}
.ds-section-dark .ds-tag {
@@ -350,19 +337,19 @@ textarea {
display: block;
width: 100%;
height: 1px;
margin-top: var(--sp-5);
margin-top: var(--sp-4);
background: linear-gradient(
90deg,
rgba(10, 10, 10, 0.55) 0%,
rgba(10, 10, 10, 0.08) 100%
rgba(10, 10, 10, 0.38) 0%,
rgba(10, 10, 10, 0.06) 100%
);
}
.ds-section-dark .ds-draw-line {
background: linear-gradient(
90deg,
rgba(204, 255, 0, 0.75) 0%,
rgba(204, 255, 0, 0.1) 100%
rgba(204, 255, 0, 0.5) 0%,
rgba(204, 255, 0, 0.08) 100%
);
}
@@ -453,14 +440,44 @@ textarea {
scroll-behavior: auto !important;
}
}
@media (max-width: 1024px) {
.ds-section {
padding: var(--sp-16) var(--sp-6);
.ds-section-header {
flex-direction: column;
gap: var(--sp-4);
}
.ds-section-header,
.ds-grid-2,
.ds-grid-4 {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.ds-section {
padding: 72px 20px;
}
.ds-section-title {
font-size: clamp(26px, 8vw, 34px);
}
.ds-section-description,
.ds-copy,
.ds-list {
font-size: 13px;
line-height: 1.75;
}
.ds-tags {
gap: 8px;
padding-top: var(--sp-5);
}
.ds-tag {
font-size: 10px;
padding: 7px 10px;
}
}
+175 -64
View File
@@ -1,53 +1,88 @@
import type { Metadata, Viewport } from "next";
import { Geist_Mono, Syne } from "next/font/google";
import localFont from "next/font/local";
import type { ReactNode } from "react";
import GoogleAnalytics from "./components/GoogleAnalytics";
import { RouteTransition } from "./components/RouteTransition";
import { SmoothScrollProvider } from "./providers/SmoothScrollProvider";
import { AnimationProvider } from "./providers/AnimationProvider";
import { SmoothScrollProvider } from "./providers/SmoothScrollProvider";
import "./globals.css";
const siteUrl = "https://evandro.dev.br";
const seoTitle =
"Engenharia de Software, Next.js, Tauri, Rust e Consultoria Tecnica | Evandro Fonseca Junior";
const seoDescription =
"Consultoria e engenharia de software para Next.js, Tauri, Rust, IoT e sistemas criticos. Arquitetura de software e integracoes hardware + software para empresas que precisam de estabilidade e escala.";
const syne = Syne({
subsets: ["latin"],
weight: ["400", "500", "600", "700", "800"],
display: "swap",
variable: "--font-syne",
});
const geistMono = Geist_Mono({
subsets: ["latin"],
weight: ["400", "500", "600"],
display: "swap",
variable: "--font-geist-mono",
});
const monumentExtended = localFont({
src: [
{
path: "../public/fonts/MonumentExtended-Regular.otf",
weight: "400",
style: "normal",
},
{
path: "../public/fonts/MonumentExtended-Ultrabold.otf",
weight: "800",
style: "normal",
},
],
display: "swap",
variable: "--font-monument-extended",
});
export const metadata: Metadata = {
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicacoes de alta performance e sistemas criticos. Desenvolvimento com Next.js, React, Tauri e arquitetura escalavel.",
title: seoTitle,
description: seoDescription,
keywords: [
"engenheiro full-stack",
"engenharia de software",
"engenheiro de software",
"Next.js",
"React",
"Tauri",
"Rust",
"Python",
"TypeScript",
"PostgreSQL",
"MySQL",
"cloud computing",
"IoT",
"automacao",
"consultoria tecnica",
"consultoria next.js",
"next.js",
"tauri",
"rust",
"arquitetura de software",
"sistemas escalaveis",
"high performance",
"sistemas criticos",
"integracao hardware software",
"iot",
"software para iot",
"engenharia full-stack",
"aplicacoes de alta performance",
],
authors: [{ name: "Evandro Fonseca Junior" }],
creator: "Evandro Fonseca Junior",
publisher: "Evandro Fonseca Junior",
applicationName: "Evandro.dev.br",
category: "Technology",
metadataBase: new URL(siteUrl),
alternates: {
canonical: "/",
},
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL("https://evandro.dev.br"),
alternates: {
canonical: "/",
},
openGraph: {
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicacoes de alta performance e sistemas criticos. Desenvolvimento com Next.js, React, Tauri e arquitetura escalavel.",
url: "https://evandro.dev.br",
siteName: "Evandro Fonseca Junior",
title: seoTitle,
description: seoDescription,
url: siteUrl,
siteName: "Evandro.dev.br",
locale: "pt_BR",
type: "website",
images: [
@@ -55,16 +90,14 @@ export const metadata: Metadata = {
url: "/opengraph-image",
width: 1200,
height: 630,
alt: "Evandro Fonseca Junior - Engenheiro de Software",
alt: "Evandro Fonseca Junior | Engenharia de Software, Next.js, Tauri e Rust",
},
],
},
twitter: {
card: "summary_large_image",
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicacoes de alta performance e sistemas criticos.",
title: seoTitle,
description: seoDescription,
creator: "@evandrodevbr",
images: ["/opengraph-image"],
},
@@ -95,41 +128,119 @@ interface RootLayoutProps {
export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "ProfilePage",
mainEntity: {
"@type": "Person",
name: "Evandro Fonseca Junior",
alternateName: "Evandro.dev",
jobTitle: "Engenheiro de Software",
url: "https://evandro.dev.br",
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
knowsAbout: [
"Software Engineering",
"Next.js",
"React",
"Tauri",
"Rust",
"TypeScript",
"System Architecture",
"Cloud Computing",
"IoT",
],
worksFor: {
"@type": "Organization",
name: "Freelancer / Consultor Independente",
"@graph": [
{
"@type": "WebSite",
"@id": `${siteUrl}/#website`,
url: siteUrl,
name: "Evandro.dev.br",
inLanguage: "pt-BR",
description: seoDescription,
},
description:
"Engenheiro de software especializado em aplicacoes de alta performance e sistemas criticos. Desenvolvimento com Next.js 15, React 19, Tauri e arquitetura escalavel.",
},
{
"@type": "Person",
"@id": `${siteUrl}/#person`,
name: "Evandro Fonseca Junior",
alternateName: ["Evandro.dev", "evandrodevbr"],
jobTitle: "Engenheiro de Software Full-Stack",
description: seoDescription,
url: siteUrl,
image: `${siteUrl}/opengraph-image`,
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"https://www.instagram.com/evandrodevbr/",
],
worksFor: {
"@type": "Organization",
name: "Consultoria Independente",
},
homeLocation: {
"@type": "Place",
name: "Garuva, Santa Catarina, Brasil",
},
knowsAbout: [
"Engenharia de Software",
"Next.js",
"Tauri",
"Rust",
"Arquitetura de Software",
"Sistemas Criticos",
"Integracao hardware e software",
"IoT",
"TypeScript",
"Cloud Computing",
],
},
{
"@type": "ProfessionalService",
"@id": `${siteUrl}/#services`,
name: "Evandro Fonseca Junior | Engenharia de Software e Consultoria Tecnica",
url: siteUrl,
description:
"Servico de engenharia de software, arquitetura de software, integracoes criticas e consultoria tecnica para Next.js, Tauri, Rust, IoT e produtos conectados.",
areaServed: [
{
"@type": "Country",
name: "Brasil",
},
{
"@type": "Place",
name: "Remote worldwide",
},
],
provider: {
"@id": `${siteUrl}/#person`,
},
serviceType: [
"Engenharia de Software",
"Consultoria Tecnica",
"Arquitetura de Software",
"Integracao hardware + software",
"Desenvolvimento Next.js",
"Desenvolvimento Tauri",
"Desenvolvimento Rust",
"IoT",
"Sistemas Criticos",
],
hasOfferCatalog: {
"@type": "OfferCatalog",
name: "Modelos de contratacao",
itemListElement: [
{
"@type": "Offer",
itemOffered: {
"@type": "Service",
name: "Auditoria e diagnostico tecnico",
},
},
{
"@type": "Offer",
itemOffered: {
"@type": "Service",
name: "Ciclos de engenharia",
},
},
{
"@type": "Offer",
itemOffered: {
"@type": "Service",
name: "Fractional Tech Lead",
},
},
],
},
},
],
};
return (
<html lang="pt-BR">
<html
lang="pt-BR"
className={`${syne.variable} ${geistMono.variable} ${monumentExtended.variable}`}
>
<head>
<link rel="canonical" href="https://evandro.dev.br" />
<link rel="canonical" href={siteUrl} />
</head>
<body>
<SmoothScrollProvider>
+6 -7
View File
@@ -1,16 +1,14 @@
"use client";
import dynamic from "next/dynamic";
import { Suspense } from "react";
import About from "./components/About";
import Contact from "./components/Contact";
import Experience from "./components/Experience";
import Footer from "./components/Footer";
import Hero from "./components/Hero";
import { SectionLoading } from "./components/SectionLoading";
import Skills from "./components/Skills";
import { WorkModalities } from "./components/WorkModalities";
const About = dynamic(() => import("./components/About"));
const Skills = dynamic(() => import("./components/Skills"));
const Experience = dynamic(() => import("./components/Experience"));
const WorkModalities = dynamic(() => import("./components/WorkModalities").then(mod => mod.WorkModalities));
const Contact = dynamic(() => import("./components/Contact"));
const Projects = dynamic(() => import("./components/Projects"));
export default function Home() {
@@ -33,3 +31,4 @@ export default function Home() {
</div>
);
}
+50 -21
View File
@@ -1,40 +1,69 @@
"use client";
import type { ReactNode } from "react";
import { useEffect, useRef } from "react";
import Lenis from "lenis";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export function SmoothScrollProvider({
children,
}: {
children: React.ReactNode;
children: ReactNode;
}) {
const lenisRef = useRef<Lenis | null>(null);
const teardownRef = useRef<() => void>(() => {});
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // Expo ease
smoothWheel: true,
});
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
const hasCoarsePointer = window.matchMedia("(pointer: coarse)").matches;
lenisRef.current = lenis;
if (prefersReducedMotion || hasCoarsePointer) {
return undefined;
}
// Sincroniza Lenis com GSAP ScrollTrigger — critico para evitar dessync
lenis.on("scroll", ScrollTrigger.update);
let isMounted = true;
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
const setupSmoothScroll = async () => {
const [{ default: Lenis }, { gsap }, { ScrollTrigger }] =
await Promise.all([
import("lenis"),
import("gsap"),
import("gsap/ScrollTrigger"),
]);
gsap.ticker.lagSmoothing(0);
if (!isMounted) {
return;
}
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true,
});
const handleScroll = () => ScrollTrigger.update();
const tick = (time: number) => {
lenis.raf(time * 1000);
};
lenis.on("scroll", handleScroll);
gsap.ticker.add(tick);
gsap.ticker.lagSmoothing(0);
teardownRef.current = () => {
lenis.off?.("scroll", handleScroll);
gsap.ticker.remove(tick);
lenis.destroy();
};
};
void setupSmoothScroll();
return () => {
lenis.destroy();
gsap.ticker.remove((time) => lenis.raf(time * 1000));
isMounted = false;
teardownRef.current();
teardownRef.current = () => {};
};
}, []);
+4 -3
View File
@@ -1,14 +1,15 @@
import { MetadataRoute } from "next";
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/admin/"],
allow: ["/", "/_next/static/", "/_next/image/"],
disallow: ["/api/", "/admin/"],
},
],
sitemap: "https://evandro.dev.br/sitemap.xml",
host: "https://evandro.dev.br",
};
}
+282
View File
@@ -0,0 +1,282 @@
import Link from "next/link";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
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: `Servico nao encontrado | ${siteName}`,
};
}
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": [
{
"@type": "Service",
"@id": `${pageUrl}#service`,
name: service.navLabel,
description: service.metaDescription,
serviceType: service.navLabel,
provider: {
"@type": "Person",
name: "Evandro Fonseca Junior",
url: siteUrl,
},
areaServed: [
{
"@type": "Country",
name: "Brasil",
},
{
"@type": "Place",
name: "Remote worldwide",
},
],
url: pageUrl,
},
{
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Inicio",
item: siteUrl,
},
{
"@type": "ListItem",
position: 2,
name: "Servicos",
item: `${siteUrl}/servicos`,
},
{
"@type": "ListItem",
position: 3,
name: service.navLabel,
item: pageUrl,
},
],
},
],
};
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<div className="max-w-[860px]">
<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.intro}
</p>
<div className="mt-sp-8 flex flex-wrap gap-sp-3">
<a href={service.ctaHref} className="ds-link ds-link-accent">
{service.ctaLabel}
</a>
<Link href="/cases" className="ds-link ds-link-muted">
Ver casos relacionados
</Link>
</div>
</div>
</div>
</section>
<section className="ds-section">
<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 servico encaixa melhor</h2>
<ul className="ds-list mt-sp-5 pl-[1.15rem]">
{service.audience.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</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]">
{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="ds-card">
<div className="ds-kicker">Como eu estruturo</div>
<h2 className="ds-card-title">Processo de trabalho</h2>
<ol className="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>
{step}
</li>
))}
</ol>
</article>
<article className="ds-card">
<div className="ds-kicker">Stack e repertorio</div>
<h2 className="ds-card-title">Tecnologias e capacidades envolvidas</h2>
<p className="ds-copy">
A stack entra como meio para resolver o problema certo, com criterio de manutencao,
observabilidade e evolucao do produto.
</p>
<ul className="ds-tags">
{service.stack.map((item) => (
<li key={item} className="ds-tag">
{item}
</li>
))}
</ul>
</article>
</div>
</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 que reforcam esse tipo de entrega</h2>
<p className="ds-section-description">
Alguns exemplos de contexto tecnico em que esse servico se conecta a entregas reais.
</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">
{item}
</li>
))}
</ul>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{caseStudy.demoUrl ? (
<a href={caseStudy.demoUrl} className="ds-link ds-link-accent" target="_blank" rel="noreferrer">
Abrir projeto
</a>
) : null}
{caseStudy.githubUrl ? (
<a href={caseStudy.githubUrl} className="ds-link ds-link-muted" target="_blank" rel="noreferrer">
Ver codigo
</a>
) : null}
</div>
</article>
))}
</div>
</div>
</section>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<div className="ds-card border-white/10 bg-[rgba(255,255,255,0.03)]">
<div className="ds-kicker">Proximo passo</div>
<h2 className="ds-card-title text-white">
Se esse servico parece aderente ao seu contexto, a conversa inicial ja pode ser objetiva.
</h2>
<p className="ds-copy !text-[rgba(255,255,255,0.76)]">
Traga o sistema, o gargalo e o objetivo de negocio. Eu estruturo a leitura tecnica e
indico o caminho mais seguro para avancar.
</p>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
<a href={service.ctaHref} className="ds-link ds-link-accent">
{service.ctaLabel}
</a>
<Link href="/servicos" className="ds-link ds-link-muted">
Ver todos os servicos
</Link>
</div>
</div>
</div>
</section>
</main>
);
}
+88
View File
@@ -0,0 +1,88 @@
import Link from "next/link";
import type { Metadata } from "next";
import { serviceHubLinks, servicePages, siteName, siteUrl } from "@/lib/site-content";
export const metadata: Metadata = {
title: `Servicos de Engenharia de Software e Consultoria Tecnica | ${siteName}`,
description:
"Servicos de engenharia de software, consultoria Next.js, Tauri, Rust, arquitetura de software e integracao hardware + software para produtos e sistemas criticos.",
alternates: {
canonical: `${siteUrl}/servicos`,
},
openGraph: {
title: `Servicos de Engenharia de Software e Consultoria Tecnica | ${siteName}`,
description:
"Servicos de engenharia de software, consultoria Next.js, Tauri, Rust, arquitetura de software e integracao hardware + software para produtos e sistemas criticos.",
url: `${siteUrl}/servicos`,
siteName,
type: "website",
},
};
export default function ServicesIndexPage() {
const jsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "CollectionPage",
"@id": `${siteUrl}/servicos#page`,
name: "Servicos de Engenharia de Software e Consultoria Tecnica",
url: `${siteUrl}/servicos`,
description:
"Hub de servicos para engenharia de software, consultoria tecnica, Next.js, Tauri, Rust, IoT e arquitetura de software.",
},
{
"@type": "ItemList",
itemListElement: servicePages.map((service, index) => ({
"@type": "ListItem",
position: index + 1,
url: `${siteUrl}/servicos/${service.slug}`,
name: service.navLabel,
})),
},
],
};
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<section className="ds-section ds-section-dark">
<div className="ds-section-shell">
<div className="max-w-[860px]">
<p className="ds-eyebrow">Hub de servicos</p>
<h1 className="ds-section-title max-w-[14ch] text-balance text-white">
Servicos de engenharia de software para contextos tecnicos de alta exigencia.
</h1>
<p className="ds-copy max-w-[64ch] !text-[rgba(255,255,255,0.78)]">
Organizei as paginas de servico por intencao real de compra: engenharia de software,
consultoria Next.js, Tauri e Rust, arquitetura de software e integracoes hardware + software.
</p>
</div>
</div>
</section>
<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">Servico especializado</div>
<h2 className="ds-card-title">{service.label}</h2>
<p className="ds-copy">{service.description}</p>
<div className="mt-sp-6">
<Link href={service.href} className="ds-link ds-link-accent">
Abrir pagina
</Link>
</div>
</article>
))}
</div>
</div>
</section>
</main>
);
}
+15 -27
View File
@@ -1,45 +1,33 @@
import { MetadataRoute } from "next";
import { servicePages, siteUrl } from "@/lib/site-content";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = "https://evandro.dev.br";
const currentDate = new Date().toISOString();
const now = new Date();
return [
{
url: baseUrl,
lastModified: currentDate,
url: siteUrl,
lastModified: now,
changeFrequency: "monthly",
priority: 1.0,
},
{
url: `${baseUrl}/#about`,
lastModified: currentDate,
url: `${siteUrl}/servicos`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
priority: 0.9,
},
{
url: `${baseUrl}/#skills`,
lastModified: currentDate,
url: `${siteUrl}/cases`,
lastModified: now,
changeFrequency: "monthly",
priority: 0.8,
priority: 0.9,
},
{
url: `${baseUrl}/#experience`,
lastModified: currentDate,
changeFrequency: "monthly",
...servicePages.map((service) => ({
url: `${siteUrl}/servicos/${service.slug}`,
lastModified: now,
changeFrequency: "monthly" as const,
priority: 0.8,
},
{
url: `${baseUrl}/#projects`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/#contact`,
lastModified: currentDate,
changeFrequency: "monthly",
priority: 0.8,
},
})),
];
}