diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index ebe092a..6f41ee7 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,8 +1,4 @@ -"use client"; - -import { motion } from "motion/react"; import Link from "next/link"; -import { useReducedMotion } from "@/app/hooks/useReducedMotion"; import HeroSystem from "./cinematic/HeroSystem"; const SOCIAL_LINKS = [ @@ -20,32 +16,43 @@ const SOCIAL_LINKS = [ }, ]; -export default function Hero() { - const shouldReduceMotion = useReducedMotion(); +/** Números reais, calculados em app/page.tsx a partir de public/portfolio-data.json (nada digitado à mão). */ +export type HeroStats = { + years?: number; + projects?: number; + demos?: number; +}; + +function statItems(stats?: HeroStats) { + if (!stats) return []; + return [ + stats.years ? [String(stats.years), "anos de experiência"] : null, + stats.projects ? [String(stats.projects), "projetos publicados"] : null, + stats.demos ? [String(stats.demos), "com demonstração online"] : null, + ].filter((item): item is [string, string] => item !== null); +} + +export default function Hero({ stats }: { stats?: HeroStats }) { + const items = statItems(stats); return (
- +
); } diff --git a/app/components/cinematic/CinematicHome.tsx b/app/components/cinematic/CinematicHome.tsx index 1af3419..6492b17 100644 --- a/app/components/cinematic/CinematicHome.tsx +++ b/app/components/cinematic/CinematicHome.tsx @@ -10,7 +10,7 @@ import Contact from "../Contact"; import EngineeringWithAI from "../EngineeringWithAI"; import Experience from "../Experience"; import Footer from "../Footer"; -import Hero from "../Hero"; +import Hero, { type HeroStats } from "../Hero"; import LighthouseMetrics from "../LighthouseMetrics"; import Projects from "../Projects"; import Skills from "../Skills"; @@ -25,7 +25,9 @@ export default function CinematicHome({ projects, experiences, skillCategories, + heroStats, }: { + heroStats?: HeroStats; projects: PortfolioProjectView[]; experiences: PortfolioExperience[]; skillCategories: PortfolioSkillCategory[]; @@ -35,7 +37,7 @@ export default function CinematicHome({ return (
- +
diff --git a/app/components/cinematic/HeroSystem.tsx b/app/components/cinematic/HeroSystem.tsx index ff5b189..c1e915c 100644 --- a/app/components/cinematic/HeroSystem.tsx +++ b/app/components/cinematic/HeroSystem.tsx @@ -88,11 +88,36 @@ export default function HeroSystem() { } } } - setCanRender(supported); + mountScene(supported); + }; + // A cena 3D (Three.js) so entra depois que a pagina ficou ociosa: titulo, imagem e botoes pintam e hidratam primeiro. + let idle: number | undefined; + let timer: ReturnType | undefined; + const cancelPending = () => { + if (idle !== undefined) window.cancelIdleCallback?.(idle); + if (timer !== undefined) clearTimeout(timer); + idle = timer = undefined; + }; + const mountScene = (value: boolean) => { + cancelPending(); + if (!value) { + setCanRender(false); + return; + } + if (typeof window.requestIdleCallback === "function") { + idle = window.requestIdleCallback(() => setCanRender(true), { + timeout: 2500, + }); + } else { + timer = setTimeout(() => setCanRender(true), 1200); + } }; updateCapability(); preference.addEventListener("change", updateCapability); - return () => preference.removeEventListener("change", updateCapability); + return () => { + cancelPending(); + preference.removeEventListener("change", updateCapability); + }; }, []); useEffect(() => { diff --git a/app/globals.css b/app/globals.css index 9c77e6e..88fbc1b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -976,6 +976,40 @@ textarea { } } +/* Hero: brilho de fundo e entrada em CSS (so posicao; o texto ja nasce visivel para o LCP) */ +.hero-glow { + position: absolute; + inset: 0; + pointer-events: none; + background: + radial-gradient(55% 65% at 78% 38%, rgba(204, 255, 0, 0.11), transparent 70%), + radial-gradient(40% 50% at 6% 92%, rgba(204, 255, 0, 0.05), transparent 70%); +} + +.hero-rise > * { + animation: hero-rise 0.8s var(--ease-out) both; + + &:nth-child(2) { + animation-delay: 0.08s; + } + + &:nth-child(3) { + animation-delay: 0.16s; + } +} + +@keyframes hero-rise { + from { + transform: translateY(16px); + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-rise > * { + animation: none; + } +} + /* Capa dos cards de projeto: captura real ou painel gerado em codigo */ .ds-cover { position: relative; @@ -1021,6 +1055,11 @@ textarea { letter-spacing: -0.05em; color: var(--slash); } + + /* so decorativa: no celular some, para a pagina nao crescer sem informacao nova */ + @media (max-width: 768px) { + display: none; + } } /* Cinematic card hover effect */ diff --git a/app/page.tsx b/app/page.tsx index 7450ea7..d8667eb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,25 @@ import { loadFeaturedProjects, portfolio } from "@/lib/portfolio-data"; import CinematicHome from "./components/cinematic/CinematicHome"; +/** Anos de experiência vêm do próprio texto do portfólio ("5 anos de experiência"); sem o trecho, o número some. */ +function yearsOfExperience() { + const match = portfolio.personal.summary.match(/(\d+)\s+anos de experi/i); + return match ? Number(match[1]) : undefined; +} + export default async function Home() { + const projects = await loadFeaturedProjects(); return ( + project.demoUrl && project.demoAvailability !== "unavailable", + ).length, + }} + projects={projects} experiences={portfolio.experiences} skillCategories={portfolio.skills.technical} />