feat: Initialize portfolio website with core sections, smooth scrolling, scroll reveal animations, and comprehensive SEO.
This commit is contained in:
1 parent
6299ae040a
commit
d54462c687
15 files changed
+433
-138
No files matched your search
+31
-26
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
Reference in new issue
Block a user