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 (
-
+
+ {/* Sem opacity:0 inicial: o título já vem visível no HTML (LCP). A entrada anima só a posição, em CSS. */}
+
-
+
+
Evandro Fonseca Junior · Engenharia de software
-
- Sistemas sob medida para operações que não cabem em soluções
- prontas.
+
+ Sistemas sob medida para
+ operações que não cabem em soluções prontas.
-
+
Projeto e desenvolvo sistemas web, aplicativos e integrações, do
entendimento do negócio à arquitetura e à entrega em produção.
@@ -53,9 +60,19 @@ export default function Hero() {
Conversar sobre meu projeto
+
+ {items.length > 0 && (
+
+ {items.map(([value, label]) => (
+
+
-
+ {label}
+
+ -
+ {value}
+
+
+ ))}
+
+ )}
{["Sistemas web", "Aplicativos", "Integrações"].map((service) => (
-
@@ -101,7 +132,7 @@ export default function Hero() {
))}
-
+
);
}
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}
/>