feat: add randomized portfolio hero variants

This commit is contained in:
Evandro Fonseca Junior committed 2026-07-10 02:46:42 -03:00
1 parent 812499b318
commit 9236fdd685
25 files changed
+1438 -658

No files matched your search

+13
View File
@@ -39,3 +39,16 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# local agent/editor state
/.augment/
.opencode/
.claude/settings.local.json
.superpowers/
# generated analysis artifacts
/graphify-out/
*.graphify.json
*.analysis.json
*.tmp.json
*.debug.json
+21
View File
@@ -0,0 +1,21 @@
node_modules/
.next/
dist/
build/
out/
coverage/
.git/
.pnpm-store/
.cache/
*.log
.env
.env.*
__pycache__/
*.pyc
.venv/
venv/
target/
*.exe
*.dll
*.so
*.dylib
+74 -22
View File
@@ -1,36 +1,88 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
<div align="center">
## Getting Started
# Evandro Dev — Portfolio
First, run the development server:
**A cinematic, interactive developer portfolio built with Next.js, React, TypeScript, and motion-first UI engineering.**
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
![Next.js](https://img.shields.io/badge/Next.js-15-000?logo=nextdotjs)
![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=111)
![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=fff)
![Biome](https://img.shields.io/badge/Code%20Style-Biome-60A5FA)
![Jest](https://img.shields.io/badge/Tests-Jest-C21325?logo=jest&logoColor=fff)
</div>
## Overview
This is the portfolio site for **Evandro Dev**. It presents professional work, case studies, technical positioning, and interactive visual storytelling through a high-polish Next.js interface.
The project combines a modern App Router codebase with motion design, 3D/visual effects, and tested UI components.
## Highlights
| Area | Details |
| --- | --- |
| Framework | Next.js App Router with React and TypeScript. |
| Motion | GSAP, Framer Motion, Lenis, and custom animation components. |
| Visuals | Three.js / React Three Fiber / Drei for interactive and cinematic surfaces. |
| Styling | Tailwind CSS, utility composition, and reusable component primitives. |
| Quality | Jest, Testing Library, property-based tests, and Biome checks. |
| Content | Portfolio sections, case pages, company logos, hero composition, and supporting docs. |
## Project structure
```txt
app/
page.tsx Main portfolio route
layout.tsx Root layout and metadata
components/ Portfolio sections and cinematic UI modules
cases/ Case-study routes/content
api/ Server/API routes
components/ Shared UI primitives
lib/ Utilities and shared logic
docs/ Supporting documentation
graphify-out/ Project analysis output
public/ Static assets
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Getting started
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
```bash
pnpm install
pnpm dev
```
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
The development server uses Turbopack through the project script.
## Learn More
## Scripts
To learn more about Next.js, take a look at the following resources:
| Command | Purpose |
| --- | --- |
| `pnpm dev` | Start the Next.js development server. |
| `pnpm build` | Build the production application. |
| `pnpm start` | Start the production server after building. |
| `pnpm lint` | Run Biome checks. |
| `pnpm format` | Format the codebase with Biome. |
| `pnpm test` | Run Jest tests. |
| `pnpm test:watch` | Run Jest in watch mode. |
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## Engineering standards
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
- Keep visual sections modular and testable.
- Respect reduced-motion and accessibility expectations for animated sections.
- Keep portfolio copy and case-study content easy to update without touching low-level animation code.
- Validate interactive components with tests when changing behavior, layout assumptions, or accessibility states.
## Deploy on Vercel
## Validation
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Before publishing changes:
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```bash
pnpm lint
pnpm test
pnpm build
```
## License
Personal portfolio project unless a repository license says otherwise.
+11 -7
View File
@@ -1,7 +1,7 @@
"use client";
"use client";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import profilePhoto from "@/public/edev.jpeg";
import profilePhoto from "@/public/edev.jpg";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import { LazyRevealImage } from "./animations/LazyRevealImage";
@@ -19,6 +19,12 @@ const STORY_DATA = [
content:
"Trabalho bem em contextos com legado, integracoes complexas e pouca margem para erro, priorizando estabilidade, leitura tecnica e evolucao sustentavel.",
},
{
label: "Production mindset",
title: "Do codigo ao sistema no ar",
content:
"Penso alem da feature: deploy, observabilidade, manutencao e riscos de operacao entram na decisao tecnica desde o primeiro corte.",
},
];
const PROFILE_POINTS = [
@@ -91,7 +97,7 @@ export default function About() {
}
>
<motion.div
className="ds-grid-2"
className="ds-grid-2 items-start"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
@@ -128,8 +134,8 @@ export default function About() {
<LazyRevealImage
src={profilePhoto}
alt="Retrato de Evandro"
className="mb-sp-6 h-[220px] w-full overflow-hidden rounded-[6px] border border-[var(--border)]"
imageClassName="object-cover"
className="mb-sp-6 aspect-square w-full overflow-hidden rounded-[6px] border border-[var(--border)]"
imageClassName="object-contain"
/>
<div className="ds-kicker">Technical profile</div>
<DrawLine delay={0.05} />
@@ -158,5 +164,3 @@ export default function About() {
</Section>
);
}
+10 -5
View File
@@ -1,16 +1,22 @@
"use client";
import type React from "react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp";
import { motion, type Variants, useReducedMotion } from "framer-motion";
import { Section } from "./Section";
import { DrawLine } from "./animations/DrawLine";
import MagneticButton from "./cinematic/MagneticButton";
const WHATSAPP_NUMBER = "5547992742365";
export default function Contact() {
const shouldReduceMotion = useReducedMotion();
const [whatsAppDisplay, setWhatsAppDisplay] = useState("WhatsApp");
useEffect(() => {
setWhatsAppDisplay(getWhatsAppDisplayNumber());
}, []);
const fieldClassName =
"mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[13px] leading-6 text-white outline-none transition-[border-color,box-shadow] focus-visible:border-[var(--slash)] focus-visible:ring-2 focus-visible:ring-[rgba(204,255,0,0.34)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]";
const [formData, setFormData] = useState({
@@ -25,8 +31,7 @@ export default function Contact() {
const messageTemplate = `*NOVA SOLICITACAO DE PROJETO*\n\n*Origem:* Portfolio Profissional\n*Prioridade:* Alta\n\n---\n*CLIENTE:* ${formData.name}\n*EMAIL:* ${formData.email}\n*AREA:* ${formData.subject.toUpperCase()}\n\n*DESCRICAO:*\n"${formData.message}"\n---\n_Mensagem gerada via Evandro.dev | Garuva, SC_`;
const encodedMessage = encodeURIComponent(messageTemplate);
const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodedMessage}`;
const whatsappUrl = buildWhatsAppUrl(messageTemplate);
window.open(whatsappUrl, "_blank", "noopener,noreferrer");
};
@@ -93,7 +98,7 @@ export default function Contact() {
projeto, performance ou construcao de produto com ownership real.
</p>
<div className="mt-sp-6 space-y-sp-4 text-[13px] leading-7 text-[rgba(255,255,255,.85)]">
<div>WhatsApp: +55 47 99274-2365</div>
<div>WhatsApp: {whatsAppDisplay}</div>
<div>E-mail: evandrofonsecajunior@gmail.com</div>
<div>Base: Garuva, Santa Catarina | atuacao remota</div>
</div>
+3 -5
View File
@@ -2,6 +2,7 @@
import { useState, ChangeEvent, FormEvent, useEffect } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { buildWhatsAppUrl } from "@/lib/whatsapp";
import { motion } from "motion/react";
interface ContactFormProps {
@@ -173,15 +174,12 @@ ${formData.userName}`;
const handleEmailFocus = () => setEmailFocused(true);
const handlePhoneFocus = () => setPhoneFocused(true);
const generateWhatsAppUrl = () => {
const message = encodeURIComponent(
const generateWhatsAppUrl = () =>
buildWhatsAppUrl(
formType === "advanced"
? generateAdvancedMessage()
: formData.customMessage || getGreetingMessageWithUserName(),
);
const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos
return `https://wa.me/${phone}?text=${message}`;
};
const generateEmailUrl = () => {
const subject = encodeURIComponent("Proposta de Projeto - Portfólio");
+8 -1
View File
@@ -1,8 +1,10 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "motion/react";
import { Mail, MessageCircle } from "lucide-react";
import { usePortfolioData } from "@/lib/usePortfolioData";
import { getWhatsAppDisplayNumber } from "@/lib/whatsapp";
import MagneticButton from "./cinematic/MagneticButton";
interface ContactInfoProps {
@@ -11,6 +13,11 @@ interface ContactInfoProps {
export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
const { data } = usePortfolioData();
const [whatsAppDisplay, setWhatsAppDisplay] = useState("WhatsApp");
useEffect(() => {
setWhatsAppDisplay(getWhatsAppDisplayNumber());
}, []);
if (!data) {
return null; // Ou um componente de carregamento/erro
@@ -58,7 +65,7 @@ export function ContactInfo({ onWhatsAppClick }: ContactInfoProps) {
</div>
<div>
<p className="text-white font-medium">WhatsApp</p>
<p className="text-gray-400">{data.contact.whatsapp}</p>
<p className="text-gray-400">{whatsAppDisplay}</p>
</div>
</MagneticButton>
+60 -42
View File
@@ -1,9 +1,15 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
import Hero from "./Hero";
let mockReducedMotion = false;
jest.mock("next/dynamic", () => () => {
const DynamicComponent = ({ variant }: { variant?: string }) => (
<div data-testid="hero-scene" data-variant={variant} />
);
DynamicComponent.displayName = "MockHeroScene";
return DynamicComponent;
});
jest.mock("framer-motion", () => {
const React = require("react");
@@ -16,15 +22,15 @@ jest.mock("framer-motion", () => {
children,
initial,
animate,
whileHover,
whileTap,
whileHover: _whileHover,
whileTap: _whileTap,
variants: _variants,
transition: _transition,
...props
}: {
children?: ReactNode;
initial?: unknown;
animate?: unknown;
whileHover?: unknown;
whileTap?: unknown;
} & Record<string, unknown>) => {
const dataProps = {
...(initial !== undefined
@@ -33,12 +39,6 @@ jest.mock("framer-motion", () => {
...(animate !== undefined
? { "data-animate": JSON.stringify(animate) }
: {}),
...(whileHover !== undefined
? { "data-while-hover": JSON.stringify(whileHover) }
: {}),
...(whileTap !== undefined
? { "data-while-tap": JSON.stringify(whileTap) }
: {}),
};
return React.createElement(tag, { ...props, ...dataProps }, children);
@@ -53,49 +53,67 @@ jest.mock("framer-motion", () => {
AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
motion,
useInView: () => true,
useReducedMotion: () => mockReducedMotion,
useReducedMotion: () => true,
};
});
function mockMatchMedia(matches: boolean) {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}
describe("Hero reduced motion", () => {
afterEach(() => {
mockReducedMotion = false;
beforeEach(() => {
mockMatchMedia(true);
jest.spyOn(Math, "random").mockReturnValue(0);
});
it("removes hover/tap scale animations when reduced motion is enabled", () => {
mockReducedMotion = true;
afterEach(() => {
jest.restoreAllMocks();
});
it("keeps essential hero content visible with reduced motion enabled", async () => {
render(<Hero />);
await waitFor(() =>
expect(screen.queryByTestId("hero-scene")).not.toBeInTheDocument(),
);
expect(screen.getByText(/Command center online/i)).toBeInTheDocument();
expect(
screen.getByRole("heading", {
level: 1,
name: /coloque seu sistema no ar/i,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /botar meu sistema no ar/i }),
).toBeInTheDocument();
});
it("keeps motion blocks declarative instead of requiring 3D", async () => {
const { container } = render(<Hero />);
const primaryAction = screen.getByText(/Iniciar projeto/i).closest("a");
const secondaryAction = screen.getByText(/Ver projetos/i).closest("a");
expect(primaryAction).not.toHaveAttribute("data-while-hover");
expect(primaryAction).not.toHaveAttribute("data-while-tap");
expect(secondaryAction).not.toHaveAttribute("data-while-hover");
expect(secondaryAction).not.toHaveAttribute("data-while-tap");
await waitFor(() =>
expect(screen.queryByTestId("hero-scene")).not.toBeInTheDocument(),
);
const rootMotionBlock = container.querySelector(
"div[data-initial][data-animate]",
);
expect(rootMotionBlock).toBeInTheDocument();
expect(rootMotionBlock?.getAttribute("data-initial")).toContain(
'"opacity":0',
);
expect(rootMotionBlock?.getAttribute("data-animate")).toContain(
'"opacity":1',
);
});
it("keeps essential hero content visible with reduced motion enabled", () => {
mockReducedMotion = true;
render(<Hero />);
expect(screen.getByLabelText("evandro.dev.br")).toBeInTheDocument();
expect(
screen.getByText(/Available for selected builds/i),
).toBeInTheDocument();
expect(screen.getByText(/Execution Brief/i)).toBeInTheDocument();
expect(rootMotionBlock?.getAttribute("data-initial")).toContain("hidden");
expect(rootMotionBlock?.getAttribute("data-animate")).toContain("visible");
});
});
+87 -23
View File
@@ -1,8 +1,16 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type { ReactNode } from "react";
import Hero from "./Hero";
jest.mock("next/dynamic", () => () => {
const DynamicComponent = ({ variant }: { variant?: string }) => (
<div data-testid="hero-scene" data-variant={variant} />
);
DynamicComponent.displayName = "MockHeroScene";
return DynamicComponent;
});
jest.mock("framer-motion", () => {
const React = require("react");
@@ -12,6 +20,12 @@ jest.mock("framer-motion", () => {
get: (_, tag: string) => {
const Component = ({
children,
initial: _initial,
animate: _animate,
whileHover: _whileHover,
whileTap: _whileTap,
variants: _variants,
transition: _transition,
...props
}: { children?: ReactNode } & Record<string, unknown>) =>
React.createElement(tag, props, children);
@@ -29,34 +43,84 @@ jest.mock("framer-motion", () => {
};
});
function mockMatchMedia(matches = false) {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}
describe("Hero", () => {
it("renders the upgraded hero content and primary actions", () => {
render(<Hero />);
expect(screen.getByLabelText("evandro.dev.br")).toBeInTheDocument();
expect(
screen.getByText(
/Engenharia de software para produtos conectados, interfaces de alta performance e sistemas criticos/i,
),
).toBeInTheDocument();
expect(screen.getByText(/Iniciar projeto/i)).toBeInTheDocument();
expect(screen.getByText(/Ver projetos/i)).toBeInTheDocument();
expect(screen.getByText(/Download CV/i)).toBeInTheDocument();
expect(screen.getByText(/Execution Brief/i)).toBeInTheDocument();
beforeEach(() => {
mockMatchMedia(false);
jest.spyOn(Math, "random").mockReturnValue(0);
jest.spyOn(window, "open").mockImplementation(() => null);
});
it("renders social links and proof content", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("renders the command center hero and primary actions", async () => {
const { container } = render(<Hero />);
await waitFor(() =>
expect(container.querySelector("[data-hero-variant]")).toHaveAttribute(
"data-hero-variant",
"command-center",
),
);
expect(
screen.getByRole("heading", {
level: 1,
name: /coloque seu sistema no ar/i,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /botar meu sistema no ar/i }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /ver casos selecionados/i }),
).toHaveAttribute("href", "/cases");
expect(screen.getByText(/Download CV/i)).toBeInTheDocument();
expect(screen.getByTestId("hero-scene")).toHaveAttribute(
"data-variant",
"command",
);
});
it("keeps social links, proof content and WhatsApp CTA", async () => {
render(<Hero />);
expect(screen.getAllByLabelText("GitHub").length).toBeGreaterThan(0);
expect(screen.getAllByLabelText("LinkedIn").length).toBeGreaterThan(0);
expect(screen.getAllByLabelText("Email").length).toBeGreaterThan(0);
await waitFor(() =>
expect(screen.getByText(/Command center online/i)).toBeInTheDocument(),
);
expect(screen.getByLabelText("GitHub")).toBeInTheDocument();
expect(screen.getByLabelText("LinkedIn")).toBeInTheDocument();
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByText(/Garuva, SC \/ Brasil/i)).toBeInTheDocument();
expect(screen.getByText(/Especialidade/i)).toBeInTheDocument();
expect(screen.getByText(/Escopo/i)).toBeInTheDocument();
expect(screen.getByText(/Formato/i)).toBeInTheDocument();
expect(screen.getByText(/Scroll/i)).toBeInTheDocument();
expect(screen.getByText(/Operacao/i)).toBeInTheDocument();
expect(screen.getByText(/Clareza/i)).toBeInTheDocument();
expect(screen.getAllByText(/Resultado/i).length).toBeGreaterThan(0);
fireEvent.click(
screen.getByRole("button", { name: /botar meu sistema no ar/i }),
);
expect(window.open).toHaveBeenCalledWith(
expect.stringContaining("https://wa.me/"),
"_blank",
"noopener,noreferrer",
);
});
});
+462 -93
View File
@@ -1,13 +1,42 @@
"use client";
import { motion, type Variants } from "framer-motion";
import dynamic from "next/dynamic";
import { motion, useReducedMotion, type Variants } from "framer-motion";
import { useEffect, useState } from "react";
import { buildWhatsAppUrl, getWhatsAppDisplayNumber } from "@/lib/whatsapp";
import MagneticButton from "./cinematic/MagneticButton";
const HeroScene = dynamic(() => import("./cinematic/HeroScene"), {
ssr: false,
});
type HeroSceneVariant =
| "command"
| "launch"
| "rescue"
| "signal"
| "diagnostic"
| "portrait";
type HeroVariant = {
id: string;
weight: number;
scene: HeroSceneVariant;
layout: "default" | "visual-first" | "split" | "report" | "portrait";
status: string;
chip: string;
eyebrow: string;
headline: string;
subtitle: string;
cta: string;
whatsappMessage: string;
sideLabel: string;
sideTitle: string;
sideText: string;
points: string[];
proof: Array<{ label: string; value: string }>;
};
const SOCIAL_LINKS = [
{
href: "https://github.com/evandrodevbr",
@@ -36,50 +65,402 @@ const SOCIAL_LINKS = [
},
];
const HERO_POINTS = [
"Produtos conectados, integracoes complexas e operacoes criticas.",
"Arquitetura mais clara para evoluir sem aumentar o caos tecnico.",
"Execucao ponta a ponta, do discovery a entrega em producao.",
const HERO_VARIANTS: HeroVariant[] = [
{
id: "command-center",
weight: 22,
scene: "command",
layout: "default",
status: "Command center online",
chip: "Sistemas em producao, sem improviso",
eyebrow:
"Evandro Fonseca Junior | Engenharia para produtos que precisam operar",
headline: "Coloque seu sistema no ar com engenharia que nao improvisa.",
subtitle:
"Se voce tem um projeto travado, um MVP para lancar ou um sistema critico precisando operar melhor, agende uma conversa tecnica e saia com o proximo passo claro para producao.",
cta: "Botar meu sistema no ar",
whatsappMessage: "Ola! Quero colocar meu sistema no ar. Podemos conversar?",
sideLabel: "Conversa tecnica pelo WhatsApp",
sideTitle: "Seu projeto precisa virar sistema em producao?",
sideText:
"Me conte objetivo, prazo e contexto. Eu respondo com uma conversa direta para destravar arquitetura, entrega e deploy.",
points: [
"Conversa tecnica para tirar o projeto do papel sem improviso.",
"Arquitetura, codigo e deploy conectados ao resultado do negocio.",
"Plano claro para colocar o sistema no ar com seguranca.",
],
proof: [
{
label: "Operacao",
value: "Fluxo pensado para discovery, build, deploy e sustentacao.",
},
{
label: "Clareza",
value: "Menos ruído entre produto, engenharia e decisao tecnica.",
},
{
label: "Resultado",
value: "Base pronta para operar, evoluir e receber proximo ciclo.",
},
],
},
{
id: "launch-sequence",
weight: 18,
scene: "launch",
layout: "visual-first",
status: "Launch sequence ready",
chip: "Discovery → build → deploy",
eyebrow: "MVP, produto e entrega em producao",
headline:
"Tire seu MVP do plano e coloque em producao com base tecnica real.",
subtitle:
"Transformo escopo solto em sequencia de lancamento: o que validar, o que construir primeiro e como publicar sem criar uma divida tecnica logo no inicio.",
cta: "Lancar meu MVP",
whatsappMessage:
"Ola! Quero lancar um MVP com uma base tecnica segura. Podemos conversar?",
sideLabel: "Sequencia de lancamento",
sideTitle: "Do primeiro corte ao deploy.",
sideText:
"A conversa define prioridade, risco e o menor caminho confiavel para colocar a primeira versao no ar.",
points: [
"Recorte de MVP sem inflar escopo.",
"Entrega incremental com criterio de producao.",
"Deploy e continuidade pensados desde o inicio.",
],
proof: [
{ label: "01", value: "Diagnostico do objetivo e gargalo principal." },
{ label: "02", value: "Plano de construcao com risco visivel." },
{ label: "03", value: "Publicacao e proximo ciclo sem perder contexto." },
],
},
{
id: "rescue-split",
weight: 16,
scene: "rescue",
layout: "split",
status: "Rescue mode available",
chip: "Legado, instabilidade e projeto travado",
eyebrow: "Rescue tecnico para sistemas que precisam voltar a andar",
headline:
"Codigo confuso, entrega travada ou sistema instavel? Vamos organizar.",
subtitle:
"Entro no ponto onde o projeto parou: leio o fluxo real, separo sintoma de causa e transformo caos tecnico em plano executavel.",
cta: "Destravar meu projeto",
whatsappMessage:
"Ola! Tenho um projeto travado ou instavel e quero destravar. Podemos conversar?",
sideLabel: "Antes / depois tecnico",
sideTitle: "Do caos operacional para uma rota clara.",
sideText:
"O primeiro objetivo nao e reescrever tudo. E encontrar o menor corte seguro que devolve controle e previsibilidade.",
points: [
"Leitura rapida de fluxo, risco e acoplamento.",
"Priorizacao do que destrava sem reescrever tudo.",
"Correcoes com criterio para nao trocar um bug por outro.",
],
proof: [
{
label: "Antes",
value: "Backlog confuso, bugs recorrentes e pouca previsibilidade.",
},
{
label: "Intervencao",
value: "Mapa de risco, cortes pequenos e decisao tecnica clara.",
},
{ label: "Depois", value: "Entrega volta a andar com menos improviso." },
],
},
{
id: "iot-signal-map",
weight: 15,
scene: "signal",
layout: "default",
status: "Signal map active",
chip: "Hardware + software + operacao",
eyebrow: "Produtos conectados, telemetria e integracoes criticas",
headline:
"Conecte dispositivo, backend e interface em uma operacao confiavel.",
subtitle:
"Atuo na fronteira que costuma quebrar primeiro: sinal fisico, API, painel, dados e rotina operacional funcionando como um unico produto.",
cta: "Conectar meu produto",
whatsappMessage:
"Ola! Quero conectar hardware, software e operacao em um produto confiavel. Podemos conversar?",
sideLabel: "Mapa de sinais",
sideTitle: "Dados, comandos e painel no mesmo fluxo.",
sideText:
"A conversa identifica fontes de dados, contratos, pontos de falha e o caminho para transformar integracao em produto.",
points: [
"Contratos claros entre dispositivo, API e interface.",
"Telemetria e dashboards com contexto operacional.",
"Base pronta para automacao, IA ou analytics.",
],
proof: [
{
label: "Entrada",
value: "Sensores, dispositivos, eventos ou sistemas externos.",
},
{
label: "Meio",
value: "APIs, WebSockets, filas, banco e observabilidade.",
},
{
label: "Saida",
value: "Painel, operacao, alerta e decisao de produto.",
},
],
},
{
id: "diagnostic-report",
weight: 15,
scene: "diagnostic",
layout: "report",
status: "Diagnostic report open",
chip: "Arquitetura antes de mais codigo",
eyebrow: "Diagnostico tecnico para decidir melhor",
headline: "Antes de escrever mais codigo, descubra o proximo passo certo.",
subtitle:
"Quando a base cresce sem clareza, mais features podem piorar o problema. Eu ajudo a mapear risco, arquitetura e prioridades antes da proxima decisao cara.",
cta: "Agendar diagnostico",
whatsappMessage:
"Ola! Quero um diagnostico tecnico para decidir o proximo passo do meu sistema. Podemos conversar?",
sideLabel: "Relatorio tecnico",
sideTitle: "Risco, arquitetura e rota de acao.",
sideText:
"Ideal para produto que precisa escalar, migrar, estabilizar ou parar de acumular retrabalho tecnico.",
points: [
"Mapa claro dos gargalos e dependencias.",
"Trade-offs traduzidos para decisao de negocio.",
"Plano de transicao sem promessa magica.",
],
proof: [
{ label: "Risco", value: "O que pode quebrar, atrasar ou custar caro." },
{
label: "Arquitetura",
value: "Onde simplificar, separar ou estabilizar primeiro.",
},
{ label: "Acao", value: "Proximo corte de implementacao com criterio." },
],
},
{
id: "cinematic-portrait",
weight: 14,
scene: "portrait",
layout: "portrait",
status: "Direct technical partner",
chip: "Conversa direta com quem implementa",
eyebrow: "Engenharia senior, ownership e execucao",
headline: "Um parceiro tecnico para transformar complexidade em entrega.",
subtitle:
"Voce nao precisa apenas de tela bonita ou codigo isolado. Precisa de alguem que entenda contexto, escolha o corte certo e acompanhe ate o sistema operar.",
cta: "Falar com Evandro",
whatsappMessage:
"Ola Evandro! Quero conversar sobre um projeto e entender como podemos colocar esse sistema no ar.",
sideLabel: "Founder-led engineering",
sideTitle: "Menos repasse, mais ownership.",
sideText:
"A conversa e comigo: contexto tecnico, prioridade, risco e caminho de entrega sem perder informacao entre camadas.",
points: [
"Atendimento consultivo com leitura tecnica real.",
"Do produto ao backend sem perder contexto.",
"Execucao com criterio, nao so codigo por demanda.",
],
proof: [
{
label: "Perfil",
value: "Engenharia full-stack com base em sistemas e integracoes.",
},
{
label: "Postura",
value: "Clareza, autonomia e decisao tecnica pragmatica.",
},
{
label: "Acesso",
value: "WhatsApp direto para conversa tecnica inicial.",
},
],
},
];
const PROOF_ITEMS = [
{
label: "Quem eu ajudo",
value:
"Empresas que dependem de integracoes, hardware ou fluxos criticos para operar com seguranca e previsibilidade.",
},
{
label: "Como entrego",
value:
"Traduzo complexidade tecnica em backlog claro, arquitetura legivel e decisao mais rapida para o time.",
},
{
label: "Por que conversar",
value:
"Voce ganha um parceiro tecnico capaz de conectar produto, backend, interface e operacao sem perder contexto.",
},
];
const LAYOUT_CLASSES: Record<HeroVariant["layout"], string> = {
default:
"grid items-start gap-sp-8 lg:grid-cols-[minmax(0,1.1fr)_minmax(340px,0.9fr)] lg:gap-sp-10",
"visual-first":
"grid items-start gap-sp-8 lg:grid-cols-[minmax(340px,0.9fr)_minmax(0,1.1fr)] lg:gap-sp-10",
split: "grid items-stretch gap-sp-8 lg:grid-cols-2 lg:gap-sp-10",
report:
"grid items-start gap-sp-8 lg:grid-cols-[minmax(360px,0.85fr)_minmax(0,1.15fr)] lg:gap-sp-10",
portrait:
"grid items-center gap-sp-8 lg:grid-cols-[minmax(0,1fr)_minmax(320px,0.72fr)] lg:gap-sp-10",
};
function pickWeightedHeroVariant() {
const total = HERO_VARIANTS.reduce((sum, variant) => sum + variant.weight, 0);
let ticket = Math.random() * total;
for (const variant of HERO_VARIANTS) {
ticket -= variant.weight;
if (ticket <= 0) return variant;
}
return HERO_VARIANTS[0];
}
function getGreeting(message: string) {
const hour = new Date().getHours();
if (hour >= 8 && hour < 12) return `Bom dia! ${message}`;
if (hour >= 13 && hour < 18) return `Boa tarde! ${message}`;
return message;
}
function SideVisual({ variant }: { variant: HeroVariant }) {
if (variant.scene === "launch") {
return (
<div className="mb-sp-6 grid gap-sp-3">
{variant.proof.map((item, index) => (
<div
key={item.label}
className="cinematic-glass flex gap-sp-4 p-sp-4"
>
<span className="font-mono text-[11px] text-[var(--slash)]">
0{index + 1}
</span>
<div>
<div className="text-[12px] font-bold uppercase tracking-[0.08em]">
{item.label}
</div>
<p className="mt-sp-1 text-[12px] leading-6 text-[rgba(244,242,237,.7)]">
{item.value}
</p>
</div>
</div>
))}
</div>
);
}
if (variant.scene === "rescue") {
return (
<div className="mb-sp-6 grid gap-sp-4 sm:grid-cols-2">
<div className="border border-[rgba(255,80,80,.28)] bg-[rgba(255,80,80,.05)] p-sp-4">
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-[rgba(255,160,160,.9)]">
Antes
</div>
<p className="mt-sp-3 text-[13px] leading-6 text-[rgba(244,242,237,.72)]">
Bugs recorrentes, escopo nebuloso e medo de mexer.
</p>
</div>
<div className="border border-[rgba(204,255,0,.35)] bg-[rgba(204,255,0,.06)] p-sp-4">
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--slash)]">
Depois
</div>
<p className="mt-sp-3 text-[13px] leading-6 text-[rgba(244,242,237,.86)]">
Rota clara, cortes menores e entrega voltando a andar.
</p>
</div>
</div>
);
}
if (variant.scene === "signal") {
return (
<div className="relative mb-sp-6 h-40 overflow-hidden border border-[rgba(255,255,255,.08)] bg-[rgba(255,255,255,.03)]">
<svg className="absolute inset-0 h-full w-full" viewBox="0 0 420 160">
<path
d="M28 116 C108 18 190 142 284 56 S380 36 400 28"
fill="none"
stroke="rgba(204,255,0,.75)"
strokeWidth="2"
/>
{[28, 150, 284, 400].map((x, index) => (
<circle
key={x}
cx={x}
cy={[116, 78, 56, 28][index]}
r="6"
fill="#ccff00"
/>
))}
</svg>
</div>
);
}
if (variant.scene === "diagnostic") {
return (
<div className="mb-sp-6 border border-[rgba(255,255,255,.1)] bg-[rgba(255,255,255,.04)] p-sp-5">
<div className="mb-sp-4 flex items-center justify-between border-b border-[rgba(255,255,255,.08)] pb-sp-3 font-mono text-[10px] uppercase tracking-[0.12em]">
<span>Risk report</span>
<span className="text-[var(--slash)]">Actionable</span>
</div>
<div className="space-y-sp-3">
{variant.proof.map((item) => (
<div key={item.label}>
<div className="flex justify-between text-[12px] text-[rgba(244,242,237,.78)]">
<span>{item.label}</span>
<span className="text-[var(--slash)]">mapped</span>
</div>
<div className="mt-sp-2 h-px bg-[linear-gradient(90deg,var(--slash),rgba(255,255,255,.08))]" />
</div>
))}
</div>
</div>
);
}
if (variant.scene === "portrait") {
return (
<div className="mb-sp-6 border border-[rgba(204,255,0,.22)] bg-[radial-gradient(circle_at_70%_20%,rgba(204,255,0,.14),transparent_35%),rgba(255,255,255,.035)] p-sp-5">
<div className="font-display text-[clamp(2.5rem,8vw,5rem)] font-extrabold leading-none tracking-[-0.08em] text-[rgba(244,242,237,.95)]">
EFJ
</div>
<div className="mt-sp-4 font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--slash)]">
Engenharia com contato direto
</div>
</div>
);
}
return (
<div className="mb-sp-6 grid gap-sp-3 font-mono text-[11px] uppercase tracking-[0.08em]">
{["deploy pipeline", "api health", "production ready"].map((item) => (
<div
key={item}
className="flex items-center justify-between border border-[rgba(255,255,255,.1)] bg-[rgba(255,255,255,.04)] px-sp-4 py-sp-3"
>
<span>{item}</span>
<span className="text-[var(--slash)]">ok</span>
</div>
))}
</div>
);
}
export default function Hero() {
const shouldReduceMotion = useReducedMotion();
const [variant, setVariant] = useState<HeroVariant>(HERO_VARIANTS[0]);
const [shouldReduceMotion, setShouldReduceMotion] = useState(false);
const [whatsAppDisplay, setWhatsAppDisplay] = useState("WhatsApp");
const hour = new Date().getHours();
const greeting =
hour >= 8 && hour < 12
? "Ola! Vi seu portfolio. Podemos conversar?"
: hour >= 13 && hour < 18
? "Boa tarde! Vi seu portfolio. Podemos conversar?"
: "Ola! Vi seu portfolio. Podemos conversar quando estiver disponivel?";
useEffect(() => {
setVariant(pickWeightedHeroVariant());
setShouldReduceMotion(
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
);
setWhatsAppDisplay(getWhatsAppDisplayNumber());
}, []);
const handleWhatsAppClick = () => {
window.open(
buildWhatsAppUrl(getGreeting(variant.whatsappMessage)),
"_blank",
"noopener,noreferrer",
);
};
// Cinematic stagger variants
const heroContainer: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
},
transition: { staggerChildren: 0.1, delayChildren: 0.3 },
},
};
@@ -93,10 +474,7 @@ export default function Hero() {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: {
duration: 1,
ease: easeOut,
},
transition: { duration: 1, ease: easeOut },
},
};
@@ -108,26 +486,29 @@ export default function Hero() {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: {
duration: 1.2,
ease: easeOut,
},
transition: { duration: 1.2, ease: easeOut },
},
};
const contentOrder =
variant.layout === "visual-first" || variant.layout === "report"
? "lg:order-2"
: "";
const panelOrder =
variant.layout === "visual-first" || variant.layout === "report"
? "lg:order-1"
: "";
return (
<section
id="home"
className="relative flex min-h-[100svh] w-full items-center overflow-hidden bg-[var(--ink)] text-[#f4f2ed]"
data-hero-variant={variant.id}
>
{/* 3D Scene Background */}
{!shouldReduceMotion && <HeroScene />}
{!shouldReduceMotion && <HeroScene variant={variant.scene} />}
{/* Cinematic overlays */}
<div className="pointer-events-none absolute inset-0 z-[1] bg-[radial-gradient(ellipse_80%_60%_at_50%_40%,transparent_0%,rgba(10,10,10,0.4)_50%,var(--ink)_100%)]" />
<div className="pointer-events-none absolute inset-0 z-[1] bg-gradient-to-b from-transparent via-transparent to-[var(--ink)]" />
{/* Scanlines effect */}
<div
className="pointer-events-none absolute inset-0 z-[2] opacity-[0.015]"
style={{
@@ -135,8 +516,6 @@ export default function Hero() {
"repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)",
}}
/>
{/* Vignette */}
<div className="pointer-events-none absolute inset-0 z-[2] bg-[radial-gradient(circle_at_50%_50%,transparent_40%,rgba(10,10,10,0.6)_100%)]" />
<div className="relative z-10 mx-auto flex w-full max-w-[1240px] flex-col px-sp-6 py-sp-16 lg:px-sp-16 lg:py-sp-12">
@@ -144,10 +523,9 @@ export default function Hero() {
initial="hidden"
animate="visible"
variants={heroContainer}
className="grid items-start gap-sp-8 lg:grid-cols-[minmax(0,1.1fr)_minmax(340px,0.9fr)] lg:gap-sp-10"
className={LAYOUT_CLASSES[variant.layout]}
>
<div className="max-w-[760px]">
{/* Status badge with glow */}
<div className={`max-w-[800px] ${contentOrder}`.trim()}>
<motion.div
className="mb-sp-5 flex flex-wrap items-center gap-sp-3"
variants={heroItem}
@@ -178,48 +556,42 @@ export default function Hero() {
}
/>
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-[rgba(244,242,237,.82)]">
Available for selected builds
{variant.status}
</span>
</div>
<div className="cinematic-glass inline-flex items-center gap-sp-2 px-4 py-2 text-[11px] text-[rgba(244,242,237,.72)]">
Produtos conectados, integracoes criticas e entrega ponta a ponta
{variant.chip}
</div>
</motion.div>
{/* Eyebrow */}
<motion.p
className="mb-sp-4 max-w-[540px] font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--slash)]"
className="mb-sp-4 max-w-[560px] font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--slash)]"
variants={heroItem}
>
Evandro Fonseca Junior | Engenheiro de software full-stack
{variant.eyebrow}
</motion.p>
{/* Main heading with cinematic reveal */}
<motion.h1
className="max-w-[920px] text-balance font-display text-[clamp(3rem,7vw,6.2rem)] font-extrabold leading-[0.9] tracking-[-0.055em]"
className="max-w-[940px] text-balance font-display text-[clamp(3rem,7vw,6.2rem)] font-extrabold leading-[0.9] tracking-[-0.055em]"
variants={heroItemSlow}
>
<span className="cinematic-text-gradient">
Engenharia de software para sistemas que nao podem falhar.
{variant.headline}
</span>
</motion.h1>
{/* Subtitle */}
<motion.p
className="mt-sp-6 max-w-[680px] text-[16px] leading-8 text-[rgba(244,242,237,.88)] lg:text-[17px]"
className="mt-sp-6 max-w-[700px] text-[16px] leading-8 text-[rgba(244,242,237,.88)] lg:text-[17px]"
variants={heroItem}
>
Ajudo empresas com produtos conectados, integracoes complexas e
operacoes criticas a transformar arquitetura confusa em software
estavel, mais rapido de evoluir e muito mais facil de operar.
{variant.subtitle}
</motion.p>
{/* Key points with staggered reveal */}
<motion.ul
className="mt-sp-8 grid gap-sp-3 sm:grid-cols-3"
variants={heroItem}
>
{HERO_POINTS.map((point, i) => (
{variant.points.map((point, i) => (
<motion.li
key={point}
className="cinematic-glass flex min-w-0 gap-sp-3 px-sp-4 py-sp-4 text-[13px] leading-6 text-[rgba(244,242,237,.82)]"
@@ -241,19 +613,19 @@ export default function Hero() {
))}
</motion.ul>
{/* CTAs with magnetic effect */}
<motion.div
className="mt-sp-8 flex flex-wrap items-center gap-sp-4"
variants={heroItem}
>
<MagneticButton
as="a"
href={`https://wa.me/5547992742365?text=${encodeURIComponent(greeting)}`}
as="button"
type="button"
onClick={handleWhatsAppClick}
className="cinematic-cta-primary inline-flex min-h-12 items-center justify-center px-sp-8 font-mono text-[11px] font-bold uppercase tracking-[0.1em] text-[#0A0A0A] no-underline"
data-cursor="View"
strength={0.25}
>
<span className="relative z-10">Agendar conversa tecnica</span>
<span className="relative z-10">{variant.cta}</span>
</MagneticButton>
<MagneticButton
@@ -277,7 +649,6 @@ export default function Hero() {
</MagneticButton>
</motion.div>
{/* Footer info with reveal */}
<motion.div
className="mt-sp-10 flex flex-wrap items-center gap-x-sp-6 gap-y-sp-3 border-t border-[rgba(255,255,255,.06)] pt-sp-6"
variants={heroItem}
@@ -296,6 +667,7 @@ export default function Hero() {
key={link.label}
as="a"
href={link.href}
aria-label={link.label}
target={link.href.startsWith("http") ? "_blank" : undefined}
rel={
link.href.startsWith("http")
@@ -322,9 +694,8 @@ export default function Hero() {
</motion.div>
</div>
{/* Aside card with cinematic entrance */}
<motion.aside
className="cinematic-aside-card relative overflow-hidden p-sp-6 lg:p-sp-8"
className={`cinematic-aside-card relative overflow-hidden p-sp-6 lg:p-sp-8 ${panelOrder}`.trim()}
initial={
shouldReduceMotion
? { opacity: 0 }
@@ -335,13 +706,8 @@ export default function Hero() {
? { opacity: 1 }
: { opacity: 1, x: 0, rotateY: 0 }
}
transition={{
duration: 1.2,
delay: 0.5,
ease: [0.16, 1, 0.3, 1],
}}
transition={{ duration: 1.2, delay: 0.5, ease: [0.16, 1, 0.3, 1] }}
>
{/* Top glow line */}
<motion.div
className="absolute inset-x-0 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(204,255,0,0.8),transparent)]"
initial={{ scaleX: 0 }}
@@ -349,34 +715,38 @@ export default function Hero() {
transition={{ duration: 1.5, delay: 1, ease: [0.16, 1, 0.3, 1] }}
/>
<SideVisual variant={variant} />
<div className="border-b border-[rgba(255,255,255,0.08)] pb-sp-5">
<div className="font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--slash)]">
Why this positioning works
{variant.sideLabel}
</div>
<h2 className="mt-sp-3 font-display text-[clamp(1.35rem,3vw,2rem)] font-extrabold leading-[1.04] tracking-[-0.03em] text-[#f7f4ee]">
Menos atrito entre produto, engenharia e operacao.
{variant.sideTitle}
</h2>
<p className="mt-sp-4 text-[14px] leading-7 text-[rgba(244,242,237,.78)]">
Entro onde a complexidade cresce e ajudo a devolver clareza,
ritmo de entrega e confiabilidade para o sistema e para o time.
{variant.sideText}
</p>
<button
type="button"
onClick={handleWhatsAppClick}
className="mt-sp-5 inline-flex w-full items-center justify-center bg-[var(--slash)] px-sp-5 py-sp-4 font-mono text-[11px] font-bold uppercase tracking-[0.1em] text-[var(--ink)] transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--slash)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--ink)]"
>
Agendar no WhatsApp: {whatsAppDisplay}
</button>
</div>
<div className="mt-sp-6 space-y-sp-5">
{PROOF_ITEMS.map((item, index) => (
{variant.proof.map((item, index) => (
<motion.article
key={item.label}
key={`${variant.id}-${item.label}`}
className="border-b border-[rgba(255,255,255,0.08)] pb-sp-5 last:border-b-0 last:pb-0"
initial={
shouldReduceMotion
? { opacity: 0 }
: { opacity: 0, y: 20, filter: "blur(6px)" }
}
animate={{
opacity: 1,
y: 0,
filter: "blur(0px)",
}}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
duration: 0.8,
delay: 0.8 + index * 0.12,
@@ -396,7 +766,6 @@ export default function Hero() {
</motion.div>
</div>
{/* Scroll indicator */}
<motion.div
className="absolute bottom-8 left-1/2 z-10 -translate-x-1/2"
initial={{ opacity: 0, y: -10 }}
+16 -16
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import { motion, useMotionValue, useSpring } from "framer-motion";
import { motion, useMotionValue } from "framer-motion";
export default function CustomCursor() {
const [isHovering, setIsHovering] = useState(false);
@@ -10,13 +10,6 @@ export default function CustomCursor() {
const [isVisible, setIsVisible] = useState(false);
const cursorX = useMotionValue(0);
const cursorY = useMotionValue(0);
const springConfig = { damping: 25, stiffness: 250, mass: 0.5 };
const x = useSpring(cursorX, springConfig);
const y = useSpring(cursorY, springConfig);
const ringSpringConfig = { damping: 20, stiffness: 150, mass: 0.8 };
const ringX = useSpring(cursorX, ringSpringConfig);
const ringY = useSpring(cursorY, ringSpringConfig);
const rafRef = useRef<number>(0);
@@ -29,7 +22,7 @@ export default function CustomCursor() {
if (!isVisible) setIsVisible(true);
});
},
[cursorX, cursorY, isVisible]
[cursorX, cursorY, isVisible],
);
useEffect(() => {
@@ -57,7 +50,7 @@ export default function CustomCursor() {
function attachHoverListeners() {
const hoverables = document.querySelectorAll(
'a, button, [role="button"], input, textarea, select, [data-cursor]'
'a, button, [role="button"], input, textarea, select, [data-cursor]',
);
for (const el of hoverables) {
el.addEventListener("mouseenter", handleElementEnter as EventListener);
@@ -89,7 +82,10 @@ export default function CustomCursor() {
}, [handleMouseMove]);
// Don't render on touch devices
if (typeof window !== "undefined" && !window.matchMedia("(pointer: fine)").matches) {
if (
typeof window !== "undefined" &&
!window.matchMedia("(pointer: fine)").matches
) {
return null;
}
@@ -105,8 +101,8 @@ export default function CustomCursor() {
<motion.div
className="pointer-events-none fixed top-0 left-0 z-[9999] mix-blend-difference"
style={{
x,
y,
x: cursorX,
y: cursorY,
translateX: "-50%",
translateY: "-50%",
}}
@@ -126,8 +122,8 @@ export default function CustomCursor() {
<motion.div
className="pointer-events-none fixed top-0 left-0 z-[9998] mix-blend-difference"
style={{
x: ringX,
y: ringY,
x: cursorX,
y: cursorY,
translateX: "-50%",
translateY: "-50%",
}}
@@ -141,7 +137,11 @@ export default function CustomCursor() {
>
<div
className="flex h-full w-full items-center justify-center rounded-full border-[#ccff00]"
style={{ borderStyle: "solid", borderColor: "#ccff00", borderWidth: "inherit" }}
style={{
borderStyle: "solid",
borderColor: "#ccff00",
borderWidth: "inherit",
}}
>
{hoverText && (
<motion.span
+96 -20
View File
@@ -1,10 +1,18 @@
"use client";
import { useRef, useMemo, useCallback, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { Float, MeshDistortMaterial } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { useCallback, useEffect, useMemo, useRef } from "react";
import * as THREE from "three";
type HeroSceneVariant =
| "command"
| "launch"
| "rescue"
| "signal"
| "diagnostic"
| "portrait";
function ParticleField({ count = 2000 }: { count?: number }) {
const mesh = useRef<THREE.Points>(null);
const mouseRef = useRef({ x: 0, y: 0 });
@@ -32,10 +40,13 @@ function ParticleField({ count = 2000 }: { count?: number }) {
return s;
}, [count]);
const handlePointerMove = useCallback((e: { clientX: number; clientY: number }) => {
mouseRef.current.x = (e.clientX / window.innerWidth) * 2 - 1;
mouseRef.current.y = -(e.clientY / window.innerHeight) * 2 + 1;
}, []);
const handlePointerMove = useCallback(
(e: { clientX: number; clientY: number }) => {
mouseRef.current.x = (e.clientX / window.innerWidth) * 2 - 1;
mouseRef.current.y = -(e.clientY / window.innerHeight) * 2 + 1;
},
[],
);
useFrame((state) => {
if (!mesh.current) return;
@@ -107,22 +118,61 @@ function ParticleField({ count = 2000 }: { count?: number }) {
);
}
function FloatingGeometry() {
function FloatingGeometry({ variant }: { variant: HeroSceneVariant }) {
const meshRef = useRef<THREE.Mesh>(null);
const mesh2Ref = useRef<THREE.Mesh>(null);
const mesh3Ref = useRef<THREE.Mesh>(null);
const layout = {
command: {
a: [4, 1.5, -3] as [number, number, number],
b: [-5, -2, -4] as [number, number, number],
c: [2, -3, -5] as [number, number, number],
scale: [1.2, 0.8, 1.5] as [number, number, number],
},
launch: {
a: [3.8, 2.1, -3] as [number, number, number],
b: [0, -1.4, -4] as [number, number, number],
c: [-3.8, -2.6, -5] as [number, number, number],
scale: [0.9, 0.9, 1.8] as [number, number, number],
},
rescue: {
a: [-3.6, 1.2, -3] as [number, number, number],
b: [3.6, 1.2, -4] as [number, number, number],
c: [0, -2.8, -5] as [number, number, number],
scale: [1, 1, 1.4] as [number, number, number],
},
signal: {
a: [-4.2, -1, -3] as [number, number, number],
b: [0.2, 1.6, -4] as [number, number, number],
c: [4.2, -0.6, -5] as [number, number, number],
scale: [0.7, 0.7, 1.2] as [number, number, number],
},
diagnostic: {
a: [4.6, 0.6, -3] as [number, number, number],
b: [-3.5, 1.8, -4] as [number, number, number],
c: [-1.2, -2.8, -5] as [number, number, number],
scale: [1.4, 0.7, 1.1] as [number, number, number],
},
portrait: {
a: [3.2, 0.4, -3] as [number, number, number],
b: [4.8, 2.1, -4] as [number, number, number],
c: [-3.6, -2.4, -5] as [number, number, number],
scale: [1.8, 0.5, 1.1] as [number, number, number],
},
}[variant];
useFrame((state) => {
const t = state.clock.elapsedTime;
if (meshRef.current) {
meshRef.current.rotation.x = t * 0.15;
meshRef.current.rotation.y = t * 0.1;
meshRef.current.position.y = Math.sin(t * 0.4) * 0.3;
meshRef.current.position.y = layout.a[1] + Math.sin(t * 0.4) * 0.3;
}
if (mesh2Ref.current) {
mesh2Ref.current.rotation.x = t * 0.1;
mesh2Ref.current.rotation.z = t * 0.12;
mesh2Ref.current.position.y = Math.cos(t * 0.3) * 0.5;
mesh2Ref.current.position.y = layout.b[1] + Math.cos(t * 0.3) * 0.5;
}
if (mesh3Ref.current) {
mesh3Ref.current.rotation.y = t * 0.08;
@@ -133,13 +183,19 @@ function FloatingGeometry() {
return (
<>
<Float speed={1.5} rotationIntensity={0.4} floatIntensity={0.6}>
<mesh ref={meshRef} position={[4, 1.5, -3]} scale={1.2}>
<icosahedronGeometry args={[1, 1]} />
<mesh ref={meshRef} position={layout.a} scale={layout.scale[0]}>
{variant === "diagnostic" ? (
<boxGeometry args={[1.4, 1.8, 0.08, 3, 3, 1]} />
) : variant === "portrait" ? (
<capsuleGeometry args={[0.55, 1.25, 6, 12]} />
) : (
<icosahedronGeometry args={[1, 1]} />
)}
<MeshDistortMaterial
color="#ccff00"
wireframe
transparent
opacity={0.15}
opacity={variant === "portrait" ? 0.1 : 0.15}
distort={0.3}
speed={2}
/>
@@ -147,8 +203,14 @@ function FloatingGeometry() {
</Float>
<Float speed={1.2} rotationIntensity={0.3} floatIntensity={0.8}>
<mesh ref={mesh2Ref} position={[-5, -2, -4]} scale={0.8}>
<octahedronGeometry args={[1, 0]} />
<mesh ref={mesh2Ref} position={layout.b} scale={layout.scale[1]}>
{variant === "launch" ? (
<coneGeometry args={[0.8, 1.6, 4]} />
) : variant === "signal" ? (
<sphereGeometry args={[0.55, 16, 16]} />
) : (
<octahedronGeometry args={[1, 0]} />
)}
<MeshDistortMaterial
color="#ffffff"
wireframe
@@ -161,8 +223,14 @@ function FloatingGeometry() {
</Float>
<Float speed={0.8} rotationIntensity={0.2} floatIntensity={0.5}>
<mesh ref={mesh3Ref} position={[2, -3, -5]} scale={1.5}>
<torusGeometry args={[1, 0.3, 16, 32]} />
<mesh ref={mesh3Ref} position={layout.c} scale={layout.scale[2]}>
{variant === "rescue" ? (
<boxGeometry args={[1.4, 1.4, 1.4, 2, 2, 2]} />
) : variant === "signal" ? (
<torusKnotGeometry args={[0.7, 0.16, 80, 8]} />
) : (
<torusGeometry args={[1, 0.3, 16, 32]} />
)}
<meshStandardMaterial
color="#ccff00"
wireframe
@@ -226,7 +294,7 @@ function GridFloor() {
}
`,
}),
[]
[],
);
return (
@@ -266,7 +334,11 @@ function CameraRig() {
return null;
}
export default function HeroScene() {
export default function HeroScene({
variant = "command",
}: {
variant?: HeroSceneVariant;
}) {
return (
<div
className="absolute inset-0 z-0"
@@ -292,12 +364,16 @@ export default function HeroScene() {
>
<fog attach="fog" args={["#0a0a0a", 5, 25]} />
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} intensity={0.5} color="#ccff00" />
<directionalLight
position={[5, 5, 5]}
intensity={0.5}
color="#ccff00"
/>
<pointLight position={[-5, 3, -5]} intensity={0.3} color="#ffffff" />
<CameraRig />
<ParticleField count={1500} />
<FloatingGeometry />
<FloatingGeometry variant={variant} />
<GridFloor />
</Canvas>
</div>
+23 -1
View File
@@ -62,17 +62,39 @@
padding: 0;
}
html,
body {
width: 100%;
max-width: 100%;
overflow-x: hidden;
overscroll-behavior-x: none;
}
html {
scroll-behavior: smooth;
}
body {
position: relative;
background: var(--paper);
color: var(--ink);
font-family: var(--font-mono);
font-size: 14px;
line-height: 1.68;
overflow-x: hidden;
}
img,
video,
canvas,
svg {
max-width: 100%;
}
@supports (overflow: clip) {
html,
body {
overflow-x: clip;
}
}
a {
-1
View File
@@ -1,6 +1,5 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Evandro Fonseca Junior - Engenheiro de Software";
export const size = {
width: 1200,
@@ -0,0 +1,61 @@
# Random Hero Variations Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build six weighted, randomly selected Hero Section layouts with matching 3D scene modes.
**Architecture:** Add data-driven hero variant configuration in `Hero.tsx`, choose a variant once on client mount, and render layout-specific panel/accent components from that config. Update `HeroScene.tsx` to accept a scene variant prop and switch geometry composition without adding dependencies.
**Tech Stack:** Next.js, React 19, TypeScript, Framer Motion, @react-three/fiber, @react-three/drei, Three.js.
---
### Task 1: Add variant data and weighted picker
**Files:**
- Modify: `app/components/Hero.tsx`
- [ ] Define `HeroVariantId`, `HeroSceneVariant`, and `HeroVariant` types.
- [ ] Add six approved variant objects with copy, points, proof items, side panel mode, layout mode, CTA text, and weights.
- [ ] Add `pickWeightedHeroVariant()` using `Math.random()` and relative weights.
- [ ] Keep `HERO_VARIANTS[0]` as deterministic SSR/default fallback.
### Task 2: Render six hero layouts from shared primitives
**Files:**
- Modify: `app/components/Hero.tsx`
- [ ] Replace fixed hero text with selected variant data.
- [ ] Add layout-specific class maps for Command Center, Launch Sequence, Rescue Split, IoT Signal Map, Diagnostic Report, and Cinematic Portrait.
- [ ] Add side panel renderer that draws distinct visual panels for each mode using existing CSS/Tailwind utilities.
- [ ] Keep primary WhatsApp CTA and secondary cases/CV actions.
### Task 3: Parameterize the 3D scene
**Files:**
- Modify: `app/components/cinematic/HeroScene.tsx`
- [ ] Export/accept a `HeroSceneVariant`-compatible prop locally as string union.
- [ ] Pass the selected scene mode from `Hero.tsx` into `HeroScene`.
- [ ] Change floating geometry positions/geometries per scene mode.
- [ ] Keep reduced motion behavior unchanged: no `HeroScene` render when reduced motion is enabled.
### Task 4: Update tests
**Files:**
- Modify: `app/components/Hero.test.tsx`
- Modify: `app/components/Hero.reducedMotion.test.tsx`
- [ ] Update assertions to current commercial CTA/copy.
- [ ] Assert at least one approved hero layout renders.
- [ ] Assert CTA remains a button and cases/CV remain available.
- [ ] Assert reduced-motion content still renders without animation-specific failures.
### Task 5: Verify
**Commands:**
- `pnpm exec tsc --noEmit --pretty false`
- `pnpm test -- Hero.test.tsx Hero.reducedMotion.test.tsx --runInBand`
- `pnpm build`
**Expected:** all commands exit 0.
@@ -0,0 +1,37 @@
# Random Hero Variations Design
## Goal
Create six structurally different Hero Section variations for the portfolio home page. Each visit should pick one variation randomly using weighted probability, while preserving the existing visual identity: dark cinematic background, slash green accent, glass panels, mono labels, strong display typography, technical lines, and WhatsApp conversion CTA.
## Approved hero variants
1. **Command Center** — Main/default hero. Two-column mission-control layout with production status, deploy/operation language, and strong CTA.
2. **Launch Sequence** — Timeline/checklist layout showing discovery → build → deploy, focused on launching MVPs and putting systems in production.
3. **Before / After Rescue** — Split layout comparing technical chaos vs organized system, focused on legacy, instability, and rescue projects.
4. **IoT Signal Map** — Signal/network layout with nodes and telemetry language, focused on hardware + software, APIs, dashboards, and connected products.
5. **Diagnostic Report** — Report/audit layout with risk, architecture, and next-step panels, focused on technical diagnosis before more code.
6. **Cinematic Portrait** — More personal consultant-led layout with trust, direct conversation, and premium technical partner positioning.
## Weighted distribution
- Command Center: 22
- Launch Sequence: 18
- Before / After Rescue: 16
- IoT Signal Map: 15
- Diagnostic Report: 15
- Cinematic Portrait: 14
Weights are relative. Command Center appears most often because it is the clearest commercial positioning.
## Implementation approach
Keep the implementation lazy and maintainable: one data array defines copy, layout, side panel, points, CTA, and scene mode. `Hero.tsx` renders the same primitives with layout-specific branches instead of six separate full components. `HeroScene.tsx` accepts a `variant` prop and changes the 3D object composition per selected hero.
## Constraints
- Do not expose the WhatsApp number as a complete literal.
- Do not add dependencies.
- Keep accessibility basics: real buttons/links, visible text, keyboard focus.
- Avoid horizontal scroll.
- Reduced motion must continue to hide the 3D scene.
+5 -9
View File
@@ -61,8 +61,7 @@ export const servicePages: ServicePageContent[] = [
],
stack: ["Next.js", "TypeScript", "Node.js", "PostgreSQL", "APIs REST"],
ctaLabel: "Agendar conversa sobre engenharia",
ctaHref:
"https://wa.me/5547992742365?text=Quero%20conversar%20sobre%20engenharia%20de%20software.",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "reddit-explorer", "sc-plus"],
},
{
@@ -96,7 +95,7 @@ export const servicePages: ServicePageContent[] = [
],
stack: ["Next.js", "React", "TypeScript", "Tailwind CSS", "SEO tecnico"],
ctaLabel: "Solicitar consultoria Next.js",
ctaHref: "https://wa.me/5547992742365?text=Quero%20consultoria%20em%20Next.js.",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "sc-plus"],
},
{
@@ -136,8 +135,7 @@ export const servicePages: ServicePageContent[] = [
"System integration",
],
ctaLabel: "Conversar sobre Tauri e Rust",
ctaHref:
"https://wa.me/5547992742365?text=Quero%20conversar%20sobre%20Tauri%20e%20Rust.",
ctaHref: "/#contact",
relatedCaseIds: ["gcc-installer"],
},
{
@@ -170,8 +168,7 @@ export const servicePages: ServicePageContent[] = [
],
stack: ["IoT", "WebSockets", "APIs", "Dashboards", "Integracao de sistemas"],
ctaLabel: "Falar sobre integracao e IoT",
ctaHref:
"https://wa.me/5547992742365?text=Quero%20falar%20sobre%20integracao%20hardware%20%2B%20software%20e%20IoT.",
ctaHref: "/#contact",
relatedCaseIds: ["twins-lab"],
},
{
@@ -211,8 +208,7 @@ export const servicePages: ServicePageContent[] = [
"Sistemas criticos",
],
ctaLabel: "Solicitar diagnostico arquitetural",
ctaHref:
"https://wa.me/5547992742365?text=Quero%20um%20diagnostico%20de%20arquitetura%20de%20software.",
ctaHref: "/#contact",
relatedCaseIds: ["nutriscan", "twins-lab", "reddit-explorer"],
},
];
+17
View File
@@ -0,0 +1,17 @@
const COUNTRY = ["5", "5"];
const AREA = ["4", "7"];
const NINE = ["9"];
const FIRST = ["9", "2", "7", "4"];
const LAST = ["2", "3", "6", "5"];
export function getWhatsAppNumber() {
return [...COUNTRY, ...AREA, ...NINE, ...FIRST, ...LAST].join("");
}
export function getWhatsAppDisplayNumber() {
return `+${COUNTRY.join("")} ${AREA.join("")} ${NINE.join("")}${FIRST.join("")}-${LAST.join("")}`;
}
export function buildWhatsAppUrl(message: string) {
return `https://wa.me/${getWhatsAppNumber()}?text=${encodeURIComponent(message)}`;
}
+3 -12
View File
@@ -9,11 +9,11 @@ const nextConfig: NextConfig = {
minimumCacheTTL: 60,
},
// Otimizações de performance
experimental: {
scrollRestoration: true,
turbopack: {
root: process.cwd(),
},
// Headers de cache e segurança
async headers(): Promise<
Array<{ source: string; headers: Array<{ key: string; value: string }> }>
@@ -59,15 +59,6 @@ const nextConfig: NextConfig = {
},
],
},
{
source: "/_next/image(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
{
source: "/fonts/(.*)",
headers: [
+2 -2
View File
@@ -15,7 +15,7 @@
"@gsap/react": "^2.1.2",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@types/three": "^0.183.1",
"@types/three": "0.182.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cobe": "^0.6.4",
@@ -30,7 +30,7 @@
"react-dom": "19.1.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"three": "^0.183.2"
"three": "0.182.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
+402 -396
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -1,15 +1,39 @@
packages:
- .
allowBuilds:
'@tailwindcss/oxide': true
sharp: true
unrs-resolver: true
onlyBuiltDependencies:
- '@tailwindcss/oxide'
- sharp
overrides:
'@babel/core@<=7.29.0': '>=7.29.6'
brace-expansion@<1.1.13: '>=1.1.13'
brace-expansion@>=2.0.0 <2.0.3: '>=2.0.3'
handlebars@>=4.0.0 <4.7.9: '>=4.7.9'
handlebars@>=4.0.0 <=4.7.8: '>=4.7.9'
handlebars@>=4.6.0 <=4.7.8: '>=4.7.9'
js-yaml@<=4.1.1: '>=4.2.0'
next@>=10.0.0 <15.5.10: '>=15.5.10'
next@>=15.5.1-canary.0 <15.5.10: '>=15.5.10'
next@>=16.0.0 <16.2.5: '>=16.2.5'
next@>=16.0.0 <16.2.6: '>=16.2.6'
next@>=16.0.0-beta.0 <16.1.7: '>=16.1.7'
next@>=16.0.0-beta.0 <16.2.3: '>=16.2.3'
next@>=16.0.1 <16.1.7: '>=16.1.7'
picomatch@<2.3.2: '>=2.3.2'
picomatch@>=4.0.0 <4.0.4: '>=4.0.4'
postcss@<8.5.10: '>=8.5.10'
tar@<7.5.7: '>=7.5.7'
tar@<7.5.8: '>=7.5.8'
tar@<=7.5.10: '>=7.5.11'
tar@<=7.5.15: '>=7.5.16'
tar@<=7.5.2: '>=7.5.3'
tar@<=7.5.3: '>=7.5.4'
tar@<=7.5.9: '>=7.5.10'
ws@>=8.0.0 <8.20.1: '>=8.20.1'
ws@>=8.0.0 <8.21.0: '>=8.21.0'
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+3 -3
View File
@@ -4,7 +4,7 @@
"title": "Desenvolvedor Full-Stack",
"location": "Garuva, Santa Catarina, Brasil",
"email": "[email protected]",
"phone": "+55 47 99274-2365",
"phone": "Disponivel pelo botao de WhatsApp",
"linkedin": "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"github": "https://github.com/evandrodevbr",
"summary": "Desenvolvedor Full-Stack com 3+ anos de experiência, especializado em soluções web modernas e inovadoras. Formado em Engenharia da Computação, com conhecimentos sólidos em computação em nuvem e inteligência artificial. Busco constantemente novos desafios onde possa aplicar minha paixão por tecnologia e resolver problemas de forma criativa."
@@ -367,8 +367,8 @@
},
"contact": {
"email": "[email protected]",
"phone": "+55 47 99274-2365",
"whatsapp": "+55 47 99274-2365",
"phone": "Disponivel pelo botao de WhatsApp",
"whatsapp": "Disponivel pelo botao de WhatsApp",
"linkedin": "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
"github": "https://github.com/evandrodevbr",
"location": "Garuva, Santa Catarina, Brasil"