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 { Section } from "./Section";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
const STORY_DATA = [
|
const STORY_DATA = [
|
||||||
{
|
{
|
||||||
id: "KERNEL_INIT",
|
id: "KERNEL_INIT",
|
||||||
@@ -45,6 +45,7 @@ export default function About() {
|
|||||||
eyebrow="WHOAMI.sys"
|
eyebrow="WHOAMI.sys"
|
||||||
title="Engenharia orientada por clareza, não por ruído"
|
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."
|
description="Transformo complexidade técnica em sistemas estáveis, operáveis e alinhados ao negócio."
|
||||||
|
disableChildrenAnimation
|
||||||
aside={
|
aside={
|
||||||
<div className="mt-sp-4 ds-metric">
|
<div className="mt-sp-4 ds-metric">
|
||||||
<span className="inline-block h-1.5 w-1.5 rounded-full bg-slash" />
|
<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">
|
||||||
<div className="ds-grid-2">
|
<div className="ds-grid-2">
|
||||||
{STORY_DATA.map((step) => (
|
{STORY_DATA.map((step, index) => (
|
||||||
<article key={step.id} className="ds-card">
|
<Reveal key={step.id} y={30} duration={0.8} delay={0.1 * index}>
|
||||||
<div className="ds-kicker">{step.id}</div>
|
<article className="ds-card h-full">
|
||||||
<h3 className="ds-card-title">{step.title}</h3>
|
<div className="ds-kicker">{step.id}</div>
|
||||||
<p className="ds-copy">{step.content}</p>
|
<h3 className="ds-card-title">{step.title}</h3>
|
||||||
</article>
|
<p className="ds-copy">{step.content}</p>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ds-card">
|
<Reveal y={30} duration={0.8} delay={0.2}>
|
||||||
<div className="ds-kicker">System Signature</div>
|
<div className="ds-card h-full">
|
||||||
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
|
<div className="ds-kicker">System Signature</div>
|
||||||
<p className="ds-copy">
|
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
|
||||||
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js,
|
<p className="ds-copy">
|
||||||
React, Node.js, Tauri, integrações e arquitetura pragmática.
|
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js,
|
||||||
</p>
|
React, Node.js, Tauri, integrações e arquitetura pragmática.
|
||||||
<div className="ds-grid-2 mt-sp-6">
|
</p>
|
||||||
{VALUES_DATA.map((value) => (
|
<div className="ds-grid-2 mt-sp-6">
|
||||||
<div
|
{VALUES_DATA.map((value) => (
|
||||||
key={value.title}
|
<div
|
||||||
className="border-t border-[var(--border)] pt-sp-4"
|
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 className="text-[11px] font-medium uppercase tracking-[0.08em]">
|
||||||
|
{value.title}
|
||||||
|
</div>
|
||||||
|
<p className="ds-copy !mt-sp-2">{value.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="ds-copy !mt-sp-2">{value.description}</p>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { usePortfolioData } from "@/lib/usePortfolioData";
|
import { usePortfolioData } from "@/lib/usePortfolioData";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
export default function Experience() {
|
export default function Experience() {
|
||||||
const { data, loading, error } = usePortfolioData();
|
const { data, loading, error } = usePortfolioData();
|
||||||
@@ -28,39 +29,42 @@ export default function Experience() {
|
|||||||
eyebrow="SYSTEM_LOGS.history"
|
eyebrow="SYSTEM_LOGS.history"
|
||||||
title="Trajetória focada em produto, legado e crescimento sustentável"
|
title="Trajetória focada em produto, legado e crescimento sustentável"
|
||||||
description="Experiências recentes onde combinei entrega rápida com responsabilidade arquitetural."
|
description="Experiências recentes onde combinei entrega rápida com responsabilidade arquitetural."
|
||||||
|
disableChildrenAnimation
|
||||||
>
|
>
|
||||||
<div className="space-y-sp-6">
|
<div className="space-y-sp-6">
|
||||||
{sortedExperiences.map((exp) => (
|
{sortedExperiences.map((exp, index) => (
|
||||||
<article key={exp.id} className="ds-card">
|
<Reveal key={exp.id} y={50} duration={0.8} delay={0.1 * index}>
|
||||||
<div className="ds-grid-2">
|
<article className="ds-card">
|
||||||
<div>
|
<div className="ds-grid-2">
|
||||||
<div className="ds-kicker">{exp.period}</div>
|
<div>
|
||||||
<h3 className="ds-card-title">{exp.title}</h3>
|
<div className="ds-kicker">{exp.period}</div>
|
||||||
<div className="mt-sp-3 text-[11px] uppercase tracking-[0.1em] text-meta">
|
<h3 className="ds-card-title">{exp.title}</h3>
|
||||||
{exp.company} · {exp.type}
|
<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>
|
</div>
|
||||||
<div className="flex flex-wrap items-start justify-start gap-sp-3 lg:justify-end">
|
<p className="ds-copy">{exp.description}</p>
|
||||||
<span className="ds-tag">{exp.location}</span>
|
<ul className="ds-list">
|
||||||
<span className="ds-tag">
|
{exp.achievements?.map((achievement) => (
|
||||||
{exp.technologies.length} tecnologias
|
<li key={achievement}>{achievement}</li>
|
||||||
</span>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
<ul className="ds-tags mt-sp-6">
|
||||||
<p className="ds-copy">{exp.description}</p>
|
{exp.technologies?.map((tech) => (
|
||||||
<ul className="ds-list">
|
<li key={tech} className="ds-tag">
|
||||||
{exp.achievements?.map((achievement) => (
|
{tech}
|
||||||
<li key={achievement}>{achievement}</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<ul className="ds-tags mt-sp-6">
|
</article>
|
||||||
{exp.technologies?.map((tech) => (
|
</Reveal>
|
||||||
<li key={tech} className="ds-tag">
|
|
||||||
{tech}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export default function Hero() {
|
|||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true, margin: "-100px" }}
|
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">
|
<div className="mb-sp-8 inline-flex items-center gap-sp-2">
|
||||||
<span
|
<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 { useMemo } from "react";
|
||||||
import { usePortfolioData } from "@/lib/usePortfolioData";
|
import { usePortfolioData } from "@/lib/usePortfolioData";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
const { data, loading, error } = usePortfolioData();
|
const { data, loading, error } = usePortfolioData();
|
||||||
@@ -24,61 +25,64 @@ export default function Projects() {
|
|||||||
title="Projetos com recorte técnico claro e impacto observável"
|
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."
|
description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
|
||||||
dark
|
dark
|
||||||
|
disableChildrenAnimation
|
||||||
>
|
>
|
||||||
<div className="ds-grid-2">
|
<div className="ds-grid-2">
|
||||||
{projects.map((project) => (
|
{projects.map((project, index) => (
|
||||||
<article key={project.id} className="ds-card">
|
<Reveal key={project.id} y={50} duration={0.8} delay={0.1 * index}>
|
||||||
<div className="flex items-start justify-between gap-sp-6">
|
<article className="ds-card h-full">
|
||||||
<div>
|
<div className="flex items-start justify-between gap-sp-6">
|
||||||
<div className="ds-kicker">{project.category}</div>
|
<div>
|
||||||
<h3 className="ds-card-title">{project.title}</h3>
|
<div className="ds-kicker">{project.category}</div>
|
||||||
<p className="mt-sp-2 text-[11px] uppercase tracking-[0.1em] text-[rgba(255,255,255,.36)]">
|
<h3 className="ds-card-title">{project.title}</h3>
|
||||||
{project.subtitle}
|
<p className="mt-sp-2 text-[11px] uppercase tracking-[0.1em] text-[rgba(255,255,255,.36)]">
|
||||||
</p>
|
{project.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="ds-tag">{project.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="ds-tag">{project.status}</span>
|
<p className="ds-copy">{project.description}</p>
|
||||||
</div>
|
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
|
||||||
<p className="ds-copy">{project.description}</p>
|
{project.githubUrl && (
|
||||||
<div className="mt-sp-6 flex flex-wrap gap-sp-3">
|
<a
|
||||||
{project.githubUrl && (
|
href={project.githubUrl}
|
||||||
<a
|
target="_blank"
|
||||||
href={project.githubUrl}
|
rel="noreferrer"
|
||||||
target="_blank"
|
className="ds-link ds-link-muted"
|
||||||
rel="noreferrer"
|
>
|
||||||
className="ds-link ds-link-muted"
|
GitHub
|
||||||
>
|
</a>
|
||||||
GitHub
|
)}
|
||||||
</a>
|
{project.demoUrl && (
|
||||||
)}
|
<a
|
||||||
{project.demoUrl && (
|
href={project.demoUrl}
|
||||||
<a
|
target="_blank"
|
||||||
href={project.demoUrl}
|
rel="noreferrer"
|
||||||
target="_blank"
|
className="ds-link ds-link-accent"
|
||||||
rel="noreferrer"
|
>
|
||||||
className="ds-link ds-link-accent"
|
Live demo
|
||||||
>
|
</a>
|
||||||
Live demo
|
)}
|
||||||
</a>
|
</div>
|
||||||
)}
|
<ul className="ds-tags mt-sp-6">
|
||||||
</div>
|
{project.technologies.slice(0, 6).map((tech: string) => (
|
||||||
<ul className="ds-tags mt-sp-6">
|
<li key={tech} className="ds-tag">
|
||||||
{project.technologies.slice(0, 6).map((tech: string) => (
|
{tech}
|
||||||
<li key={tech} className="ds-tag">
|
</li>
|
||||||
{tech}
|
))}
|
||||||
</li>
|
</ul>
|
||||||
))}
|
<div className="mt-sp-6 flex flex-wrap gap-sp-4">
|
||||||
</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 && (
|
|
||||||
<span className="ds-metric">
|
<span className="ds-metric">
|
||||||
Team {project.metrics.teamSize}
|
Complexity {project.metrics?.complexity || 3}/5
|
||||||
</span>
|
</span>
|
||||||
)}
|
{project.metrics?.teamSize && (
|
||||||
</div>
|
<span className="ds-metric">
|
||||||
</article>
|
Team {project.metrics.teamSize}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</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 type React from "react";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -8,6 +9,7 @@ interface SectionProps {
|
|||||||
dark?: boolean;
|
dark?: boolean;
|
||||||
aside?: React.ReactNode;
|
aside?: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
disableChildrenAnimation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Section = ({
|
export const Section = ({
|
||||||
@@ -18,22 +20,31 @@ export const Section = ({
|
|||||||
dark,
|
dark,
|
||||||
aside,
|
aside,
|
||||||
children,
|
children,
|
||||||
|
disableChildrenAnimation = false,
|
||||||
}: SectionProps) => {
|
}: SectionProps) => {
|
||||||
const baseClass = dark ? "ds-section ds-section-dark" : "ds-section";
|
const baseClass = dark ? "ds-section ds-section-dark" : "ds-section";
|
||||||
return (
|
return (
|
||||||
<section id={id} className={baseClass}>
|
<section id={id} className={baseClass}>
|
||||||
<div className="ds-section-shell">
|
<div className="ds-section-shell">
|
||||||
<div className="ds-section-header">
|
<Reveal y={40} duration={0.8}>
|
||||||
<div className="ds-section-meta">
|
<div className="ds-section-header">
|
||||||
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
|
<div className="ds-section-meta">
|
||||||
<h2 className="ds-section-title">{title}</h2>
|
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
|
||||||
{description && (
|
<h2 className="ds-section-title">{title}</h2>
|
||||||
<p className="ds-section-description">{description}</p>
|
{description && (
|
||||||
)}
|
<p className="ds-section-description">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{aside && <aside>{aside}</aside>}
|
||||||
</div>
|
</div>
|
||||||
{aside && <aside>{aside}</aside>}
|
</Reveal>
|
||||||
</div>
|
{!disableChildrenAnimation ? (
|
||||||
<div>{children}</div>
|
<Reveal y={60} duration={1} delay={0.2}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</Reveal>
|
||||||
|
) : (
|
||||||
|
<div>{children}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
+18
-14
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { usePortfolioData } from "@/lib/usePortfolioData";
|
import { usePortfolioData } from "@/lib/usePortfolioData";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
export default function Skills() {
|
export default function Skills() {
|
||||||
const { data, loading, error } = usePortfolioData();
|
const { data, loading, error } = usePortfolioData();
|
||||||
@@ -18,23 +19,26 @@ export default function Skills() {
|
|||||||
title="Stack modular para produto, operação e escala"
|
title="Stack modular para produto, operação e escala"
|
||||||
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
|
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
|
||||||
dark
|
dark
|
||||||
|
disableChildrenAnimation
|
||||||
>
|
>
|
||||||
<div className="ds-grid-2">
|
<div className="ds-grid-2">
|
||||||
{data.skills.technical.map((category, index) => (
|
{data.skills.technical.map((category, index) => (
|
||||||
<article key={category.category} className="ds-card">
|
<Reveal key={category.category} y={40} duration={0.8} delay={0.1 * index}>
|
||||||
<div className="flex items-center justify-between gap-sp-4">
|
<article className="ds-card h-full">
|
||||||
<div className="ds-kicker">ID_0{index + 1}X</div>
|
<div className="flex items-center justify-between gap-sp-4">
|
||||||
<div className="ds-metric">{category.skills.length} items</div>
|
<div className="ds-kicker">ID_0{index + 1}X</div>
|
||||||
</div>
|
<div className="ds-metric">{category.skills.length} items</div>
|
||||||
<h3 className="ds-card-title">{category.category}</h3>
|
</div>
|
||||||
<ul className="ds-tags mt-sp-6">
|
<h3 className="ds-card-title">{category.category}</h3>
|
||||||
{category.skills.map((skill) => (
|
<ul className="ds-tags mt-sp-6">
|
||||||
<li key={skill} className="ds-tag">
|
{category.skills.map((skill) => (
|
||||||
{skill}
|
<li key={skill} className="ds-tag">
|
||||||
</li>
|
{skill}
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</article>
|
</ul>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
|
import { Reveal } from "./Reveal";
|
||||||
|
|
||||||
interface Tier {
|
interface Tier {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -268,6 +269,7 @@ export function WorkModalities() {
|
|||||||
eyebrow="CONTRACT_PROTOCOLS.sys"
|
eyebrow="CONTRACT_PROTOCOLS.sys"
|
||||||
title="Engenharia sob demanda. Previsibilidade de entrega sem overhead."
|
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."
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -275,9 +277,12 @@ export function WorkModalities() {
|
|||||||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||||||
gap: "var(--sp-6)",
|
gap: "var(--sp-6)",
|
||||||
}}
|
}}
|
||||||
|
className="max-md:grid-cols-1 max-md:!flex max-md:!flex-col"
|
||||||
>
|
>
|
||||||
{TIERS.map((tier) => (
|
{TIERS.map((tier, index) => (
|
||||||
<TierCard key={tier.id} tier={tier} />
|
<Reveal key={tier.id} y={40} duration={0.8} delay={0.1 * index}>
|
||||||
|
<TierCard tier={tier} />
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</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 type { Metadata, Viewport } from "next";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import GoogleAnalytics from "./components/GoogleAnalytics";
|
import GoogleAnalytics from "./components/GoogleAnalytics";
|
||||||
|
import { SmoothScrollProvider } from "./providers/SmoothScrollProvider";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
// ... existing metadata ...
|
||||||
title:
|
title:
|
||||||
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
|
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
|
||||||
description:
|
description:
|
||||||
@@ -137,12 +139,14 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
|
|||||||
<link rel="canonical" href="https://evandro.dev.br" />
|
<link rel="canonical" href="https://evandro.dev.br" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script
|
<SmoothScrollProvider>
|
||||||
type="application/ld+json"
|
<script
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
type="application/ld+json"
|
||||||
/>
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
{children}
|
/>
|
||||||
<GoogleAnalytics />
|
{children}
|
||||||
|
<GoogleAnalytics />
|
||||||
|
</SmoothScrollProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import Hero from "./components/Hero";
|
|||||||
import Projects from "./components/Projects";
|
import Projects from "./components/Projects";
|
||||||
import Skills from "./components/Skills";
|
import Skills from "./components/Skills";
|
||||||
import { WorkModalities } from "./components/WorkModalities";
|
import { WorkModalities } from "./components/WorkModalities";
|
||||||
|
import { Preloader } from "./components/Preloader";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col overflow-x-hidden">
|
<div className="flex min-h-screen flex-col overflow-x-hidden">
|
||||||
|
<Preloader />
|
||||||
<main className="ds-main">
|
<main className="ds-main">
|
||||||
<Hero />
|
<Hero />
|
||||||
<About />
|
<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}</>;
|
||||||
|
}
|
||||||
@@ -10,11 +10,13 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@gsap/react": "^2.1.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cobe": "^0.6.4",
|
"cobe": "^0.6.4",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
|
"lenis": "^1.3.18",
|
||||||
"lucide-react": "^0.542.0",
|
"lucide-react": "^0.542.0",
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
"next": "15.5.9",
|
"next": "15.5.9",
|
||||||
|
|||||||
Generated
+35
@@ -17,6 +17,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@gsap/react':
|
||||||
|
specifier: ^2.1.2
|
||||||
|
version: 2.1.2([email protected])([email protected])
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -32,6 +35,9 @@ importers:
|
|||||||
gsap:
|
gsap:
|
||||||
specifier: ^3.13.0
|
specifier: ^3.13.0
|
||||||
version: 3.13.0
|
version: 3.13.0
|
||||||
|
lenis:
|
||||||
|
specifier: ^1.3.18
|
||||||
|
version: 1.3.18([email protected])
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.542.0
|
specifier: ^0.542.0
|
||||||
version: 0.542.0([email protected])
|
version: 0.542.0([email protected])
|
||||||
@@ -148,6 +154,12 @@ packages:
|
|||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
|
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]':
|
'@img/[email protected]':
|
||||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -546,6 +558,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
|
||||||
hasBin: true
|
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]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -812,6 +838,11 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@gsap/[email protected]([email protected])([email protected])':
|
||||||
|
dependencies:
|
||||||
|
gsap: 3.13.0
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
'@img/[email protected]':
|
'@img/[email protected]':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1091,6 +1122,10 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]([email protected]):
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user