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:
1 parent
9236fdd685
commit
d76bbf5974
41 files changed
+1359
-492
No files matched your search
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user