feat: Initialize portfolio website with core sections, smooth scrolling, scroll reveal animations, and comprehensive SEO.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-07 15:39:38 -03:00
1 parent 6299ae040a
commit d54462c687
15 files changed
+433 -138

No files matched your search

+31 -26
View File
@@ -1,5 +1,5 @@
import { Section } from "./Section";
import { Reveal } from "./Reveal";
const STORY_DATA = [
{
id: "KERNEL_INIT",
@@ -45,6 +45,7 @@ export default function About() {
eyebrow="WHOAMI.sys"
title="Engenharia orientada por clareza, não por ruído"
description="Transformo complexidade técnica em sistemas estáveis, operáveis e alinhados ao negócio."
disableChildrenAnimation
aside={
<div className="mt-sp-4 ds-metric">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-slash" />
@@ -54,36 +55,40 @@ export default function About() {
>
<div className="ds-grid-2">
<div className="ds-grid-2">
{STORY_DATA.map((step) => (
<article key={step.id} className="ds-card">
<div className="ds-kicker">{step.id}</div>
<h3 className="ds-card-title">{step.title}</h3>
<p className="ds-copy">{step.content}</p>
</article>
{STORY_DATA.map((step, index) => (
<Reveal key={step.id} y={30} duration={0.8} delay={0.1 * index}>
<article className="ds-card h-full">
<div className="ds-kicker">{step.id}</div>
<h3 className="ds-card-title">{step.title}</h3>
<p className="ds-copy">{step.content}</p>
</article>
</Reveal>
))}
</div>
<div className="ds-card">
<div className="ds-kicker">System Signature</div>
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
<p className="ds-copy">
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js,
React, Node.js, Tauri, integrações e arquitetura pragmática.
</p>
<div className="ds-grid-2 mt-sp-6">
{VALUES_DATA.map((value) => (
<div
key={value.title}
className="border-t border-[var(--border)] pt-sp-4"
>
<div className="text-[11px] font-medium uppercase tracking-[0.08em]">
{value.title}
<Reveal y={30} duration={0.8} delay={0.2}>
<div className="ds-card h-full">
<div className="ds-kicker">System Signature</div>
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
<p className="ds-copy">
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js,
React, Node.js, Tauri, integrações e arquitetura pragmática.
</p>
<div className="ds-grid-2 mt-sp-6">
{VALUES_DATA.map((value) => (
<div
key={value.title}
className="border-t border-[var(--border)] pt-sp-4"
>
<div className="text-[11px] font-medium uppercase tracking-[0.08em]">
{value.title}
</div>
<p className="ds-copy !mt-sp-2">{value.description}</p>
</div>
<p className="ds-copy !mt-sp-2">{value.description}</p>
</div>
))}
))}
</div>
</div>
</div>
</Reveal>
</div>
</Section>
);
+33 -29
View File
@@ -3,6 +3,7 @@
import { useMemo } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { Reveal } from "./Reveal";
export default function Experience() {
const { data, loading, error } = usePortfolioData();
@@ -28,39 +29,42 @@ export default function Experience() {
eyebrow="SYSTEM_LOGS.history"
title="Trajetória focada em produto, legado e crescimento sustentável"
description="Experiências recentes onde combinei entrega rápida com responsabilidade arquitetural."
disableChildrenAnimation
>
<div className="space-y-sp-6">
{sortedExperiences.map((exp) => (
<article key={exp.id} className="ds-card">
<div className="ds-grid-2">
<div>
<div className="ds-kicker">{exp.period}</div>
<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}
{sortedExperiences.map((exp, index) => (
<Reveal key={exp.id} y={50} duration={0.8} delay={0.1 * index}>
<article className="ds-card">
<div className="ds-grid-2">
<div>
<div className="ds-kicker">{exp.period}</div>
<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">
{exp.technologies.length} tecnologias
</span>
</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">
{exp.technologies.length} tecnologias
</span>
</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) => (
<li key={tech} className="ds-tag">
{tech}
</li>
))}
</ul>
</article>
<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) => (
<li key={tech} className="ds-tag">
{tech}
</li>
))}
</ul>
</article>
</Reveal>
))}
</div>
</Section>
+1 -1
View File
@@ -150,7 +150,7 @@ export default function Hero() {
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, ease: "easeOut" }}
transition={{ duration: 0.8, ease: "easeOut", delay: 3.2 }}
>
<div className="mb-sp-8 inline-flex items-center gap-sp-2">
<span
+93
View File
@@ -0,0 +1,93 @@
"use client";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
export function Preloader() {
const [progress, setProgress] = useState(0);
const [complete, setComplete] = useState(false);
useEffect(() => {
document.body.style.overflow = "hidden";
window.scrollTo(0, 0);
const duration = 2500;
const interval = 25;
const steps = duration / interval;
let currentStep = 0;
const timer = setInterval(() => {
currentStep++;
const newProgress = Math.min(Math.floor((currentStep / steps) * 100), 100);
setProgress(newProgress);
if (currentStep >= steps) {
clearInterval(timer);
setTimeout(() => {
setComplete(true);
setTimeout(() => {
document.body.style.overflow = "auto";
}, 1000); // Wait for exit animation
}, 400); // Pause briefly at exactly 100%
}
}, interval);
return () => {
clearInterval(timer);
document.body.style.overflow = "auto";
};
}, []);
return (
<AnimatePresence>
{!complete && (
<motion.div
initial={{ y: 0 }}
exit={{ y: "-100vh" }}
transition={{ duration: 0.9, ease: [0.76, 0, 0.24, 1] }}
className="fixed inset-0 z-[100] flex flex-col items-center justify-center bg-[#0A0A0A] text-white"
>
<div className="flex w-full max-w-sm flex-col items-center justify-center gap-6 px-6">
<div className="overflow-hidden">
<motion.h1
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 1, ease: [0.76, 0, 0.24, 1], delay: 0.1 }}
className="text-4xl sm:text-5xl font-extrabold tracking-tight"
style={{ fontFamily: "'Monument Extended', sans-serif" }}
>
evandro<span className="text-[#E8FF00]">.dev</span>
</motion.h1>
</div>
<div className="h-[2px] w-full overflow-hidden bg-white/10">
<motion.div
className="h-full bg-[#E8FF00]"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.1, ease: "linear" }}
/>
</div>
<div className="flex w-full justify-between overflow-hidden text-[11px] uppercase tracking-widest text-white/50">
<motion.div
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 0.8, ease: [0.76, 0, 0.24, 1], delay: 0.2 }}
>
Carregando
</motion.div>
<motion.div
initial={{ y: "100%" }}
animate={{ y: 0 }}
transition={{ duration: 0.8, ease: [0.76, 0, 0.24, 1], delay: 0.3 }}
>
{progress}%
</motion.div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
+54 -50
View File
@@ -3,6 +3,7 @@
import { useMemo } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { Reveal } from "./Reveal";
export default function Projects() {
const { data, loading, error } = usePortfolioData();
@@ -24,61 +25,64 @@ export default function Projects() {
title="Projetos com recorte técnico claro e impacto observável"
description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
dark
disableChildrenAnimation
>
<div className="ds-grid-2">
{projects.map((project) => (
<article key={project.id} className="ds-card">
<div className="flex items-start justify-between gap-sp-6">
<div>
<div className="ds-kicker">{project.category}</div>
<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>
{projects.map((project, index) => (
<Reveal key={project.id} y={50} duration={0.8} delay={0.1 * index}>
<article className="ds-card h-full">
<div className="flex items-start justify-between gap-sp-6">
<div>
<div className="ds-kicker">{project.category}</div>
<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>
</div>
<span className="ds-tag">{project.status}</span>
</div>
<span className="ds-tag">{project.status}</span>
</div>
<p className="ds-copy">{project.description}</p>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-muted"
>
GitHub
</a>
)}
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-accent"
>
Live demo
</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 {project.metrics?.complexity || 3}/5
</span>
{project.metrics?.teamSize && (
<p className="ds-copy">{project.description}</p>
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
{project.githubUrl && (
<a
href={project.githubUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-muted"
>
GitHub
</a>
)}
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noreferrer"
className="ds-link ds-link-accent"
>
Live demo
</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">
Team {project.metrics.teamSize}
Complexity {project.metrics?.complexity || 3}/5
</span>
)}
</div>
</article>
{project.metrics?.teamSize && (
<span className="ds-metric">
Team {project.metrics.teamSize}
</span>
)}
</div>
</article>
</Reveal>
))}
</div>
</Section>
+36
View File
@@ -0,0 +1,36 @@
"use client";
import { ReactNode } from "react";
import { useScrollReveal } from "../hooks/useScrollReveal";
interface RevealProps {
children: ReactNode;
y?: number;
duration?: number;
delay?: number;
ease?: string;
start?: string;
end?: string;
scrub?: boolean | number;
className?: string;
}
export function Reveal({
children,
y,
duration,
delay,
ease,
start,
end,
scrub,
className = "",
}: RevealProps) {
const ref = useScrollReveal({ y, duration, delay, ease, start, end, scrub });
return (
<div ref={ref as React.RefObject<HTMLDivElement>} className={className}>
{children}
</div>
);
}
+21 -10
View File
@@ -1,4 +1,5 @@
import type React from "react";
import { Reveal } from "./Reveal";
interface SectionProps {
id?: string;
@@ -8,6 +9,7 @@ interface SectionProps {
dark?: boolean;
aside?: React.ReactNode;
children: React.ReactNode;
disableChildrenAnimation?: boolean;
}
export const Section = ({
@@ -18,22 +20,31 @@ export const Section = ({
dark,
aside,
children,
disableChildrenAnimation = false,
}: SectionProps) => {
const baseClass = dark ? "ds-section ds-section-dark" : "ds-section";
return (
<section id={id} className={baseClass}>
<div className="ds-section-shell">
<div className="ds-section-header">
<div className="ds-section-meta">
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
<h2 className="ds-section-title">{title}</h2>
{description && (
<p className="ds-section-description">{description}</p>
)}
<Reveal y={40} duration={0.8}>
<div className="ds-section-header">
<div className="ds-section-meta">
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
<h2 className="ds-section-title">{title}</h2>
{description && (
<p className="ds-section-description">{description}</p>
)}
</div>
{aside && <aside>{aside}</aside>}
</div>
{aside && <aside>{aside}</aside>}
</div>
<div>{children}</div>
</Reveal>
{!disableChildrenAnimation ? (
<Reveal y={60} duration={1} delay={0.2}>
<div>{children}</div>
</Reveal>
) : (
<div>{children}</div>
)}
</div>
</section>
);
+18 -14
View File
@@ -2,6 +2,7 @@
import { usePortfolioData } from "@/lib/usePortfolioData";
import { Section } from "./Section";
import { Reveal } from "./Reveal";
export default function Skills() {
const { data, loading, error } = usePortfolioData();
@@ -18,23 +19,26 @@ export default function Skills() {
title="Stack modular para produto, operação e escala"
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
dark
disableChildrenAnimation
>
<div className="ds-grid-2">
{data.skills.technical.map((category, index) => (
<article key={category.category} className="ds-card">
<div className="flex items-center justify-between gap-sp-4">
<div className="ds-kicker">ID_0{index + 1}X</div>
<div className="ds-metric">{category.skills.length} items</div>
</div>
<h3 className="ds-card-title">{category.category}</h3>
<ul className="ds-tags mt-sp-6">
{category.skills.map((skill) => (
<li key={skill} className="ds-tag">
{skill}
</li>
))}
</ul>
</article>
<Reveal key={category.category} y={40} duration={0.8} delay={0.1 * index}>
<article className="ds-card h-full">
<div className="flex items-center justify-between gap-sp-4">
<div className="ds-kicker">ID_0{index + 1}X</div>
<div className="ds-metric">{category.skills.length} items</div>
</div>
<h3 className="ds-card-title">{category.category}</h3>
<ul className="ds-tags mt-sp-6">
{category.skills.map((skill) => (
<li key={skill} className="ds-tag">
{skill}
</li>
))}
</ul>
</article>
</Reveal>
))}
</div>
</Section>
+7 -2
View File
@@ -2,6 +2,7 @@
import { useState } from "react";
import { Section } from "./Section";
import { Reveal } from "./Reveal";
interface Tier {
id: string;
@@ -268,6 +269,7 @@ export function WorkModalities() {
eyebrow="CONTRACT_PROTOCOLS.sys"
title="Engenharia sob demanda. Previsibilidade de entrega sem overhead."
description="Três níveis de contratação desenhados como uma esteira de valor — do diagnóstico rápido à parceria estratégica de longo prazo."
disableChildrenAnimation
>
<div
style={{
@@ -275,9 +277,12 @@ export function WorkModalities() {
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "var(--sp-6)",
}}
className="max-md:grid-cols-1 max-md:!flex max-md:!flex-col"
>
{TIERS.map((tier) => (
<TierCard key={tier.id} tier={tier} />
{TIERS.map((tier, index) => (
<Reveal key={tier.id} y={40} duration={0.8} delay={0.1 * index}>
<TierCard tier={tier} />
</Reveal>
))}
</div>
</Section>
+48
View File
@@ -0,0 +1,48 @@
"use client";
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
export function useScrollReveal(options?: {
y?: number;
duration?: number;
delay?: number;
ease?: string;
start?: string;
end?: string;
scrub?: boolean | number;
}) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
if (!ref.current) return;
// Lazy registration to avoid Next.js hydration mismatch issues
gsap.registerPlugin(ScrollTrigger);
const ctx = gsap.context(() => {
gsap.fromTo(
ref.current,
{ y: options?.y ?? 60, opacity: 0 },
{
y: 0,
opacity: 1,
duration: options?.duration ?? 0.9,
delay: options?.delay ?? 0,
ease: options?.ease ?? "power3.out",
scrollTrigger: {
trigger: ref.current,
start: options?.start ?? "top 85%",
end: options?.end ?? "top 50%",
scrub: options?.scrub ?? false,
},
}
);
}, ref);
return () => ctx.revert();
}, [options?.y, options?.duration, options?.delay, options?.ease, options?.start, options?.end, options?.scrub]);
return ref;
}
+10 -6
View File
@@ -1,9 +1,11 @@
import type { Metadata, Viewport } from "next";
import { ReactNode } from "react";
import GoogleAnalytics from "./components/GoogleAnalytics";
import { SmoothScrollProvider } from "./providers/SmoothScrollProvider";
import "./globals.css";
export const metadata: Metadata = {
// ... existing metadata ...
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
@@ -137,12 +139,14 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
<link rel="canonical" href="https://evandro.dev.br" />
</head>
<body>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
<GoogleAnalytics />
<SmoothScrollProvider>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{children}
<GoogleAnalytics />
</SmoothScrollProvider>
</body>
</html>
);
+2
View File
@@ -5,10 +5,12 @@ import Hero from "./components/Hero";
import Projects from "./components/Projects";
import Skills from "./components/Skills";
import { WorkModalities } from "./components/WorkModalities";
import { Preloader } from "./components/Preloader";
export default function Home() {
return (
<div className="flex min-h-screen flex-col overflow-x-hidden">
<Preloader />
<main className="ds-main">
<Hero />
<About />
+42
View File
@@ -0,0 +1,42 @@
"use client";
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;
}) {
const lenisRef = useRef<Lenis | null>(null);
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // Expo ease
smoothWheel: true,
});
lenisRef.current = lenis;
// Sincroniza Lenis com GSAP ScrollTrigger — critico para evitar dessync
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
return () => {
lenis.destroy();
gsap.ticker.remove((time) => lenis.raf(time * 1000));
};
}, []);
return <>{children}</>;
}
+2
View File
@@ -10,11 +10,13 @@
"format": "biome format --write"
},
"dependencies": {
"@gsap/react": "^2.1.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cobe": "^0.6.4",
"framer-motion": "^12.23.12",
"gsap": "^3.13.0",
"lenis": "^1.3.18",
"lucide-react": "^0.542.0",
"motion": "^12.23.12",
"next": "15.5.9",
+35
View File
@@ -17,6 +17,9 @@ importers:
.:
dependencies:
'@gsap/react':
specifier: ^2.1.2
version: 2.1.2([email protected])([email protected])
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -32,6 +35,9 @@ importers:
gsap:
specifier: ^3.13.0
version: 3.13.0
lenis:
specifier: ^1.3.18
version: 1.3.18([email protected])
lucide-react:
specifier: ^0.542.0
version: 0.542.0([email protected])
@@ -148,6 +154,12 @@ packages:
'@emnapi/[email protected]':
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
'@gsap/[email protected]':
resolution: {integrity: sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==}
peerDependencies:
gsap: ^3.12.5
react: '>=17'
'@img/[email protected]':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
@@ -546,6 +558,20 @@ packages:
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-7KBl3V7vx5y1h05pu9fNFZS66I0+1eZ+zUGNNNBKtEn3BONZy+nkHWvdEe2b+zKT+6WX1x7zyOb1zbYYOs6tcg==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
react: '>=17.0.0'
vue: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
react:
optional: true
vue:
optional: true
[email protected]:
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
engines: {node: '>= 12.0.0'}
@@ -812,6 +838,11 @@ snapshots:
tslib: 2.8.1
optional: true
'@gsap/[email protected]([email protected])([email protected])':
dependencies:
gsap: 3.13.0
react: 19.1.0
'@img/[email protected]':
optional: true
@@ -1091,6 +1122,10 @@ snapshots:
[email protected]: {}
[email protected]([email protected]):
optionalDependencies:
react: 19.1.0
[email protected]:
optional: true