feat: revamp portfolio with case studies, curriculo page, and local SEO

Adds dynamic case study and curriculo routes, project health monitoring
for live demo status, and strengthens local SEO (structured data,
service-area targeting for Garuva/Joinville/Curitiba, a dedicated
"criação de sites" service page) so search results better match the
professional title (Engenheiro da Computação) and experience (5 anos).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Evandro Fonseca JuniorandClaude Sonnet 5 committed 2026-07-15 01:31:46 -03:00
1 parent 9236fdd685
commit d76bbf5974
41 files changed
+1359 -492

No files matched your search

+74 -146
View File
@@ -1,177 +1,105 @@
"use client";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
const INTRO_LINES = [
"EVANDRO",
"FONSECA",
"JUNIOR",
];
const EXIT_EASE: [number, number, number, number] = [0.76, 0, 0.24, 1];
export default function PageIntro() {
const [isVisible, setIsVisible] = useState(true);
const [counter, setCounter] = useState(0);
const [mounted, setMounted] = useState(false);
const [visible, setVisible] = useState(true);
const reduceMotion = useReducedMotion();
useEffect(() => {
setMounted(true);
// Remove the immediate blocking overlay now that the animated loader is ready
const blocker = document.getElementById("__loading-blocker");
if (blocker) blocker.remove();
}, []);
const timeout = window.setTimeout(
() => setVisible(false),
reduceMotion ? 150 : 1350,
);
useEffect(() => {
if (!mounted) return;
// Animate counter from 0 to 100
const duration = 2000;
const start = performance.now();
const tick = (now: number) => {
const elapsed = now - start;
const progress = Math.min(elapsed / duration, 1);
// Eased progress
const eased = 1 - Math.pow(1 - progress, 3);
setCounter(Math.floor(eased * 100));
if (progress < 1) {
requestAnimationFrame(tick);
} else {
// Wait a beat then hide
setTimeout(() => setIsVisible(false), 400);
}
};
requestAnimationFrame(tick);
}, [mounted]);
return () => window.clearTimeout(timeout);
}, [reduceMotion]);
if (!mounted) return null;
return createPortal(
<AnimatePresence>
{isVisible && (
{visible && (
<motion.div
className="fixed inset-0 z-[99999] flex items-center justify-center bg-[#0a0a0a]"
style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, width: "100vw", height: "100vh" }}
exit={{
clipPath: "inset(0 0 100% 0)",
transition: { duration: 1, ease: [0.76, 0, 0.24, 1] },
}}
className="fixed inset-0 z-[99999] grid overflow-hidden bg-[var(--ink)] text-[#f4f2ed]"
initial={{ opacity: 1 }}
exit={
reduceMotion
? { opacity: 0, transition: { duration: 0.15 } }
: {
clipPath: "inset(0 0 100% 0)",
transition: { duration: 0.65, ease: EXIT_EASE },
}
}
aria-live="polite"
aria-label="Preparando o portfólio"
>
{/* Horizontal lines decoration */}
<div className="pointer-events-none absolute inset-0 overflow-hidden">
{[...Array(5)].map((_, i) => (
<motion.div
key={i}
className="absolute left-0 h-px w-full bg-[rgba(204,255,0,0.06)]"
style={{ top: `${20 + i * 15}%` }}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{
duration: 1.5,
delay: 0.1 * i,
ease: [0.76, 0, 0.24, 1],
}}
/>
))}
</div>
<div className="pointer-events-none absolute inset-0 opacity-30 [background-image:linear-gradient(rgba(255,255,255,.06)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.06)_1px,transparent_1px)] [background-size:48px_48px]" />
<div className="pointer-events-none absolute inset-x-0 top-[42%] h-px bg-[var(--slash)] opacity-70" />
{/* Counter */}
<motion.div
className="absolute bottom-12 right-12 font-mono text-[clamp(48px,8vw,96px)] font-bold leading-none tracking-tighter text-[rgba(204,255,0,0.15)]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
{counter.toString().padStart(3, "0")}
</motion.div>
{/* Progress bar */}
<div className="absolute bottom-8 left-12 right-12 h-px bg-[rgba(255,255,255,0.06)]">
<div className="relative m-auto w-[min(88vw,680px)]">
<motion.p
className="font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--slash)]"
initial={reduceMotion ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: [0.16, 1, 0.3, 1] }}
>
SINAL DE ENTRADA
</motion.p>
<motion.h1
className="mt-sp-4 font-display text-[clamp(3rem,12vw,8rem)] font-extrabold leading-[0.82] tracking-[-0.08em]"
initial={reduceMotion ? false : { opacity: 0, y: 28 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.48,
delay: 0.06,
ease: [0.16, 1, 0.3, 1],
}}
>
EVANDRO<span className="text-[var(--slash)]">.</span>DEV
</motion.h1>
<motion.div
className="h-full bg-[var(--slash)]"
initial={{ width: "0%" }}
animate={{ width: `${counter}%` }}
transition={{ duration: 0.1 }}
/>
</div>
{/* Name reveal */}
<div className="relative flex flex-col items-center gap-0">
{INTRO_LINES.map((line, i) => (
<div key={line} className="overflow-hidden">
<motion.div
className="font-display text-[clamp(2.5rem,10vw,7rem)] font-extrabold leading-[0.9] tracking-[-0.06em] text-[#f4f2ed]"
initial={{ y: "100%", rotateX: -40 }}
animate={{ y: "0%", rotateX: 0 }}
transition={{
duration: 0.8,
delay: 0.3 + i * 0.12,
ease: [0.16, 1, 0.3, 1],
}}
>
{line}
</motion.div>
</div>
))}
{/* Subtitle */}
<motion.div
className="mt-6 overflow-hidden"
initial={{ opacity: 0 }}
className="mt-sp-8 flex items-center gap-sp-4"
initial={reduceMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1, duration: 0.6 }}
transition={{ duration: 0.28, delay: 0.18 }}
>
<motion.p
className="font-mono text-[10px] uppercase tracking-[0.3em] text-[rgba(204,255,0,0.7)]"
initial={{ y: 20 }}
animate={{ y: 0 }}
transition={{ delay: 1, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
Software Engineer
</motion.p>
<span className="relative block h-2 w-2" aria-hidden="true">
<span className="absolute inset-0 bg-[var(--slash)]" />
{!reduceMotion && (
<motion.span
className="absolute inset-0 bg-[var(--slash)]"
animate={{ scale: [1, 2.8], opacity: [0.8, 0] }}
transition={{
duration: 0.8,
repeat: Number.POSITIVE_INFINITY,
}}
/>
)}
</span>
<p className="font-mono text-[11px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.72)]">
Preparando o portfólio
</p>
<span
className="h-px flex-1 bg-[rgba(244,242,237,.22)]"
aria-hidden="true"
/>
</motion.div>
</div>
{/* Corner markers */}
<div className="pointer-events-none absolute inset-0">
{/* Top-left */}
<motion.div
className="absolute left-8 top-8"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
transition={{ delay: 0.5 }}
>
<div className="h-6 w-px bg-[var(--slash)]" />
<div className="absolute left-0 top-0 h-px w-6 bg-[var(--slash)]" />
</motion.div>
{/* Top-right */}
<motion.div
className="absolute right-8 top-8"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
transition={{ delay: 0.6 }}
>
<div className="absolute right-0 top-0 h-6 w-px bg-[var(--slash)]" />
<div className="absolute right-0 top-0 h-px w-6 bg-[var(--slash)]" />
</motion.div>
{/* Bottom-left */}
<motion.div
className="absolute bottom-8 left-8"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
transition={{ delay: 0.7 }}
>
<div className="h-6 w-px bg-[var(--slash)]" />
<div className="absolute bottom-0 left-0 h-px w-6 bg-[var(--slash)]" />
</motion.div>
<div className="absolute bottom-8 left-6 right-6 flex justify-between font-mono text-[9px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.45)] lg:bottom-10 lg:left-12 lg:right-12">
<span>Engenharia de software</span>
<span>Garuva · Joinville · Curitiba</span>
</div>
</motion.div>
)}
</AnimatePresence>,
document.body
document.body,
);
}