From d54462c687e804fb9ae7c3bec85e401a1a754434 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 7 Mar 2026 15:39:38 -0300 Subject: [PATCH] feat: Initialize portfolio website with core sections, smooth scrolling, scroll reveal animations, and comprehensive SEO. --- app/components/About.tsx | 57 +++++++------- app/components/Experience.tsx | 62 ++++++++------- app/components/Hero.tsx | 2 +- app/components/Preloader.tsx | 93 ++++++++++++++++++++++ app/components/Projects.tsx | 104 +++++++++++++------------ app/components/Reveal.tsx | 36 +++++++++ app/components/Section.tsx | 31 +++++--- app/components/Skills.tsx | 32 ++++---- app/components/WorkModalities.tsx | 9 ++- app/hooks/useScrollReveal.ts | 48 ++++++++++++ app/layout.tsx | 16 ++-- app/page.tsx | 2 + app/providers/SmoothScrollProvider.tsx | 42 ++++++++++ package.json | 2 + pnpm-lock.yaml | 35 +++++++++ 15 files changed, 433 insertions(+), 138 deletions(-) create mode 100644 app/components/Preloader.tsx create mode 100644 app/components/Reveal.tsx create mode 100644 app/hooks/useScrollReveal.ts create mode 100644 app/providers/SmoothScrollProvider.tsx diff --git a/app/components/About.tsx b/app/components/About.tsx index a42386b..27f7805 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -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={
@@ -54,36 +55,40 @@ export default function About() { >
- {STORY_DATA.map((step) => ( -
-
{step.id}
-

{step.title}

-

{step.content}

-
+ {STORY_DATA.map((step, index) => ( + +
+
{step.id}
+

{step.title}

+

{step.content}

+
+
))}
-
-
System Signature
-

Base técnica e postura de entrega

-

- Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js, - React, Node.js, Tauri, integrações e arquitetura pragmática. -

-
- {VALUES_DATA.map((value) => ( -
-
- {value.title} + +
+
System Signature
+

Base técnica e postura de entrega

+

+ Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js, + React, Node.js, Tauri, integrações e arquitetura pragmática. +

+
+ {VALUES_DATA.map((value) => ( +
+
+ {value.title} +
+

{value.description}

-

{value.description}

-
- ))} + ))} +
-
+
); diff --git a/app/components/Experience.tsx b/app/components/Experience.tsx index 54c8e77..8a1766d 100644 --- a/app/components/Experience.tsx +++ b/app/components/Experience.tsx @@ -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 >
- {sortedExperiences.map((exp) => ( -
-
-
-
{exp.period}
-

{exp.title}

-
- {exp.company} · {exp.type} + {sortedExperiences.map((exp, index) => ( + +
+
+
+
{exp.period}
+

{exp.title}

+
+ {exp.company} · {exp.type} +
+
+
+ {exp.location} + + {exp.technologies.length} tecnologias +
-
- {exp.location} - - {exp.technologies.length} tecnologias - -
-
-

{exp.description}

-
    - {exp.achievements?.map((achievement) => ( -
  • {achievement}
  • - ))} -
-
    - {exp.technologies?.map((tech) => ( -
  • - {tech} -
  • - ))} -
-
+

{exp.description}

+
    + {exp.achievements?.map((achievement) => ( +
  • {achievement}
  • + ))} +
+
    + {exp.technologies?.map((tech) => ( +
  • + {tech} +
  • + ))} +
+ + ))}
diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index e9af4ee..efe3a7d 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -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 }} >
{ + 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 ( + + {!complete && ( + +
+
+ + evandro.dev + +
+ +
+ +
+ +
+ + Carregando + + + {progress}% + +
+
+
+ )} +
+ ); +} diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index a429b84..2ba17b7 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -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 >
- {projects.map((project) => ( -
-
-
-
{project.category}
-

{project.title}

-

- {project.subtitle} -

+ {projects.map((project, index) => ( + +
+
+
+
{project.category}
+

{project.title}

+

+ {project.subtitle} +

+
+ {project.status}
- {project.status} -
-

{project.description}

-
- {project.githubUrl && ( - - GitHub - - )} - {project.demoUrl && ( - - Live demo - - )} -
-
    - {project.technologies.slice(0, 6).map((tech: string) => ( -
  • - {tech} -
  • - ))} -
-
- - Complexity {project.metrics?.complexity || 3}/5 - - {project.metrics?.teamSize && ( +

{project.description}

+
+ {project.githubUrl && ( + + GitHub + + )} + {project.demoUrl && ( + + Live demo + + )} +
+
    + {project.technologies.slice(0, 6).map((tech: string) => ( +
  • + {tech} +
  • + ))} +
+
- Team {project.metrics.teamSize} + Complexity {project.metrics?.complexity || 3}/5 - )} -
-
+ {project.metrics?.teamSize && ( + + Team {project.metrics.teamSize} + + )} +
+ + ))}
diff --git a/app/components/Reveal.tsx b/app/components/Reveal.tsx new file mode 100644 index 0000000..5aaa721 --- /dev/null +++ b/app/components/Reveal.tsx @@ -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 ( +
} className={className}> + {children} +
+ ); +} diff --git a/app/components/Section.tsx b/app/components/Section.tsx index 168fd12..021dc4e 100644 --- a/app/components/Section.tsx +++ b/app/components/Section.tsx @@ -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 (
-
-
- {eyebrow &&

{eyebrow}

} -

{title}

- {description && ( -

{description}

- )} + +
+
+ {eyebrow &&

{eyebrow}

} +

{title}

+ {description && ( +

{description}

+ )} +
+ {aside && }
- {aside && } -
-
{children}
+ + {!disableChildrenAnimation ? ( + +
{children}
+
+ ) : ( +
{children}
+ )}
); diff --git a/app/components/Skills.tsx b/app/components/Skills.tsx index 9136771..4f8ed39 100644 --- a/app/components/Skills.tsx +++ b/app/components/Skills.tsx @@ -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 >
{data.skills.technical.map((category, index) => ( -
-
-
ID_0{index + 1}X
-
{category.skills.length} items
-
-

{category.category}

-
    - {category.skills.map((skill) => ( -
  • - {skill} -
  • - ))} -
-
+ +
+
+
ID_0{index + 1}X
+
{category.skills.length} items
+
+

{category.category}

+
    + {category.skills.map((skill) => ( +
  • + {skill} +
  • + ))} +
+
+
))}
diff --git a/app/components/WorkModalities.tsx b/app/components/WorkModalities.tsx index d058ce9..a30dc9f 100644 --- a/app/components/WorkModalities.tsx +++ b/app/components/WorkModalities.tsx @@ -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 >
- {TIERS.map((tier) => ( - + {TIERS.map((tier, index) => ( + + + ))}
diff --git a/app/hooks/useScrollReveal.ts b/app/hooks/useScrollReveal.ts new file mode 100644 index 0000000..a9af952 --- /dev/null +++ b/app/hooks/useScrollReveal.ts @@ -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(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; +} diff --git a/app/layout.tsx b/app/layout.tsx index 5f6c1df..5bff897 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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) { -