feat: Initialize portfolio project with core pages, UI components, and configuration files.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-07 10:27:04 -03:00
1 parent aa07d6faeb
commit 0523052864
38 files changed
+1165 -460

No files matched your search

+13 -13
View File
@@ -5,13 +5,13 @@ const STORY_DATA = [
id: "KERNEL_INIT",
title: "Engenharia de fundamentos",
content:
"Projeto sistemas com foco em previsibilidade, manutenção e crescimento real. Escala entra na arquitetura desde o início.",
"Projeto sistemas com foco em previsibilidade, manutenção e crescimento real. Escala entra na arquitetura desde o início.",
},
{
id: "RUNTIME_EXEC",
title: "Entrega sob restrições",
title: "Entrega sob restrições",
content:
"Atuo bem em cenários com legado, prazos apertados e decisões de produto em movimento, sem sacrificar legibilidade técnica.",
"Atuo bem em cenários com legado, prazos apertados e decisões de produto em movimento, sem sacrificar legibilidade técnica.",
},
];
@@ -19,22 +19,22 @@ const VALUES_DATA = [
{
title: "Pragmatismo arquitetural",
description:
"Decisões guiadas por ROI, operabilidade e clareza para o time que mantém o sistema depois.",
"Decisões guiadas por ROI, operabilidade e clareza para o time que mantém o sistema depois.",
},
{
title: "Fundação técnica",
title: "Fundação técnica",
description:
"Fundamentos fortes em dados, protocolos e modelagem para evitar soluções frágeis e modismos vazios.",
"Fundamentos fortes em dados, protocolos e modelagem para evitar soluções frágeis e modismos vazios.",
},
{
title: "Ownership real",
description:
"Responsabilidade sobre entrega, comunicação e qualidade final, não só sobre o trecho de código escrito.",
"Responsabilidade sobre entrega, comunicação e qualidade final, não só sobre o trecho de código escrito.",
},
{
title: "Security by design",
description:
"Integridade, proteção de dados e conformidade tratadas como parte do produto, não como remendo.",
"Integridade, proteção de dados e conformidade tratadas como parte do produto, não como remendo.",
},
];
@@ -43,12 +43,12 @@ export default function About() {
<Section
id="about"
eyebrow="WHOAMI.sys"
title="Engenharia orientada por clareza, não por ruído"
description="Transformo complexidade técnica em sistemas estáveis, operáveis e alinhados ao negócio."
title="Engenharia orientada por clareza, não por ruído"
description="Transformo complexidade técnica em sistemas estáveis, operáveis e alinhados ao negócio."
aside={
<div className="mt-sp-4 ds-metric">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-slash" />
Garuva, SC · Remote worldwide
Garuva, SC · Remote worldwide
</div>
}
>
@@ -65,10 +65,10 @@ export default function About() {
<div className="ds-card">
<div className="ds-kicker">System Signature</div>
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
<h3 className="ds-card-title">Base técnica e postura de entrega</h3>
<p className="ds-copy">
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js,
React, Node.js, Tauri, integrações e arquitetura pragmática.
React, Node.js, Tauri, integrações e arquitetura pragmática.
</p>
<div className="ds-grid-2 mt-sp-6">
{VALUES_DATA.map((value) => (
+15 -11
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import type React from "react";
import { useState } from "react";
@@ -29,23 +29,23 @@ export default function Contact() {
<Section
id="contact"
eyebrow="CONTACT_PROTOCOL.init"
title="Vamos discutir o próximo sistema"
title="Vamos discutir o próximo sistema"
description="Descreva o contexto, o gargalo ou o produto. O contato abre direto no WhatsApp com briefing estruturado."
dark
>
<div className="ds-grid-2">
<div className="ds-card">
<div className="ds-kicker">Response SLA</div>
<h3 className="ds-card-title">Retorno inicial em até 24h</h3>
<h3 className="ds-card-title">Retorno inicial em até 24h</h3>
<p className="ds-copy">
Ideal para discovery técnico, revisão de arquitetura, rescue de
projeto, otimização de performance ou construção de produto do
Ideal para discovery técnico, revisão de arquitetura, rescue de
projeto, otimização de performance ou construção de produto do
zero.
</p>
<div className="mt-sp-6 space-y-sp-4 text-[12px] leading-7 text-[rgba(255,255,255,.7)]">
<div>WhatsApp: +55 47 99274-2365</div>
<div>E-mail: evandrofonsecajunior@gmail.com</div>
<div>Base: Garuva, Santa Catarina · atuação remota</div>
<div>Base: Garuva, Santa Catarina · atuação remota</div>
</div>
</div>
@@ -62,7 +62,8 @@ export default function Contact() {
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.03)] px-sp-4 py-[14px] text-[12px] text-white outline-none transition-colors focus:border-slash"
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{ color: "#fff", backgroundColor: "rgba(255,255,255,0.06)" }}
/>
</div>
<div>
@@ -77,7 +78,8 @@ export default function Contact() {
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.03)] px-sp-4 py-[14px] text-[12px] text-white outline-none transition-colors focus:border-slash"
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{ color: "#fff", backgroundColor: "rgba(255,255,255,0.06)" }}
/>
</div>
<div>
@@ -90,12 +92,13 @@ export default function Contact() {
onChange={(e) =>
setFormData({ ...formData, subject: e.target.value })
}
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.03)] px-sp-4 py-[14px] text-[12px] text-white outline-none transition-colors focus:border-slash"
className="mt-sp-2 w-full border border-[rgba(255,255,255,.12)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{ color: "#fff", backgroundColor: "#1a1a1a" }}
>
<option value="full-stack">Desenvolvimento Full-Stack</option>
<option value="cloud">Cloud & Infrastructure</option>
<option value="iot">Internet of Things (IoT)</option>
<option value="consultancy">Consultoria Técnica Sênior</option>
<option value="consultancy">Consultoria Técnica Sênior</option>
</select>
</div>
<div>
@@ -110,7 +113,8 @@ export default function Contact() {
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
className="mt-sp-2 w-full resize-none border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.03)] px-sp-4 py-[14px] text-[12px] text-white outline-none transition-colors focus:border-slash"
className="mt-sp-2 w-full resize-none border border-[rgba(255,255,255,.12)] bg-[rgba(255,255,255,.06)] px-sp-4 py-[14px] text-[12px] outline-none transition-colors focus:border-slash"
style={{ color: "#fff", backgroundColor: "rgba(255,255,255,0.06)" }}
/>
</div>
<button type="submit" className="ds-link ds-link-accent border-0">
+9 -9
View File
@@ -12,7 +12,7 @@ interface ContactFormProps {
export function ContactForm({ formType, setFormType }: ContactFormProps) {
const { data } = usePortfolioData();
const [contactMethod, setContactMethod] = useState<"whatsapp" | "email">(
"whatsapp"
"whatsapp",
);
const [userName, setUserName] = useState<string>("");
const [userEmail, setUserEmail] = useState<string>("");
@@ -57,8 +57,8 @@ export function ContactForm({ formType, setFormType }: ContactFormProps) {
hour >= 8 && hour < 12
? "manhã"
: hour >= 12 && hour < 18
? "tarde"
: "noite";
? "tarde"
: "noite";
return `Olá Evandro, espero que esta ${dayPart} esteja sendo produtiva.
@@ -119,8 +119,8 @@ ${userName}`;
hasInternalTeam === "sim"
? "Sim"
: hasInternalTeam === "nao"
? "Não"
: "Parcialmente"
? "Não"
: "Parcialmente"
}\n`;
}
@@ -186,7 +186,7 @@ ${userName}`;
};
const handleTechnicalComplexityChange = (
e: ChangeEvent<HTMLSelectElement>
e: ChangeEvent<HTMLSelectElement>,
) => {
setTechnicalComplexity(e.target.value);
};
@@ -203,7 +203,7 @@ ${userName}`;
const message = encodeURIComponent(
formType === "advanced"
? generateAdvancedMessage()
: customMessage || getGreetingMessageWithUserName()
: customMessage || getGreetingMessageWithUserName(),
);
const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos
return `https://wa.me/${phone}?text=${message}`;
@@ -216,7 +216,7 @@ ${userName}`;
? generateAdvancedMessage()
: `Nome: ${userName}\nEmail: ${userEmail}\nTelefone: ${userPhone}\nEmpresa/Posição: ${userCompany}\nComo conheceu: ${howFound}\n\nTipo de Projeto: ${projectType}\nPrazo Desejado: ${desiredDeadline}\nOrçamento Disponível: ${budgetValue}\nComplexidade Técnica: ${technicalComplexity}\nTem equipe interna: ${hasInternalTeam}\n\nMensagem: ${
customMessage || getGreetingMessageWithUserName()
}`
}`,
);
return `mailto:${data.contact.email}?subject=${subject}&body=${body}`;
};
@@ -367,7 +367,7 @@ ${userName}`;
if (totalValue > 0) {
return `Estimativa: ${Math.round(
hours
hours,
)}h × R$ ${hourlyRate}/h = R$ ${totalValue.toFixed(0)}`;
}
+4 -7
View File
@@ -1,16 +1,13 @@
'use client';
"use client";
import Script from 'next/script';
import Script from "next/script";
const GA_MEASUREMENT_ID = 'G-QTT9QT0T3S';
const GA_MEASUREMENT_ID = "G-QTT9QT0T3S";
export default function GoogleAnalytics() {
return (
<>
<Script
src="/sys/libs/vendor-utils.js"
strategy="afterInteractive"
/>
<Script src="/sys/libs/vendor-utils.js" strategy="afterInteractive" />
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
+39 -63
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import { useState } from "react";
import { Logo } from "./Logo";
@@ -18,31 +18,23 @@ export function Header() {
return (
<header
className="fixed left-0 right-0 top-0 z-50 border-b backdrop-blur-xl"
style={{
borderColor: "rgba(255,255,255,.1)",
background: "rgba(10,10,10,.96)",
}}
className="ds-section-dark fixed inset-x-0 top-0 z-50 h-[52px] w-full border-b border-[rgba(255,255,255,0.06)] bg-[var(--ink)]/95 px-sp-6 backdrop-blur-xl lg:px-sp-16"
>
<div className="mx-auto flex h-[52px] w-full max-w-[1240px] items-center justify-between px-6 lg:px-16">
<div className="ds-section-shell flex h-full items-center justify-between">
<a href="#home" className="flex items-center gap-3 no-underline">
<Logo variant="mark" size="sm" theme="dark" />
<span
className="font-display text-sm font-extrabold tracking-tight"
style={{ color: "#FFFFFF" }}
>
evandro<span style={{ color: "#E8FF00" }}>.dev.br</span>
<Logo variant="mark" size="sm" theme="dark" className="shrink-0" />
<span className="font-display text-sm font-extrabold tracking-tight text-white">
evandro<span className="text-[var(--slash)]">.dev.br</span>
</span>
</a>
<nav className="hidden items-center lg:flex">
<ul className="flex list-none items-center gap-2">
<ul className="flex list-none items-center gap-sp-2">
{navItems.map((item) => (
<li key={item.href}>
<a
href={item.href}
className="rounded-sm px-3 py-2 text-[10px] uppercase tracking-[0.14em] no-underline transition-all duration-200 hover:bg-white/10"
style={{ color: "rgba(255,255,255,.86)" }}
className="ds-link rounded-sm px-sp-3 py-sp-2 text-white/85 transition-colors hover:bg-white/10 hover:text-white"
>
{item.label}
</a>
@@ -51,76 +43,60 @@ export function Header() {
</ul>
</nav>
<div className="flex items-center gap-4">
<span
className="hidden rounded-full border px-[10px] py-1 text-[9px] uppercase tracking-[0.12em] lg:inline-block"
style={{
borderColor: "rgba(255,255,255,.15)",
color: "rgba(255,255,255,.35)",
}}
>
<div className="flex items-center gap-sp-4">
<span className="ds-metric hidden rounded-full border border-white/10 px-[10px] py-1 text-white/40 lg:inline-flex">
v1.0 / 2026
</span>
<a
href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.pdf"
className="hidden rounded-sm px-4 py-2 text-[10px] font-semibold uppercase tracking-[0.1em] no-underline transition-all duration-200 hover:shadow-[0_0_0_1px_rgba(232,255,0,.8),0_8px_24px_rgba(232,255,0,.35)] lg:inline-flex"
style={{ background: "#E8FF00", color: "#0A0A0A" }}
className="ds-link ds-link-accent hidden rounded-sm font-semibold transition-shadow hover:shadow-[0_0_0_1px_rgba(232,255,0,.8),0_8px_24px_rgba(232,255,0,.35)] lg:inline-flex"
>
Download CV
</a>
<button
type="button"
className="flex h-8 w-8 flex-col items-center justify-center gap-1.5 border-none bg-transparent lg:hidden"
className="flex h-8 w-8 flex-col items-center justify-center gap-sp-2 border-none bg-transparent lg:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Menu"
aria-expanded={mobileMenuOpen}
>
<span
className={`h-0.5 w-5 bg-white transition-transform duration-200 ${mobileMenuOpen ? "translate-y-2 rotate-45" : ""}`}
/>
<span
className={`h-0.5 w-5 bg-white transition-opacity duration-200 ${mobileMenuOpen ? "opacity-0" : ""}`}
/>
<span
className={`h-0.5 w-5 bg-white transition-transform duration-200 ${mobileMenuOpen ? "-translate-y-2 -rotate-45" : ""}`}
/>
<span className={`h-0.5 w-5 bg-white transition-transform duration-300 ${mobileMenuOpen ? "translate-y-[6px] rotate-45" : ""}`} />
<span className={`h-0.5 w-5 bg-white transition-opacity duration-300 ${mobileMenuOpen ? "opacity-0" : ""}`} />
<span className={`h-0.5 w-5 bg-white transition-transform duration-300 ${mobileMenuOpen ? "-translate-y-[6px] -rotate-45" : ""}`} />
</button>
</div>
</div>
<div
className={`absolute left-0 right-0 top-[52px] overflow-hidden border-b backdrop-blur-xl transition-all duration-300 lg:hidden ${mobileMenuOpen ? "max-h-[420px] opacity-100" : "max-h-0 opacity-0"}`}
style={{
borderColor: "rgba(255,255,255,.1)",
background: "rgba(10,10,10,.98)",
}}
className={`absolute inset-x-0 top-[52px] overflow-hidden border-b border-[rgba(255,255,255,0.06)] bg-[var(--ink)]/98 px-sp-6 backdrop-blur-xl transition-all duration-300 lg:hidden lg:px-sp-16 ${mobileMenuOpen ? "max-h-[420px] opacity-100" : "max-h-0 opacity-0"}`}
>
<nav className="px-6 py-4">
<ul className="flex list-none flex-col gap-2">
{navItems.map((item) => (
<li key={item.href}>
<div className="ds-section-shell flex flex-col py-sp-6">
<nav className="mb-sp-6">
<ul className="flex list-none flex-col gap-sp-2">
{navItems.map((item) => (
<li key={item.href}>
<a
href={item.href}
className="ds-link block rounded-sm py-sp-3 text-white/85 transition-all hover:pl-sp-2 hover:text-white"
onClick={() => setMobileMenuOpen(false)}
>
{item.label}
</a>
</li>
))}
<li>
<a
href={item.href}
className="block rounded-sm px-3 py-3 text-[11px] uppercase tracking-[0.14em] no-underline transition-all duration-200 hover:bg-white/10"
style={{ color: "rgba(255,255,255,.9)" }}
onClick={() => setMobileMenuOpen(false)}
href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.pdf"
className="ds-link ds-link-accent mt-sp-2 inline-flex rounded-sm font-semibold"
>
{item.label}
Download CV
</a>
</li>
))}
<li>
<a
href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.pdf"
className="mt-2 inline-flex rounded-sm px-4 py-3 text-[10px] font-semibold uppercase tracking-[0.1em] no-underline"
style={{ background: "#E8FF00", color: "#0A0A0A" }}
>
Download CV
</a>
</li>
</ul>
</nav>
</ul>
</nav>
</div>
</div>
</header>
);
+37 -22
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import { Logo } from "./Logo";
@@ -48,15 +48,14 @@ export default function Hero() {
return (
<section
id="home"
className="relative overflow-hidden px-6 py-24 lg:px-16"
style={{ background: "#0A0A0A", color: "#FFFFFF" }}
className="relative overflow-hidden bg-[#0A0A0A] px-sp-4 py-sp-12 text-white lg:px-sp-8"
>
<div className="pointer-events-none absolute inset-0 bg-grid-light opacity-70" />
<div className="pointer-events-none absolute inset-x-0 top-0 h-40 bg-[radial-gradient(circle_at_top,rgba(232,255,0,0.18),transparent_60%)]" />
<div className="relative z-10 mx-auto grid min-h-[calc(100vh-52px)] w-full max-w-[1240px] grid-cols-1 items-center gap-16 lg:grid-cols-2">
<div className="relative z-10 mx-auto grid min-h-[calc(100vh-52px)] w-full max-w-[1240px] grid-cols-1 items-center gap-8 lg:grid-cols-2">
<div>
<div className="mb-8 inline-flex items-center gap-2">
<div className="mb-sp-8 inline-flex items-center gap-sp-2">
<span
className="h-1.5 w-1.5 animate-pulse rounded-full"
style={{ background: "#E8FF00" }}
@@ -70,10 +69,10 @@ export default function Hero() {
</div>
<h1
className="mb-6 font-display text-5xl font-extrabold leading-[0.92] tracking-[-0.04em] md:text-6xl lg:text-7xl xl:text-[5.4rem]"
style={{ color: "#FFFFFF" }}
className="mb-sp-6 text-[13vw] sm:text-7xl md:text-[6rem] lg:text-[7rem] xl:text-[8rem] leading-[0.9] tracking-[-0.01em]"
style={{ color: "#FFFFFF", fontFamily: "'Monument Extended', sans-serif", fontWeight: 800 }}
>
Evandro
evandro
<br />
<span style={{ color: "#E8FF00" }}>.dev</span>
<br />
@@ -81,7 +80,7 @@ export default function Hero() {
</h1>
<p
className="mb-10 max-w-[460px] text-[13px] leading-7"
className="mb-sp-10 max-w-[460px] text-[13px] leading-7"
style={{ color: "rgba(255,255,255,.72)" }}
>
Engenheiro de Software especializado em aplicacoes de alta
@@ -89,16 +88,16 @@ export default function Hero() {
React, Tauri e arquitetura escalavel.
</p>
<div className="mb-8 flex flex-wrap items-center gap-4">
<div className="mb-sp-8 flex flex-wrap items-center gap-sp-4">
<a
href={`https://wa.me/5547992742365?text=${encodeURIComponent(greeting)}`}
className="inline-flex h-11 items-center rounded-sm px-6 text-[11px] font-semibold uppercase tracking-[0.08em] no-underline transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_0_0_1px_rgba(232,255,0,.8),0_8px_24px_rgba(232,255,0,.35)]"
className="inline-flex h-11 items-center rounded-sm px-sp-6 text-[11px] font-semibold uppercase tracking-[0.08em] no-underline transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_0_0_1px_rgba(232,255,0,.8),0_8px_24px_rgba(232,255,0,.35)]"
style={{ background: "#E8FF00", color: "#0A0A0A" }}
>
Iniciar projeto
</a>
<div className="flex items-center gap-2">
<div className="flex items-center gap-sp-2">
{SOCIAL_LINKS.map((link) => (
<a
key={link.label}
@@ -141,7 +140,7 @@ export default function Hero() {
</div>
<div
className="mt-10 grid max-w-[440px] grid-cols-3 gap-px border max-sm:grid-cols-1"
className="mt-sp-10 grid max-w-[440px] grid-cols-3 gap-px border max-sm:grid-cols-1"
style={{
borderColor: "rgba(255,255,255,.12)",
background: "rgba(255,255,255,.08)",
@@ -161,7 +160,7 @@ export default function Hero() {
<span style={{ color: "#E8FF00" }}>{item.accent}</span>
</div>
<div
className="mt-1 text-[9px] uppercase tracking-[0.12em]"
className="mt-sp-1 text-[9px] uppercase tracking-[0.12em]"
style={{ color: "rgba(255,255,255,.35)" }}
>
{item.label}
@@ -171,7 +170,7 @@ export default function Hero() {
</div>
</div>
<div className="flex flex-col items-end gap-6 max-lg:items-start">
<div className="flex flex-col items-end gap-sp-6 max-lg:items-start">
<div
className="relative flex aspect-square w-full max-w-[440px] items-center justify-center border max-lg:max-w-[320px]"
style={{
@@ -183,35 +182,51 @@ export default function Hero() {
className="absolute left-6 top-5 text-[9px] tracking-[0.14em]"
style={{ color: "rgba(255,255,255,.32)" }}
>
D / SLASH MARK
EV / LOGO MARK
</span>
<Logo variant="mark" size="xl" theme="dark" />
<svg
width="280"
height="280"
viewBox="0 0 120 120"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="shrink-0 transition-transform duration-700 hover:scale-110"
>
<rect x="20" y="30" width="12" height="60" fill="currentColor" />
<rect x="32" y="30" width="28" height="12" fill="currentColor" />
<rect x="32" y="54" width="18" height="12" fill="currentColor" />
<rect x="32" y="78" width="28" height="12" fill="currentColor" />
<polygon points="62,90 74,90 92,30 80,30" fill="var(--slash)" />
<circle cx="104" cy="84" r="6" fill="currentColor" />
</svg>
<span
className="absolute bottom-5 right-6 text-[9px] tracking-[0.14em]"
style={{ color: "#E8FF00", opacity: 0.78 }}
style={{ color: "var(--slash)", opacity: 0.78 }}
>
CONCEITO ESCOLHIDO
</span>
</div>
<div
className="flex items-center gap-3 text-[9px] tracking-[0.12em]"
className="flex items-center gap-sp-3 text-[9px] tracking-[0.12em]"
style={{ color: "rgba(255,255,255,.32)" }}
>
<span
className="h-px w-6"
style={{ background: "rgba(255,255,255,.18)" }}
/>
D / Slash mark · conceito final
EV / Full mark · identidade visual
</div>
<div
className="flex items-center gap-3 text-[9px] tracking-[0.12em]"
className="flex items-center gap-sp-3 text-[9px] tracking-[0.12em]"
style={{ color: "rgba(255,255,255,.32)" }}
>
<span
className="h-px w-6"
style={{ background: "rgba(255,255,255,.18)" }}
/>
Escala: 280x200 · viewbox
Escala: 280x280 · svg viewbox
</div>
</div>
</div>
+160
View File
@@ -0,0 +1,160 @@
"use client";
interface LogoProps {
variant?: "mark" | "wordmark" | "icon";
size?: "sm" | "md" | "lg" | "xl";
theme?: "dark" | "light" | "accent";
className?: string;
showTagline?: boolean;
}
const sizeMap = {
sm: { width: 32, height: 32 },
md: { width: 48, height: 48 },
lg: { width: 64, height: 64 },
xl: { width: 120, height: 80 },
};
export function Logo({
variant = "mark",
size = "md",
theme = "dark",
className = "",
showTagline = false,
}: Readonly<LogoProps>) {
const dimensions = sizeMap[size];
const colors = {
dark: {
primary: "#FFFFFF",
slash: "#E8FF00",
},
light: {
primary: "#0A0A0A",
slash: "#0A0A0A",
},
accent: {
primary: "#0A0A0A",
slash: "rgba(0,0,0,.3)",
},
};
const c = colors[theme];
if (variant === "icon") {
return (
<svg
width={dimensions.width}
height={dimensions.height}
viewBox="0 0 64 64"
fill="none"
className={className}
role="img"
aria-label="Evandro icon logo"
>
<title>Evandro icon logo</title>
<rect width="64" height="64" rx="14" fill={c.primary} />
<rect
x="10"
y="10"
width="7"
height="44"
fill={theme === "dark" ? "#0A0A0A" : c.primary}
/>
<rect
x="17"
y="10"
width="18"
height="7"
fill={theme === "dark" ? "#0A0A0A" : c.primary}
/>
<rect
x="17"
y="28.5"
width="14"
height="7"
fill={theme === "dark" ? "#0A0A0A" : c.primary}
/>
<rect
x="17"
y="47"
width="18"
height="7"
fill={theme === "dark" ? "#0A0A0A" : c.primary}
/>
<polygon points="37,38 41,38 50,26 46,26" fill={c.slash} />
<circle
cx="52"
cy="47"
r="5"
fill={theme === "dark" ? "#0A0A0A" : c.primary}
/>
</svg>
);
}
if (variant === "wordmark") {
return (
<div className={`flex items-center gap-3 ${className}`}>
<svg
width={dimensions.width}
height={dimensions.height}
viewBox="0 0 48 32"
fill="none"
role="img"
aria-label="Evandro wordmark logo"
>
<title>Evandro wordmark logo</title>
<rect x="2" y="2" width="7" height="28" fill={c.primary} />
<rect x="9" y="2" width="20" height="7" fill={c.primary} />
<rect x="9" y="12.5" width="16" height="7" fill={c.primary} />
<rect x="9" y="23" width="20" height="7" fill={c.primary} />
<polygon points="30,22 34,22 42,10 38,10" fill={c.slash} />
<circle cx="44" cy="25" r="4" fill={c.primary} />
</svg>
<div>
<div
className="font-display text-lg font-extrabold tracking-tight"
style={{ color: c.primary }}
>
evandro<span style={{ color: c.slash }}>.</span>dev
<span style={{ color: c.slash }}>.</span>br
</div>
{showTagline && (
<div
className="mt-1 text-[9px] tracking-widest"
style={{
color:
theme === "dark"
? "rgba(255,255,255,.25)"
: "rgba(10,10,10,.4)",
}}
>
DESENVOLVIMENTO DE SOFTWARE
</div>
)}
</div>
</div>
);
}
return (
<svg
width={dimensions.width}
height={dimensions.height}
viewBox="0 0 120 80"
fill="none"
className={className}
role="img"
aria-label="Evandro mark logo"
>
<title>Evandro mark logo</title>
<rect x="10" y="8" width="9" height="64" fill={c.primary} />
<rect x="19" y="8" width="28" height="9" fill={c.primary} />
<rect x="19" y="35.5" width="22" height="9" fill={c.primary} />
<rect x="19" y="63" width="28" height="9" fill={c.primary} />
<polygon points="50,55 57,55 74,25 67,25" fill={c.slash} />
<circle cx="80" cy="63" r="7" fill={c.primary} />
</svg>
);
}
+3 -3
View File
@@ -1,4 +1,4 @@
"use client";
"use client";
import { useMemo } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData";
@@ -21,8 +21,8 @@ export default function Projects() {
<Section
id="projects"
eyebrow="PRODUCAO_INTELECTUAL"
title="Projetos com recorte técnico claro e impacto observável"
description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
title="Projetos com recorte técnico claro e impacto observável"
description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
dark
>
<div className="ds-grid-2">
+40
View File
@@ -0,0 +1,40 @@
import type React from "react";
interface SectionProps {
id?: string;
eyebrow?: string;
title: string;
description?: string;
dark?: boolean;
aside?: React.ReactNode;
children: React.ReactNode;
}
export const Section = ({
id,
eyebrow,
title,
description,
dark,
aside,
children,
}: SectionProps) => {
const baseClass = dark ? "ds-section ds-section-dark" : "ds-section";
return (
<section id={id} className={baseClass}>
<div className="ds-section-shell">
<div className="ds-section-header">
<div className="ds-section-meta">
{eyebrow && <p className="ds-eyebrow">{eyebrow}</p>}
<h2 className="ds-section-title">{title}</h2>
{description && (
<p className="ds-section-description">{description}</p>
)}
</div>
{aside && <aside>{aside}</aside>}
</div>
<div>{children}</div>
</div>
</section>
);
};
+2 -2
View File
@@ -15,8 +15,8 @@ export default function Skills() {
<Section
id="skills"
eyebrow="MODULE_REGISTRY.lib"
title="Stack modular para produto, operação e escala"
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
title="Stack modular para produto, operação e escala"
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
dark
>
<div className="ds-grid-2">
+225 -81
View File
@@ -1,108 +1,252 @@
"use client";
import { useState } from "react";
import { Section } from "./Section";
const WorkModalitiesData = [
interface Tier {
id: string;
badge: string;
level: string;
title: string;
tagline: string;
description: string;
deliverables: string[];
pricing: string;
pricingNote: string;
featured?: boolean;
ctaLabel: string;
ctaHref: string;
}
const TIERS: Tier[] = [
{
title: "Intervenção Ad-hoc",
value: "R$ 35 - 70/hora",
badge: "Agilidade",
idealFor:
"Resolução de gargalos técnicos específicos e consultoria de arquitetura imediata.",
id: "discovery",
badge: "Entrada / Baixo Risco",
level: "01",
title: "Auditoria & Diagnóstico Técnico",
tagline: "Fotografia clara do problema antes de qualquer comprometimento.",
description:
"Engenharia sob demanda com foco em troubleshooting, performance e decisões rápidas com rastreabilidade.",
benefits: [
"Diagnóstico sênior imediato",
"Sem compromisso de longo prazo",
"Documentação de alterações",
"Análise profunda de arquitetura, performance ou segurança com entrega de um plano de ação executável. Ideal para quem precisa de direção antes de contratar execução.",
deliverables: [
"Relatório de gargalos e dívida técnica",
"Mapeamento de riscos e dependências",
"Sugestão de stack e cronograma de mitigação",
"Plano de ação priorizado",
],
pricing: "Valor Fixo",
pricingNote: "Contrato único. Entrega em até 5 dias úteis.",
featured: false,
ctaLabel: "Solicitar diagnóstico",
ctaHref: "#contact",
},
{
title: "Alocação de sprint",
value: "Eficiência financeira",
badge: "Mais escolhido",
idealFor:
"Times que precisam de entrega contínua com previsibilidade de orçamento e prioridade técnica.",
id: "cycles",
badge: "Mais Escolhido",
level: "02",
title: "Ciclos de Engenharia",
tagline: "Capacidade produtiva garantida. Entrega a cada 2 semanas.",
description:
"Pacotes de horas pré-pagas com redução de custo unitário e espaço reservado na agenda para evolução constante.",
benefits: [
"Taxa horária reduzida",
"Garantia de disponibilidade",
"Prioridade em bugs críticos",
"Desenvolvimento contínuo com a previsibilidade de orçamento do escopo fechado e a agilidade de sprints. Milestones claros, sem amarras de contrato longo.",
deliverables: [
"Features em produção a cada ciclo",
"Refatoração iterativa com rastreabilidade",
"Documentação técnica atualizada",
"Capacidade de pivotar prioridades no início de cada sprint",
],
pricing: "Fixo por Ciclo",
pricingNote: "Assinatura quinzenal ou mensal. Cancele a qualquer ciclo.",
featured: true,
ctaLabel: "Iniciar ciclo de engenharia",
ctaHref: "#contact",
},
{
title: "Engenheiro dedicado",
value: "A partir de R$ 2.800/mês",
badge: "Estratégico",
idealFor:
"Empresas que precisam de liderança técnica contínua, coordenação de backlog e direção arquitetural.",
id: "fractional",
badge: "Premium / High-Ticket",
level: "03",
title: "Fractional Tech Lead",
tagline: "Liderança técnica senior. Sem o custo de um CTO full-time.",
description:
"Parceria recorrente para sustentação, evolução de sistema, revisão de dívida técnica e tomada de decisão.",
benefits: [
"SLA prioritário",
"Gestão de débito técnico",
"Consultoria estratégica",
],
},
{
title: "Squad por projeto",
value: "Escopo fechado",
badge: "Controle total",
idealFor:
"Produtos novos, replatforming ou migrações com orçamento definido por entregáveis e marcos claros.",
description:
"Execução orientada a milestones com redução de risco através de definição objetiva de escopo e critérios de done.",
benefits: [
"Estimativa de riscos inclusa",
"Cronograma de entregas",
"Garantia pós-entrega",
"Direção arquitetural, gestão de risco e sustentação de operações críticas. Para empresas que precisam de decisões de alto impacto com responsabilidade contínua.",
deliverables: [
"SLA prioritário Zero-Risk",
"Code review e liderança do time interno",
"Decisões de infra: Cloud, VPS, CI/CD",
"Consultoria estratégica recorrente",
],
pricing: "Retainer Mensal",
pricingNote: "Renovação mensal. Vagas limitadas.",
featured: false,
ctaLabel: "Agendar conversa estratégica",
ctaHref: "#contact",
},
];
function TierCard({ tier }: { tier: Tier }) {
const [hovered, setHovered] = useState(false);
return (
<article
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
tabIndex={0}
style={{
border: tier.featured ? "2px solid #0A0A0A" : "1px solid rgba(10,10,10,0.1)",
background: tier.featured ? "#0A0A0A" : "#fff",
padding: "var(--sp-6)",
display: "flex",
flexDirection: "column",
gap: "var(--sp-4)",
position: "relative",
transition: "border-color 0.3s ease, box-shadow 0.3s ease",
outline: "none",
cursor: "default",
boxShadow: hovered
? "0 8px 32px rgba(10,10,10,0.1)"
: "0 1px 4px rgba(10,10,10,0.04)",
}}
>
{/* Top row */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<span
style={{
fontSize: "9px",
letterSpacing: "0.16em",
textTransform: "uppercase",
color: tier.featured ? "#E8FF00" : "rgba(10,10,10,0.4)",
}}
>
{tier.badge}
</span>
<span
style={{
fontFamily: "var(--font-display)",
fontSize: "11px",
color: tier.featured ? "rgba(255,255,255,0.15)" : "rgba(10,10,10,0.12)",
fontWeight: 800,
letterSpacing: "0.04em",
}}
>
{tier.level}
</span>
</div>
{/* Title */}
<div>
<h3
style={{
fontFamily: "var(--font-display)",
fontSize: "clamp(18px, 2.2vw, 26px)",
lineHeight: 1.1,
letterSpacing: "-0.02em",
fontWeight: 800,
color: tier.featured ? "#fff" : "#0A0A0A",
}}
>
{tier.title}
</h3>
<p
style={{
marginTop: "var(--sp-2)",
fontSize: "11px",
color: tier.featured ? "rgba(232,255,0,0.85)" : "rgba(10,10,10,0.5)",
lineHeight: 1.6,
}}
>
{tier.tagline}
</p>
</div>
{/* Description */}
<p style={{ fontSize: "12px", color: tier.featured ? "rgba(255,255,255,0.7)" : "rgba(10,10,10,0.65)", lineHeight: 1.8 }}>
{tier.description}
</p>
{/* Deliverables */}
<ul
style={{
paddingLeft: "var(--sp-5)",
fontSize: "12px",
color: tier.featured ? "rgba(255,255,255,0.6)" : "rgba(10,10,10,0.6)",
lineHeight: 1.9,
flexGrow: 1,
}}
>
{tier.deliverables.map((d) => (
<li key={d} style={{ listStyleType: "disc" }}>
{d}
</li>
))}
</ul>
{/* Pricing */}
<div
style={{
borderTop: tier.featured ? "1px solid rgba(255,255,255,0.1)" : "1px solid rgba(10,10,10,0.08)",
paddingTop: "var(--sp-4)",
}}
>
<div
style={{
fontFamily: "var(--font-display)",
fontSize: "clamp(14px, 1.5vw, 18px)",
fontWeight: 800,
color: tier.featured ? "#E8FF00" : "#0A0A0A",
letterSpacing: "-0.01em",
}}
>
{tier.pricing}
</div>
<div style={{ fontSize: "10px", color: tier.featured ? "rgba(255,255,255,0.3)" : "rgba(10,10,10,0.35)", marginTop: "4px", letterSpacing: "0.06em" }}>
{tier.pricingNote}
</div>
</div>
{/* CTA — reveals on hover/focus */}
<a
href={tier.ctaHref}
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "12px 18px",
fontSize: "10px",
letterSpacing: "0.12em",
textTransform: "uppercase",
textDecoration: "none",
fontWeight: 700,
background: tier.featured ? "#E8FF00" : "#0A0A0A",
color: tier.featured ? "#0A0A0A" : "#fff",
transition: "opacity 0.25s ease, transform 0.25s ease",
opacity: hovered ? 1 : 0,
transform: hovered ? "translateY(0)" : "translateY(6px)",
pointerEvents: hovered ? "auto" : "none",
}}
>
{tier.ctaLabel} →
</a>
</article>
);
}
export function WorkModalities() {
return (
<Section
id="modalities"
eyebrow="CONTRACT_PROTOCOLS.sys"
title="Formatos de contratação para contextos técnicos distintos"
description="Modelos pensados para reduzir overhead, preservar velocidade de decisão e manter previsibilidade."
title="Engenharia sob demanda. Previsibilidade de entrega sem overhead."
description="Três níveis de contratação desenhados como uma esteira de valor — do diagnóstico rápido à parceria estratégica de longo prazo."
>
<div className="ds-grid-4 mb-sp-6">
{[
["Agências tradicionais", "Alto overhead"],
["Consultorias big tech", "Custo inflacionado"],
["Freelancers generalistas", "Risco técnico"],
["Engenheiro especialista", "High ROI"],
].map(([label, value]) => (
<div key={label} className="ds-panel">
<div className="ds-kicker">{label}</div>
<div className="mt-sp-3 font-display text-2xl font-extrabold tracking-[-0.02em]">
{value}
</div>
</div>
))}
</div>
<div className="ds-grid-2">
{WorkModalitiesData.map((modality) => (
<article key={modality.title} className="ds-card">
<div className="flex items-center justify-between gap-sp-4">
<span className="ds-kicker">{modality.badge}</span>
<span className="ds-tag">{modality.value}</span>
</div>
<h3 className="ds-card-title">{modality.title}</h3>
<p className="ds-copy">{modality.description}</p>
<div className="mt-sp-5 rounded-[var(--r-md)] bg-[rgba(10,10,10,.03)] p-sp-4 text-[11px] leading-7 text-ink-80">
Ideal para: {modality.idealFor}
</div>
<ul className="ds-list">
{modality.benefits.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</article>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "var(--sp-6)",
}}
>
{TIERS.map((tier) => (
<TierCard key={tier.id} tier={tier} />
))}
</div>
</Section>
+140
View File
@@ -0,0 +1,140 @@
// src/components/ui/NumberIcons.tsx
import React from 'react';
interface NumberIconProps extends React.SVGProps<SVGSVGElement> {
/** Tamanho em pixels. Escala mantendo o viewBox 80x80 */
size?: number | string;
/** Classe opcional para sobreposição de estilos */
className?: string;
}
const baseProps = {
fill: 'none',
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 80 80',
};
/**
* Primitive SVG elements compartilhados.
* Depende da classe pai 'group' para engatilhar as micro-interações.
*/
const GraphicShell = () => (
<>
{/* Base Background */}
<rect width="80" height="80" rx="8" className="fill-[#0A0A0A]" />
{/* Borda reativa ao hover */}
<rect
x="1" y="1" width="78" height="78" rx="7"
className="stroke-white/5 transition-colors duration-400 ease-[var(--ease-out)] group-hover:stroke-white/15"
strokeWidth="1"
/>
{/* Grid de background sutil (Cyberpunk/Tech feel) */}
<g className="stroke-white/[0.03]" strokeWidth="1">
<path d="M0 20H80M0 60H80M20 0V80M60 0V80" />
</g>
{/* Brand Slash (Acento) com Motion */}
<polygon
points="58,66 62,66 70,50 66,50"
className="fill-[#E8FF00] transition-transform duration-400 ease-[var(--ease-out)] origin-bottom-right group-hover:-translate-y-1 group-hover:scale-110"
/>
</>
);
export const NumberOne = ({ size = 80, className = '', ...props }: NumberIconProps) => (
<svg
width={size}
height={size}
className={`group ${className}`}
role="img"
aria-label="Passo 1"
{...baseProps}
{...props}
>
<GraphicShell />
<text
x="40" y="54"
fontFamily="Syne, sans-serif"
fontSize="40"
fontWeight="800"
textAnchor="middle"
className="fill-white transition-transform duration-400 ease-[var(--ease-out)] group-hover:-translate-y-0.5"
>
1
</text>
</svg>
);
export const NumberTwo = ({ size = 80, className = '', ...props }: NumberIconProps) => (
<svg
width={size}
height={size}
className={`group ${className}`}
role="img"
aria-label="Passo 2"
{...baseProps}
{...props}
>
<GraphicShell />
<text
x="40" y="54"
fontFamily="Syne, sans-serif"
fontSize="40"
fontWeight="800"
textAnchor="middle"
className="fill-white transition-transform duration-400 ease-[var(--ease-out)] group-hover:-translate-y-0.5"
>
2
</text>
</svg>
);
export const NumberThree = ({ size = 80, className = '', ...props }: NumberIconProps) => (
<svg
width={size}
height={size}
className={`group ${className}`}
role="img"
aria-label="Passo 3"
{...baseProps}
{...props}
>
<GraphicShell />
<text
x="40" y="54"
fontFamily="Syne, sans-serif"
fontSize="40"
fontWeight="800"
textAnchor="middle"
className="fill-white transition-transform duration-400 ease-[var(--ease-out)] group-hover:-translate-y-0.5"
>
3
</text>
</svg>
);
export const NumberFour = ({ size = 80, className = '', ...props }: NumberIconProps) => (
<svg
width={size}
height={size}
className={`group ${className}`}
role="img"
aria-label="Passo 4"
{...baseProps}
{...props}
>
<GraphicShell />
<text
x="40" y="54"
fontFamily="Syne, sans-serif"
fontSize="40"
fontWeight="800"
textAnchor="middle"
className="fill-white transition-transform duration-400 ease-[var(--ease-out)] group-hover:-translate-y-0.5"
>
4
</text>
</svg>
);
+33 -1
View File
@@ -1,9 +1,40 @@
@import url("https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Geist+Mono:wght@300;400;500;600&display=swap");
@font-face {
font-family: 'Monument Extended';
src: url('/fonts/MonumentExtended-Regular.otf') format('opentype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Monument Extended';
src: url('/fonts/MonumentExtended-Ultrabold.otf') format('opentype');
font-weight: 800;
font-style: normal;
font-display: swap;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@theme {
--spacing-sp-1: var(--sp-1);
--spacing-sp-2: var(--sp-2);
--spacing-sp-3: var(--sp-3);
--spacing-sp-4: var(--sp-4);
--spacing-sp-5: var(--sp-5);
--spacing-sp-6: var(--sp-6);
--spacing-sp-8: var(--sp-8);
--spacing-sp-10: var(--sp-10);
--spacing-sp-12: var(--sp-12);
--spacing-sp-16: var(--sp-16);
--spacing-sp-20: var(--sp-20);
--spacing-sp-24: var(--sp-24);
}
:root {
--ink: #0a0a0a;
--ink-80: rgba(10, 10, 10, 0.8);
@@ -35,6 +66,7 @@
--r-lg: 16px;
--r-full: 999px;
--font-display: "Syne", sans-serif;
--font-hero: 'Monument Extended', sans-serif;
--font-mono: "Geist Mono", monospace;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
@@ -100,7 +132,7 @@ textarea {
border-bottom: 1px solid var(--border);
}
.ds-section:nth-of-type(even) {
.ds-section:nth-of-type(even):not(.ds-section-dark) {
background: var(--paper-dark);
}
+17 -14
View File
@@ -4,7 +4,8 @@ import GoogleAnalytics from "./components/GoogleAnalytics";
import "./globals.css";
export const metadata: Metadata = {
title: "Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicações de alta performance e sistemas críticos. Desenvolvimento com Next.js, React, Tauri e arquitetura escalável.",
keywords: [
@@ -42,7 +43,8 @@ export const metadata: Metadata = {
canonical: "/",
},
openGraph: {
title: "Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicações de alta performance e sistemas críticos. Desenvolvimento com Next.js, React, Tauri e arquitetura escalável.",
url: "https://evandro.dev.br",
@@ -60,7 +62,8 @@ export const metadata: Metadata = {
},
twitter: {
card: "summary_large_image",
title: "Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description:
"Engenheiro Full-Stack especializado em aplicações de alta performance e sistemas críticos. Desenvolvimento com Next.js, React, Tauri e arquitetura escalável.",
creator: "@evandrodevbr",
@@ -94,17 +97,17 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "ProfilePage",
"mainEntity": {
mainEntity: {
"@type": "Person",
"name": "Evandro Fonseca Junior",
"alternateName": "Evandro.dev",
"jobTitle": "Engenheiro de Software",
"url": "https://evandro.dev.br",
"sameAs": [
name: "Evandro Fonseca Junior",
alternateName: "Evandro.dev",
jobTitle: "Engenheiro de Software",
url: "https://evandro.dev.br",
sameAs: [
"https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
],
"knowsAbout": [
knowsAbout: [
"Software Engineering",
"Next.js",
"React",
@@ -119,11 +122,11 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
"PostgreSQL",
"MySQL",
],
"worksFor": {
worksFor: {
"@type": "Organization",
"name": "Freelancer / Consultor Independente",
name: "Freelancer / Consultor Independente",
},
"description":
description:
"Engenheiro de software especializado em aplicações de alta performance e sistemas críticos. Desenvolvimento com Next.js 15, React 19, Tauri e arquitetura escalável. Experiência em automação, computação em nuvem e sistemas distribuídos.",
},
};
@@ -143,4 +146,4 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
</body>
</html>
);
}
}
+20 -19
View File
@@ -1,29 +1,30 @@
import { MetadataRoute } from 'next'
import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Evandro Fonseca Junior - Portfólio',
short_name: 'Evandro.dev',
description: 'Portfólio profissional de Evandro Fonseca Junior, Desenvolvedor Full-Stack',
start_url: '/',
display: 'standalone',
background_color: '#000000',
theme_color: '#000000',
orientation: 'portrait-primary',
categories: ['portfolio', 'developer', 'technology'],
lang: 'pt-BR',
dir: 'ltr',
name: "Evandro Fonseca Junior - Portfólio",
short_name: "Evandro.dev",
description:
"Portfólio profissional de Evandro Fonseca Junior, Desenvolvedor Full-Stack",
start_url: "/",
display: "standalone",
background_color: "#000000",
theme_color: "#000000",
orientation: "portrait-primary",
categories: ["portfolio", "developer", "technology"],
lang: "pt-BR",
dir: "ltr",
icons: [
{
src: '/favicon.ico',
sizes: 'any',
type: 'image/x-icon',
src: "/favicon.ico",
sizes: "any",
type: "image/x-icon",
},
{
src: '/icon.svg',
sizes: 'any',
type: 'image/svg+xml',
src: "/icon.svg",
sizes: "any",
type: "image/svg+xml",
},
],
}
};
}
+4 -2
View File
@@ -7,7 +7,9 @@ export default function NotFound() {
<div className="max-w-2xl mx-auto text-center">
<div className="mb-8">
<Code2 className="w-24 h-24 text-blue-500 mx-auto mb-4 opacity-50" />
<h1 className="text-6xl md:text-8xl font-black text-white mb-4">404</h1>
<h1 className="text-6xl md:text-8xl font-black text-white mb-4">
404
</h1>
<h2 className="text-2xl md:text-3xl font-bold text-slate-400 mb-4">
Página não encontrada
</h2>
@@ -15,7 +17,7 @@ export default function NotFound() {
A página que você está procurando não existe ou foi movida.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/"
+36 -38
View File
@@ -1,51 +1,49 @@
import { ImageResponse } from 'next/og'
import { ImageResponse } from "next/og";
export const runtime = 'edge'
export const alt = 'Evandro Fonseca Junior - Engenheiro de Software'
export const runtime = "edge";
export const alt = "Evandro Fonseca Junior - Engenheiro de Software";
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
};
export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
(
<div
style={{
background: "#030712",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: "white",
fontFamily: "system-ui",
}}
>
<div
style={{
background: '#030712',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontFamily: 'system-ui',
fontSize: 64,
fontWeight: "bold",
marginBottom: 20,
textAlign: "center",
}}
>
<div
style={{
fontSize: 64,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
}}
>
Evandro Fonseca Junior
</div>
<div
style={{
fontSize: 32,
color: '#3b82f6',
marginTop: 10,
textAlign: 'center',
}}
>
Engenheiro de Software
</div>
Evandro Fonseca Junior
</div>
),
{ ...size }
)
<div
style={{
fontSize: 32,
color: "#3b82f6",
marginTop: 10,
textAlign: "center",
}}
>
Engenheiro de Software
</div>
</div>,
{ ...size },
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ import { WorkModalities } from "./components/WorkModalities";
export default function Home() {
return (
<div>
<div className="flex min-h-screen flex-col overflow-x-hidden">
<Header />
<main className="ds-main">
<Hero />
+6 -6
View File
@@ -1,14 +1,14 @@
import { MetadataRoute } from 'next'
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/_next/', '/admin/'],
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/", "/admin/"],
},
],
sitemap: 'https://evandro.dev.br/sitemap.xml',
}
sitemap: "https://evandro.dev.br/sitemap.xml",
};
}
+10 -10
View File
@@ -1,45 +1,45 @@
import { MetadataRoute } from 'next'
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://evandro.dev.br'
const currentDate = new Date().toISOString()
const baseUrl = "https://evandro.dev.br";
const currentDate = new Date().toISOString();
return [
{
url: baseUrl,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 1.0,
},
{
url: `${baseUrl}/#about`,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/#skills`,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/#experience`,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/#projects`,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/#contact`,
lastModified: currentDate,
changeFrequency: 'monthly',
changeFrequency: "monthly",
priority: 0.8,
},
]
];
}
+41 -27
View File
@@ -1,7 +1,7 @@
'use client';
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
import { gsap } from 'gsap';
import { InertiaPlugin } from 'gsap/InertiaPlugin';
"use client";
import React, { useRef, useEffect, useCallback, useMemo } from "react";
import { gsap } from "gsap";
import { InertiaPlugin } from "gsap/InertiaPlugin";
gsap.registerPlugin(InertiaPlugin);
@@ -46,15 +46,15 @@ function hexToRgb(hex: string) {
return {
r: parseInt(m[1], 16),
g: parseInt(m[2], 16),
b: parseInt(m[3], 16)
b: parseInt(m[3], 16),
};
}
const DotGrid: React.FC<DotGridProps> = ({
dotSize = 16,
gap = 32,
baseColor = '#5227FF',
activeColor = '#5227FF',
baseColor = "#5227FF",
activeColor = "#5227FF",
proximity = 150,
speedTrigger = 100,
shockRadius = 250,
@@ -62,8 +62,8 @@ const DotGrid: React.FC<DotGridProps> = ({
maxSpeed = 5000,
resistance = 750,
returnDuration = 1.5,
className = '',
style
className = "",
style,
}) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -76,14 +76,14 @@ const DotGrid: React.FC<DotGridProps> = ({
speed: 0,
lastTime: 0,
lastX: 0,
lastY: 0
lastY: 0,
});
const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);
const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);
const circlePath = useMemo(() => {
if (typeof window === 'undefined' || !window.Path2D) return null;
if (typeof window === "undefined" || !window.Path2D) return null;
const p = new Path2D();
p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);
@@ -102,7 +102,7 @@ const DotGrid: React.FC<DotGridProps> = ({
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (ctx) ctx.scale(dpr, dpr);
const cols = Math.floor((width + gap) / (dotSize + gap));
@@ -138,7 +138,7 @@ const DotGrid: React.FC<DotGridProps> = ({
const draw = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -178,15 +178,15 @@ const DotGrid: React.FC<DotGridProps> = ({
useEffect(() => {
buildGrid();
let ro: ResizeObserver | null = null;
if ('ResizeObserver' in window) {
if ("ResizeObserver" in window) {
ro = new ResizeObserver(buildGrid);
wrapperRef.current && ro.observe(wrapperRef.current);
} else {
(window as Window).addEventListener('resize', buildGrid);
(window as Window).addEventListener("resize", buildGrid);
}
return () => {
if (ro) ro.disconnect();
else window.removeEventListener('resize', buildGrid);
else window.removeEventListener("resize", buildGrid);
};
}, [buildGrid]);
@@ -231,10 +231,10 @@ const DotGrid: React.FC<DotGridProps> = ({
xOffset: 0,
yOffset: 0,
duration: returnDuration,
ease: 'elastic.out(1,0.75)'
ease: "elastic.out(1,0.75)",
});
dot._inertiaApplied = false;
}
},
});
}
}
@@ -259,29 +259,43 @@ const DotGrid: React.FC<DotGridProps> = ({
xOffset: 0,
yOffset: 0,
duration: returnDuration,
ease: 'elastic.out(1,0.75)'
ease: "elastic.out(1,0.75)",
});
dot._inertiaApplied = false;
}
},
});
}
}
};
const throttledMove = throttle(onMove, 50);
window.addEventListener('mousemove', throttledMove, { passive: true });
window.addEventListener('click', onClick);
window.addEventListener("mousemove", throttledMove, { passive: true });
window.addEventListener("click", onClick);
return () => {
window.removeEventListener('mousemove', throttledMove);
window.removeEventListener('click', onClick);
window.removeEventListener("mousemove", throttledMove);
window.removeEventListener("click", onClick);
};
}, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]);
}, [
maxSpeed,
speedTrigger,
proximity,
resistance,
returnDuration,
shockRadius,
shockStrength,
]);
return (
<section className={`p-4 flex items-center justify-center h-full w-full relative ${className}`} style={style}>
<section
className={`p-4 flex items-center justify-center h-full w-full relative ${className}`}
style={style}
>
<div ref={wrapperRef} className="w-full h-full relative">
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full pointer-events-none" />
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full pointer-events-none"
/>
</div>
</section>
);
+3 -3
View File
@@ -284,7 +284,7 @@ export default function FaultyTerminal({
const ditherValue = useMemo(
() => (typeof dither === "boolean" ? (dither ? 1 : 0) : dither),
[dither]
[dither],
);
const handleMouseMove = useCallback((e: MouseEvent) => {
@@ -316,7 +316,7 @@ export default function FaultyTerminal({
value: new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height
gl.canvas.width / gl.canvas.height,
),
},
uScale: { value: scale },
@@ -354,7 +354,7 @@ export default function FaultyTerminal({
program.uniforms.iResolution.value = new Color(
gl.canvas.width,
gl.canvas.height,
gl.canvas.width / gl.canvas.height
gl.canvas.width / gl.canvas.height,
);
}
+109 -79
View File
@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
export type LogoItem =
| {
@@ -21,7 +27,7 @@ export type LogoItem =
export interface LogoLoopProps {
logos: LogoItem[];
speed?: number;
direction?: 'left' | 'right';
direction?: "left" | "right";
width?: number | string;
logoHeight?: number;
gap?: number;
@@ -37,28 +43,29 @@ export interface LogoLoopProps {
const ANIMATION_CONFIG = {
SMOOTH_TAU: 0.25,
MIN_COPIES: 2,
COPY_HEADROOM: 2
COPY_HEADROOM: 2,
} as const;
const toCssLength = (value?: number | string): string | undefined =>
typeof value === 'number' ? `${value}px` : (value ?? undefined);
typeof value === "number" ? `${value}px` : (value ?? undefined);
const cx = (...parts: Array<string | false | null | undefined>) => parts.filter(Boolean).join(' ');
const cx = (...parts: Array<string | false | null | undefined>) =>
parts.filter(Boolean).join(" ");
const useResizeObserver = (
callback: () => void,
elements: Array<React.RefObject<Element | null>>,
dependencies: React.DependencyList
dependencies: React.DependencyList,
) => {
useEffect(() => {
if (!window.ResizeObserver) {
const handleResize = () => callback();
window.addEventListener('resize', handleResize);
window.addEventListener("resize", handleResize);
callback();
return () => window.removeEventListener('resize', handleResize);
return () => window.removeEventListener("resize", handleResize);
}
const observers = elements.map(ref => {
const observers = elements.map((ref) => {
if (!ref.current) return null;
const observer = new ResizeObserver(callback);
observer.observe(ref.current);
@@ -68,7 +75,7 @@ const useResizeObserver = (
callback();
return () => {
observers.forEach(observer => observer?.disconnect());
observers.forEach((observer) => observer?.disconnect());
};
}, dependencies);
};
@@ -76,10 +83,10 @@ const useResizeObserver = (
const useImageLoader = (
seqRef: React.RefObject<HTMLUListElement | null>,
onLoad: () => void,
dependencies: React.DependencyList
dependencies: React.DependencyList,
) => {
useEffect(() => {
const images = seqRef.current?.querySelectorAll('img') ?? [];
const images = seqRef.current?.querySelectorAll("img") ?? [];
if (images.length === 0) {
onLoad();
@@ -94,20 +101,20 @@ const useImageLoader = (
}
};
images.forEach(img => {
images.forEach((img) => {
const htmlImg = img as HTMLImageElement;
if (htmlImg.complete) {
handleImageLoad();
} else {
htmlImg.addEventListener('load', handleImageLoad, { once: true });
htmlImg.addEventListener('error', handleImageLoad, { once: true });
htmlImg.addEventListener("load", handleImageLoad, { once: true });
htmlImg.addEventListener("error", handleImageLoad, { once: true });
}
});
return () => {
images.forEach(img => {
img.removeEventListener('load', handleImageLoad);
img.removeEventListener('error', handleImageLoad);
images.forEach((img) => {
img.removeEventListener("load", handleImageLoad);
img.removeEventListener("error", handleImageLoad);
});
};
}, dependencies);
@@ -118,7 +125,7 @@ const useAnimationLoop = (
targetVelocity: number,
seqWidth: number,
isHovered: boolean,
pauseOnHover: boolean
pauseOnHover: boolean,
) => {
const rafRef = useRef<number | null>(null);
const lastTimestampRef = useRef<number | null>(null);
@@ -130,17 +137,18 @@ const useAnimationLoop = (
if (!track) return;
const prefersReduced =
typeof window !== 'undefined' &&
typeof window !== "undefined" &&
window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (seqWidth > 0) {
offsetRef.current = ((offsetRef.current % seqWidth) + seqWidth) % seqWidth;
offsetRef.current =
((offsetRef.current % seqWidth) + seqWidth) % seqWidth;
track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
}
if (prefersReduced) {
track.style.transform = 'translate3d(0, 0, 0)';
track.style.transform = "translate3d(0, 0, 0)";
return () => {
lastTimestampRef.current = null;
};
@@ -151,12 +159,14 @@ const useAnimationLoop = (
lastTimestampRef.current = timestamp;
}
const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000;
const deltaTime =
Math.max(0, timestamp - lastTimestampRef.current) / 1000;
lastTimestampRef.current = timestamp;
const target = pauseOnHover && isHovered ? 0 : targetVelocity;
const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
const easingFactor =
1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
velocityRef.current += (target - velocityRef.current) * easingFactor;
if (seqWidth > 0) {
@@ -187,72 +197,87 @@ export const LogoLoop = React.memo<LogoLoopProps>(
({
logos,
speed = 120,
direction = 'left',
width = '100%',
direction = "left",
width = "100%",
logoHeight = 28,
gap = 32,
pauseOnHover = true,
fadeOut = false,
fadeOutColor,
scaleOnHover = false,
ariaLabel = 'Partner logos',
ariaLabel = "Partner logos",
className,
style
style,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null);
const seqRef = useRef<HTMLUListElement>(null);
const [seqWidth, setSeqWidth] = useState<number>(0);
const [copyCount, setCopyCount] = useState<number>(ANIMATION_CONFIG.MIN_COPIES);
const [copyCount, setCopyCount] = useState<number>(
ANIMATION_CONFIG.MIN_COPIES,
);
const [isHovered, setIsHovered] = useState<boolean>(false);
const targetVelocity = useMemo(() => {
const magnitude = Math.abs(speed);
const directionMultiplier = direction === 'left' ? 1 : -1;
const directionMultiplier = direction === "left" ? 1 : -1;
const speedMultiplier = speed < 0 ? -1 : 1;
return magnitude * directionMultiplier * speedMultiplier;
}, [speed, direction]);
const updateDimensions = useCallback(() => {
const containerWidth = containerRef.current?.clientWidth ?? 0;
const sequenceWidth = seqRef.current?.getBoundingClientRect?.()?.width ?? 0;
const sequenceWidth =
seqRef.current?.getBoundingClientRect?.()?.width ?? 0;
if (sequenceWidth > 0) {
setSeqWidth(Math.ceil(sequenceWidth));
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
const copiesNeeded =
Math.ceil(containerWidth / sequenceWidth) +
ANIMATION_CONFIG.COPY_HEADROOM;
setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
}
}, []);
useResizeObserver(updateDimensions, [containerRef, seqRef], [logos, gap, logoHeight]);
useResizeObserver(
updateDimensions,
[containerRef, seqRef],
[logos, gap, logoHeight],
);
useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight]);
useAnimationLoop(trackRef, targetVelocity, seqWidth, isHovered, pauseOnHover);
useAnimationLoop(
trackRef,
targetVelocity,
seqWidth,
isHovered,
pauseOnHover,
);
const cssVariables = useMemo(
() =>
({
'--logoloop-gap': `${gap}px`,
'--logoloop-logoHeight': `${logoHeight}px`,
...(fadeOutColor && { '--logoloop-fadeColor': fadeOutColor })
"--logoloop-gap": `${gap}px`,
"--logoloop-logoHeight": `${logoHeight}px`,
...(fadeOutColor && { "--logoloop-fadeColor": fadeOutColor }),
}) as React.CSSProperties,
[gap, logoHeight, fadeOutColor]
[gap, logoHeight, fadeOutColor],
);
const rootClasses = useMemo(
() =>
cx(
'relative overflow-x-hidden group',
'[--logoloop-gap:32px]',
'[--logoloop-logoHeight:28px]',
'[--logoloop-fadeColorAuto:#ffffff]',
'dark:[--logoloop-fadeColorAuto:#0b0b0b]',
scaleOnHover && 'py-[calc(var(--logoloop-logoHeight)*0.1)]',
className
"relative overflow-x-hidden group",
"[--logoloop-gap:32px]",
"[--logoloop-logoHeight:28px]",
"[--logoloop-fadeColorAuto:#ffffff]",
"dark:[--logoloop-fadeColorAuto:#0b0b0b]",
scaleOnHover && "py-[calc(var(--logoloop-logoHeight)*0.1)]",
className,
),
[scaleOnHover, className]
[scaleOnHover, className],
);
const handleMouseEnter = useCallback(() => {
@@ -265,15 +290,15 @@ export const LogoLoop = React.memo<LogoLoopProps>(
const renderLogoItem = useCallback(
(item: LogoItem, key: React.Key) => {
const isNodeItem = 'node' in item;
const isNodeItem = "node" in item;
const content = isNodeItem ? (
<span
className={cx(
'inline-flex items-center',
'motion-reduce:transition-none',
"inline-flex items-center",
"motion-reduce:transition-none",
scaleOnHover &&
'transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120'
"transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120",
)}
aria-hidden={!!(item as any).href && !(item as any).ariaLabel}
>
@@ -282,19 +307,19 @@ export const LogoLoop = React.memo<LogoLoopProps>(
) : (
<img
className={cx(
'h-[var(--logoloop-logoHeight)] w-auto block object-contain',
'[-webkit-user-drag:none] pointer-events-none',
'[image-rendering:-webkit-optimize-contrast]',
'motion-reduce:transition-none',
"h-[var(--logoloop-logoHeight)] w-auto block object-contain",
"[-webkit-user-drag:none] pointer-events-none",
"[image-rendering:-webkit-optimize-contrast]",
"motion-reduce:transition-none",
scaleOnHover &&
'transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120'
"transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120",
)}
src={(item as any).src}
srcSet={(item as any).srcSet}
sizes={(item as any).sizes}
width={(item as any).width}
height={(item as any).height}
alt={(item as any).alt ?? ''}
alt={(item as any).alt ?? ""}
title={(item as any).title}
loading="lazy"
decoding="async"
@@ -309,13 +334,13 @@ export const LogoLoop = React.memo<LogoLoopProps>(
const inner = (item as any).href ? (
<a
className={cx(
'inline-flex items-center no-underline rounded',
'transition-opacity duration-200 ease-linear',
'hover:opacity-80',
'focus-visible:outline focus-visible:outline-current focus-visible:outline-offset-2'
"inline-flex items-center no-underline rounded",
"transition-opacity duration-200 ease-linear",
"hover:opacity-80",
"focus-visible:outline focus-visible:outline-current focus-visible:outline-offset-2",
)}
href={(item as any).href}
aria-label={itemAriaLabel || 'logo link'}
aria-label={itemAriaLabel || "logo link"}
target="_blank"
rel="noreferrer noopener"
>
@@ -328,8 +353,8 @@ export const LogoLoop = React.memo<LogoLoopProps>(
return (
<li
className={cx(
'flex-none mr-[var(--logoloop-gap)] text-[length:var(--logoloop-logoHeight)] leading-[1]',
scaleOnHover && 'overflow-visible group/item'
"flex-none mr-[var(--logoloop-gap)] text-[length:var(--logoloop-logoHeight)] leading-[1]",
scaleOnHover && "overflow-visible group/item",
)}
key={key}
role="listitem"
@@ -338,7 +363,7 @@ export const LogoLoop = React.memo<LogoLoopProps>(
</li>
);
},
[scaleOnHover]
[scaleOnHover],
);
const logoLists = useMemo(
@@ -351,19 +376,21 @@ export const LogoLoop = React.memo<LogoLoopProps>(
aria-hidden={copyIndex > 0}
ref={copyIndex === 0 ? seqRef : undefined}
>
{logos.map((item, itemIndex) => renderLogoItem(item, `${copyIndex}-${itemIndex}`))}
{logos.map((item, itemIndex) =>
renderLogoItem(item, `${copyIndex}-${itemIndex}`),
)}
</ul>
)),
[copyCount, logos, renderLogoItem]
[copyCount, logos, renderLogoItem],
);
const containerStyle = useMemo(
(): React.CSSProperties => ({
width: toCssLength(width) ?? '100%',
width: toCssLength(width) ?? "100%",
...cssVariables,
...style
...style,
}),
[width, cssVariables, style]
[width, cssVariables, style],
);
return (
@@ -381,33 +408,36 @@ export const LogoLoop = React.memo<LogoLoopProps>(
<div
aria-hidden
className={cx(
'pointer-events-none absolute inset-y-0 left-0 z-[1]',
'w-[clamp(24px,8%,120px)]',
'bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]'
"pointer-events-none absolute inset-y-0 left-0 z-[1]",
"w-[clamp(24px,8%,120px)]",
"bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]",
)}
/>
<div
aria-hidden
className={cx(
'pointer-events-none absolute inset-y-0 right-0 z-[1]',
'w-[clamp(24px,8%,120px)]',
'bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]'
"pointer-events-none absolute inset-y-0 right-0 z-[1]",
"w-[clamp(24px,8%,120px)]",
"bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]",
)}
/>
</>
)}
<div
className={cx('flex w-max will-change-transform select-none', 'motion-reduce:transform-none')}
className={cx(
"flex w-max will-change-transform select-none",
"motion-reduce:transform-none",
)}
ref={trackRef}
>
{logoLists}
</div>
</div>
);
}
},
);
LogoLoop.displayName = 'LogoLoop';
LogoLoop.displayName = "LogoLoop";
export default LogoLoop;
+1 -1
View File
@@ -80,7 +80,7 @@ const Squares: React.FC<SquaresProps> = ({
0,
canvas.width / 2,
canvas.height / 2,
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2,
);
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
gradient.addColorStop(1, "#060010");
+43 -27
View File
@@ -1,7 +1,15 @@
'use client';
"use client";
import { ElementType, useEffect, useRef, useState, createElement, useMemo, useCallback } from 'react';
import { gsap } from 'gsap';
import {
ElementType,
useEffect,
useRef,
useState,
createElement,
useMemo,
useCallback,
} from "react";
import { gsap } from "gsap";
interface TextTypeProps {
className?: string;
@@ -26,17 +34,17 @@ interface TextTypeProps {
const TextType = ({
text,
as: Component = 'div',
as: Component = "div",
typingSpeed = 50,
initialDelay = 0,
pauseDuration = 2000,
deletingSpeed = 30,
loop = true,
className = '',
className = "",
showCursor = true,
hideCursorWhileTyping = false,
cursorCharacter = '|',
cursorClassName = '',
cursorCharacter = "|",
cursorClassName = "",
cursorBlinkDuration = 0.5,
textColors = [],
variableSpeed,
@@ -45,7 +53,7 @@ const TextType = ({
reverseMode = false,
...props
}: TextTypeProps & React.HTMLAttributes<HTMLElement>) => {
const [displayedText, setDisplayedText] = useState('');
const [displayedText, setDisplayedText] = useState("");
const [currentCharIndex, setCurrentCharIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false);
const [currentTextIndex, setCurrentTextIndex] = useState(0);
@@ -53,7 +61,10 @@ const TextType = ({
const cursorRef = useRef<HTMLSpanElement>(null);
const containerRef = useRef<HTMLElement>(null);
const textArray = useMemo(() => (Array.isArray(text) ? text : [text]), [text]);
const textArray = useMemo(
() => (Array.isArray(text) ? text : [text]),
[text],
);
const getRandomSpeed = useCallback(() => {
if (!variableSpeed) return typingSpeed;
@@ -62,7 +73,7 @@ const TextType = ({
}, [variableSpeed, typingSpeed]);
const getCurrentTextColor = () => {
if (textColors.length === 0) return '#ffffff';
if (textColors.length === 0) return "#ffffff";
return textColors[currentTextIndex % textColors.length];
};
@@ -70,14 +81,14 @@ const TextType = ({
if (!startOnVisible || !containerRef.current) return;
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
});
},
{ threshold: 0.1 }
{ threshold: 0.1 },
);
observer.observe(containerRef.current);
@@ -92,7 +103,7 @@ const TextType = ({
duration: cursorBlinkDuration,
repeat: -1,
yoyo: true,
ease: 'power2.inOut'
ease: "power2.inOut",
});
}
}, [showCursor, cursorBlinkDuration]);
@@ -103,11 +114,13 @@ const TextType = ({
let timeout: NodeJS.Timeout;
const currentText = textArray[currentTextIndex];
const processedText = reverseMode ? currentText.split('').reverse().join('') : currentText;
const processedText = reverseMode
? currentText.split("").reverse().join("")
: currentText;
const executeTypingAnimation = () => {
if (isDeleting) {
if (displayedText === '') {
if (displayedText === "") {
setIsDeleting(false);
if (currentTextIndex === textArray.length - 1 && !loop) {
return;
@@ -117,22 +130,24 @@ const TextType = ({
onSentenceComplete(textArray[currentTextIndex], currentTextIndex);
}
setCurrentTextIndex(prev => (prev + 1) % textArray.length);
setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
setCurrentCharIndex(0);
timeout = setTimeout(() => {}, pauseDuration);
} else {
timeout = setTimeout(() => {
setDisplayedText(prev => prev.slice(0, -1));
setDisplayedText((prev) => prev.slice(0, -1));
}, deletingSpeed);
}
} else {
if (currentCharIndex < processedText.length) {
timeout = setTimeout(
() => {
setDisplayedText(prev => prev + processedText[currentCharIndex]);
setCurrentCharIndex(prev => prev + 1);
setDisplayedText(
(prev) => prev + processedText[currentCharIndex],
);
setCurrentCharIndex((prev) => prev + 1);
},
variableSpeed ? getRandomSpeed() : typingSpeed
variableSpeed ? getRandomSpeed() : typingSpeed,
);
} else if (textArray.length > 1) {
timeout = setTimeout(() => {
@@ -142,7 +157,7 @@ const TextType = ({
}
};
if (currentCharIndex === 0 && !isDeleting && displayedText === '') {
if (currentCharIndex === 0 && !isDeleting && displayedText === "") {
timeout = setTimeout(executeTypingAnimation, initialDelay);
} else {
executeTypingAnimation();
@@ -163,18 +178,19 @@ const TextType = ({
isVisible,
reverseMode,
variableSpeed,
onSentenceComplete
onSentenceComplete,
]);
const shouldHideCursor =
hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting);
hideCursorWhileTyping &&
(currentCharIndex < textArray[currentTextIndex].length || isDeleting);
return createElement(
Component,
{
ref: containerRef,
className: `inline-block whitespace-pre-wrap tracking-tight ${className}`,
...props
...props,
},
<span className="inline" style={{ color: getCurrentTextColor() }}>
{displayedText}
@@ -182,11 +198,11 @@ const TextType = ({
showCursor && (
<span
ref={cursorRef}
className={`ml-1 inline-block opacity-100 ${shouldHideCursor ? 'hidden' : ''} ${cursorClassName}`}
className={`ml-1 inline-block opacity-100 ${shouldHideCursor ? "hidden" : ""} ${cursorClassName}`}
>
{cursorCharacter}
</span>
)
),
);
};
@@ -16,7 +16,7 @@ export function AnimatedGradientText({
<motion.span
className={cn(
"bg-gradient-to-r from-cyan-400 via-blue-500 to-blue-600 bg-clip-text text-transparent font-bold",
className
className,
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
+1 -1
View File
@@ -13,7 +13,7 @@ export function AuroraText({ children, className }: AuroraTextProps) {
<motion.span
className={cn(
"bg-gradient-to-r from-blue-400 via-cyan-500 to-blue-500 bg-clip-text text-transparent",
className
className,
)}
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
+2 -2
View File
@@ -20,7 +20,7 @@ export function BentoGrid({ children, className }: BentoGridProps) {
<div
className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto justify-center",
className
className,
)}
>
{children}
@@ -39,7 +39,7 @@ export function BentoGridItem({
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm p-6",
`col-span-${colSpan} row-span-${rowSpan}`,
className
className,
)}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
+1 -1
View File
@@ -13,7 +13,7 @@ export function MagicCard({ children, className }: MagicCardProps) {
<motion.div
className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm",
className
className,
)}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
+1 -1
View File
@@ -18,7 +18,7 @@ export function ShimmerButton({
<motion.button
className={cn(
"relative overflow-hidden rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 px-6 py-3 font-semibold text-white shadow-lg",
className
className,
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
+1 -1
View File
@@ -165,7 +165,7 @@ const Particles: React.FC<ParticlesProps> = ({
positions.set([x * r, y * r, z * r], i * 3);
randoms.set(
[Math.random(), Math.random(), Math.random(), Math.random()],
i * 4
i * 4,
);
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
colors.set(col, i * 3);
+3 -3
View File
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}
+58
View File
@@ -0,0 +1,58 @@
<svg width="640" height="260" viewBox="0 0 640 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@800&amp;family=Geist+Mono:wght@400;500&amp;display=swap');
.title {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 32px;
fill: #0A0A0A;
}
.role {
font-family: 'Geist Mono', monospace;
font-size: 14px;
fill: #777777;
}
.contact {
font-family: 'Geist Mono', monospace;
font-size: 14px;
font-weight: 500;
fill: #0A0A0A;
}
.footer {
font-family: 'Geist Mono', monospace;
font-size: 12px;
fill: #777777;
letter-spacing: 0.1em;
}
</style>
</defs>
<rect width="640" height="260" rx="8" fill="#FFFFFF" stroke="#EAEAEA" stroke-width="1.5" />
<path d="M0 200 L0 252 A8 8 0 0 0 8 260 L632 260 A8 8 0 0 0 640 252 L640 200 Z" fill="#F4F2ED" />
<line x1="0" y1="200" x2="640" y2="200" stroke="#EAEAEA" stroke-width="1.5" />
<g transform="translate(45, 60) scale(0.45)">
<rect x="20" y="30" width="12" height="60" fill="#0A0A0A" />
<rect x="32" y="30" width="28" height="12" fill="#0A0A0A" />
<rect x="32" y="54" width="18" height="12" fill="#0A0A0A" />
<rect x="32" y="78" width="28" height="12" fill="#0A0A0A" />
<polygon points="62,90 74,90 92,30 80,30" fill="#E8FF00" />
<circle cx="104" cy="84" r="6" fill="#0A0A0A" />
</g>
<rect x="130" y="55" width="4" height="110" rx="2" fill="#E8FF00" />
<text x="160" y="80" class="title">Evandro</text>
<text x="160" y="108" class="role">Full-Stack Developer · evandro.dev.br</text>
<text x="160" y="145" class="contact">[email protected]</text>
<text x="160" y="170" class="contact">evandro.dev.br · github.com/evandrodevbr</text>
<text x="45" y="234" class="footer">DESENVOLVIMENTO DE SOFTWARE · SANTA CATARINA, BR</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="30" width="12" height="60" fill="currentColor" />
<rect x="32" y="30" width="28" height="12" fill="currentColor" />
<rect x="32" y="54" width="18" height="12" fill="currentColor" />
<rect x="32" y="78" width="28" height="12" fill="currentColor" />
<polygon points="62,90 74,90 92,30 80,30" fill="#E8FF00" />
<circle cx="104" cy="84" r="6" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 510 B

+73
View File
@@ -0,0 +1,73 @@
<svg width="680" height="400" viewBox="0 0 680 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@800&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap');
.bc-name {
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 44px;
fill: #FFFFFF;
}
.bc-role {
font-family: 'Geist Mono', monospace;
font-size: 14px;
font-weight: 500;
fill: rgba(255, 255, 255, 0.4);
letter-spacing: 0.2em;
}
.bc-contact {
font-family: 'Geist Mono', monospace;
font-size: 15px;
fill: rgba(255, 255, 255, 0.5);
}
.bc-contact-hl {
fill: rgba(255, 255, 255, 0.9);
font-weight: 600;
}
</style>
<radialGradient id="grad" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(340 200) rotate(90) scale(400 680)">
<stop stop-color="#1A1A1A" />
<stop offset="1" stop-color="#050505" />
</radialGradient>
</defs>
<!-- Background with subtle depth -->
<rect width="680" height="400" rx="24" fill="url(#grad)" />
<rect x="1" y="1" width="678" height="398" rx="23" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" />
<!-- Decorative pattern -->
<circle cx="680" cy="40" r="320" fill="none" stroke="rgba(232, 255, 0, 0.03)" stroke-width="1" />
<circle cx="680" cy="40" r="240" fill="none" stroke="rgba(255, 255, 255, 0.02)" stroke-width="1" />
<!-- Header Section -->
<g transform="translate(60, 80)">
<text y="10" class="bc-name">evandro<tspan fill="#E8FF00">.dev.br</tspan></text>
<text y="50" class="bc-role">FULL-STACK DEVELOPER</text>
</g>
<!-- Contact Section with Accent Bar -->
<g transform="translate(60, 250)">
<rect x="-20" y="8" width="4" height="96" rx="2" fill="#E8FF00" />
<text y="24" class="bc-contact">evandro.dev.br</text>
<text y="60" class="bc-contact bc-contact-hl">[email protected]</text>
<text y="96" class="bc-contact">github.com/evandrodevbr</text>
</g>
<!-- Logo Section - Powerhouse of the design -->
<g transform="translate(420, 100) scale(1.6)">
<!-- Logo Shadow/Glow -->
<rect x="18" y="28" width="16" height="64" fill="rgba(255,255,255,0.03)" filter="blur(8px)" />
<!-- Main Logo Pieces -->
<rect x="20" y="30" width="12" height="60" fill="#FFFFFF" />
<rect x="32" y="30" width="28" height="12" fill="#FFFFFF" />
<rect x="32" y="54" width="18" height="12" fill="#FFFFFF" />
<rect x="32" y="78" width="28" height="12" fill="#FFFFFF" />
<polygon points="62,90 74,90 92,30 80,30" fill="#E8FF00" />
<circle cx="104" cy="84" r="6" fill="#FFFFFF" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.
Binary file not shown.
+3 -11
View File
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,9 +19,7 @@
}
],
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
},
"include": [
@@ -35,7 +29,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}