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", id: "KERNEL_INIT",
title: "Engenharia de fundamentos", title: "Engenharia de fundamentos",
content: 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", id: "RUNTIME_EXEC",
title: "Entrega sob restrições", title: "Entrega sob restrições",
content: 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", title: "Pragmatismo arquitetural",
description: 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: 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", title: "Ownership real",
description: 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", title: "Security by design",
description: 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 <Section
id="about" id="about"
eyebrow="WHOAMI.sys" eyebrow="WHOAMI.sys"
title="Engenharia orientada por clareza, não por ruído" 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." description="Transformo complexidade técnica em sistemas estáveis, operáveis e alinhados ao negócio."
aside={ aside={
<div className="mt-sp-4 ds-metric"> <div className="mt-sp-4 ds-metric">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-slash" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-slash" />
Garuva, SC · Remote worldwide Garuva, SC · Remote worldwide
</div> </div>
} }
> >
@@ -65,10 +65,10 @@ export default function About() {
<div className="ds-card"> <div className="ds-card">
<div className="ds-kicker">System Signature</div> <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"> <p className="ds-copy">
Evandro Fonseca Junior. Engenheiro Full-Stack com foco em Next.js, 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> </p>
<div className="ds-grid-2 mt-sp-6"> <div className="ds-grid-2 mt-sp-6">
{VALUES_DATA.map((value) => ( {VALUES_DATA.map((value) => (
+15 -11
View File
@@ -1,4 +1,4 @@
"use client"; "use client";
import type React from "react"; import type React from "react";
import { useState } from "react"; import { useState } from "react";
@@ -29,23 +29,23 @@ export default function Contact() {
<Section <Section
id="contact" id="contact"
eyebrow="CONTACT_PROTOCOL.init" 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." description="Descreva o contexto, o gargalo ou o produto. O contato abre direto no WhatsApp com briefing estruturado."
dark dark
> >
<div className="ds-grid-2"> <div className="ds-grid-2">
<div className="ds-card"> <div className="ds-card">
<div className="ds-kicker">Response SLA</div> <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"> <p className="ds-copy">
Ideal para discovery técnico, revisão de arquitetura, rescue de Ideal para discovery técnico, revisão de arquitetura, rescue de
projeto, otimização de performance ou construção de produto do projeto, otimização de performance ou construção de produto do
zero. zero.
</p> </p>
<div className="mt-sp-6 space-y-sp-4 text-[12px] leading-7 text-[rgba(255,255,255,.7)]"> <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>WhatsApp: +55 47 99274-2365</div>
<div>E-mail: evandrofonsecajunior@gmail.com</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>
</div> </div>
@@ -62,7 +62,8 @@ export default function Contact() {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, name: e.target.value }) 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>
<div> <div>
@@ -77,7 +78,8 @@ export default function Contact() {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, email: e.target.value }) 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>
<div> <div>
@@ -90,12 +92,13 @@ export default function Contact() {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, subject: e.target.value }) 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="full-stack">Desenvolvimento Full-Stack</option>
<option value="cloud">Cloud & Infrastructure</option> <option value="cloud">Cloud & Infrastructure</option>
<option value="iot">Internet of Things (IoT)</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> </select>
</div> </div>
<div> <div>
@@ -110,7 +113,8 @@ export default function Contact() {
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, message: e.target.value }) 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> </div>
<button type="submit" className="ds-link ds-link-accent border-0"> <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) { export function ContactForm({ formType, setFormType }: ContactFormProps) {
const { data } = usePortfolioData(); const { data } = usePortfolioData();
const [contactMethod, setContactMethod] = useState<"whatsapp" | "email">( const [contactMethod, setContactMethod] = useState<"whatsapp" | "email">(
"whatsapp" "whatsapp",
); );
const [userName, setUserName] = useState<string>(""); const [userName, setUserName] = useState<string>("");
const [userEmail, setUserEmail] = useState<string>(""); const [userEmail, setUserEmail] = useState<string>("");
@@ -57,8 +57,8 @@ export function ContactForm({ formType, setFormType }: ContactFormProps) {
hour >= 8 && hour < 12 hour >= 8 && hour < 12
? "manhã" ? "manhã"
: hour >= 12 && hour < 18 : hour >= 12 && hour < 18
? "tarde" ? "tarde"
: "noite"; : "noite";
return `Olá Evandro, espero que esta ${dayPart} esteja sendo produtiva. return `Olá Evandro, espero que esta ${dayPart} esteja sendo produtiva.
@@ -119,8 +119,8 @@ ${userName}`;
hasInternalTeam === "sim" hasInternalTeam === "sim"
? "Sim" ? "Sim"
: hasInternalTeam === "nao" : hasInternalTeam === "nao"
? "Não" ? "Não"
: "Parcialmente" : "Parcialmente"
}\n`; }\n`;
} }
@@ -186,7 +186,7 @@ ${userName}`;
}; };
const handleTechnicalComplexityChange = ( const handleTechnicalComplexityChange = (
e: ChangeEvent<HTMLSelectElement> e: ChangeEvent<HTMLSelectElement>,
) => { ) => {
setTechnicalComplexity(e.target.value); setTechnicalComplexity(e.target.value);
}; };
@@ -203,7 +203,7 @@ ${userName}`;
const message = encodeURIComponent( const message = encodeURIComponent(
formType === "advanced" formType === "advanced"
? generateAdvancedMessage() ? generateAdvancedMessage()
: customMessage || getGreetingMessageWithUserName() : customMessage || getGreetingMessageWithUserName(),
); );
const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos const phone = data.contact.whatsapp.replace(/\D/g, ""); // Remove caracteres não numéricos
return `https://wa.me/${phone}?text=${message}`; return `https://wa.me/${phone}?text=${message}`;
@@ -216,7 +216,7 @@ ${userName}`;
? generateAdvancedMessage() ? 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: ${ : `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() customMessage || getGreetingMessageWithUserName()
}` }`,
); );
return `mailto:${data.contact.email}?subject=${subject}&body=${body}`; return `mailto:${data.contact.email}?subject=${subject}&body=${body}`;
}; };
@@ -367,7 +367,7 @@ ${userName}`;
if (totalValue > 0) { if (totalValue > 0) {
return `Estimativa: ${Math.round( return `Estimativa: ${Math.round(
hours hours,
)}h × R$ ${hourlyRate}/h = R$ ${totalValue.toFixed(0)}`; )}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() { export default function GoogleAnalytics() {
return ( return (
<> <>
<Script <Script src="/sys/libs/vendor-utils.js" strategy="afterInteractive" />
src="/sys/libs/vendor-utils.js"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive"> <Script id="google-analytics" strategy="afterInteractive">
{` {`
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
+39 -63
View File
@@ -1,4 +1,4 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Logo } from "./Logo"; import { Logo } from "./Logo";
@@ -18,31 +18,23 @@ export function Header() {
return ( return (
<header <header
className="fixed left-0 right-0 top-0 z-50 border-b backdrop-blur-xl" 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"
style={{
borderColor: "rgba(255,255,255,.1)",
background: "rgba(10,10,10,.96)",
}}
> >
<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"> <a href="#home" className="flex items-center gap-3 no-underline">
<Logo variant="mark" size="sm" theme="dark" /> <Logo variant="mark" size="sm" theme="dark" className="shrink-0" />
<span <span className="font-display text-sm font-extrabold tracking-tight text-white">
className="font-display text-sm font-extrabold tracking-tight" evandro<span className="text-[var(--slash)]">.dev.br</span>
style={{ color: "#FFFFFF" }}
>
evandro<span style={{ color: "#E8FF00" }}>.dev.br</span>
</span> </span>
</a> </a>
<nav className="hidden items-center lg:flex"> <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) => ( {navItems.map((item) => (
<li key={item.href}> <li key={item.href}>
<a <a
href={item.href} 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" className="ds-link rounded-sm px-sp-3 py-sp-2 text-white/85 transition-colors hover:bg-white/10 hover:text-white"
style={{ color: "rgba(255,255,255,.86)" }}
> >
{item.label} {item.label}
</a> </a>
@@ -51,76 +43,60 @@ export function Header() {
</ul> </ul>
</nav> </nav>
<div className="flex items-center gap-4"> <div className="flex items-center gap-sp-4">
<span <span className="ds-metric hidden rounded-full border border-white/10 px-[10px] py-1 text-white/40 lg:inline-flex">
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)",
}}
>
v1.0 / 2026 v1.0 / 2026
</span> </span>
<a <a
href="/Profile.pdf" href="/Profile.pdf"
download="Evandro_Fonseca_Junior_CV.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" 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"
style={{ background: "#E8FF00", color: "#0A0A0A" }}
> >
Download CV Download CV
</a> </a>
<button <button
type="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)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Menu" aria-label="Menu"
aria-expanded={mobileMenuOpen}
> >
<span <span className={`h-0.5 w-5 bg-white transition-transform duration-300 ${mobileMenuOpen ? "translate-y-[6px] rotate-45" : ""}`} />
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-300 ${mobileMenuOpen ? "opacity-0" : ""}`} />
/> <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-200 ${mobileMenuOpen ? "opacity-0" : ""}`}
/>
<span
className={`h-0.5 w-5 bg-white transition-transform duration-200 ${mobileMenuOpen ? "-translate-y-2 -rotate-45" : ""}`}
/>
</button> </button>
</div> </div>
</div> </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"}`} 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"}`}
style={{
borderColor: "rgba(255,255,255,.1)",
background: "rgba(10,10,10,.98)",
}}
> >
<nav className="px-6 py-4"> <div className="ds-section-shell flex flex-col py-sp-6">
<ul className="flex list-none flex-col gap-2"> <nav className="mb-sp-6">
{navItems.map((item) => ( <ul className="flex list-none flex-col gap-sp-2">
<li key={item.href}> {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 <a
href={item.href} href="/Profile.pdf"
className="block rounded-sm px-3 py-3 text-[11px] uppercase tracking-[0.14em] no-underline transition-all duration-200 hover:bg-white/10" download="Evandro_Fonseca_Junior_CV.pdf"
style={{ color: "rgba(255,255,255,.9)" }} className="ds-link ds-link-accent mt-sp-2 inline-flex rounded-sm font-semibold"
onClick={() => setMobileMenuOpen(false)}
> >
{item.label} Download CV
</a> </a>
</li> </li>
))} </ul>
<li> </nav>
<a </div>
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>
</div> </div>
</header> </header>
); );
+37 -22
View File
@@ -1,4 +1,4 @@
"use client"; "use client";
import { Logo } from "./Logo"; import { Logo } from "./Logo";
@@ -48,15 +48,14 @@ export default function Hero() {
return ( return (
<section <section
id="home" id="home"
className="relative overflow-hidden px-6 py-24 lg:px-16" className="relative overflow-hidden bg-[#0A0A0A] px-sp-4 py-sp-12 text-white lg:px-sp-8"
style={{ background: "#0A0A0A", color: "#FFFFFF" }}
> >
<div className="pointer-events-none absolute inset-0 bg-grid-light opacity-70" /> <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="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>
<div className="mb-8 inline-flex items-center gap-2"> <div className="mb-sp-8 inline-flex items-center gap-sp-2">
<span <span
className="h-1.5 w-1.5 animate-pulse rounded-full" className="h-1.5 w-1.5 animate-pulse rounded-full"
style={{ background: "#E8FF00" }} style={{ background: "#E8FF00" }}
@@ -70,10 +69,10 @@ export default function Hero() {
</div> </div>
<h1 <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]" 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" }} style={{ color: "#FFFFFF", fontFamily: "'Monument Extended', sans-serif", fontWeight: 800 }}
> >
Evandro evandro
<br /> <br />
<span style={{ color: "#E8FF00" }}>.dev</span> <span style={{ color: "#E8FF00" }}>.dev</span>
<br /> <br />
@@ -81,7 +80,7 @@ export default function Hero() {
</h1> </h1>
<p <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)" }} style={{ color: "rgba(255,255,255,.72)" }}
> >
Engenheiro de Software especializado em aplicacoes de alta Engenheiro de Software especializado em aplicacoes de alta
@@ -89,16 +88,16 @@ export default function Hero() {
React, Tauri e arquitetura escalavel. React, Tauri e arquitetura escalavel.
</p> </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 <a
href={`https://wa.me/5547992742365?text=${encodeURIComponent(greeting)}`} 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" }} style={{ background: "#E8FF00", color: "#0A0A0A" }}
> >
Iniciar projeto Iniciar projeto
</a> </a>
<div className="flex items-center gap-2"> <div className="flex items-center gap-sp-2">
{SOCIAL_LINKS.map((link) => ( {SOCIAL_LINKS.map((link) => (
<a <a
key={link.label} key={link.label}
@@ -141,7 +140,7 @@ export default function Hero() {
</div> </div>
<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={{ style={{
borderColor: "rgba(255,255,255,.12)", borderColor: "rgba(255,255,255,.12)",
background: "rgba(255,255,255,.08)", background: "rgba(255,255,255,.08)",
@@ -161,7 +160,7 @@ export default function Hero() {
<span style={{ color: "#E8FF00" }}>{item.accent}</span> <span style={{ color: "#E8FF00" }}>{item.accent}</span>
</div> </div>
<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)" }} style={{ color: "rgba(255,255,255,.35)" }}
> >
{item.label} {item.label}
@@ -171,7 +170,7 @@ export default function Hero() {
</div> </div>
</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 <div
className="relative flex aspect-square w-full max-w-[440px] items-center justify-center border max-lg:max-w-[320px]" className="relative flex aspect-square w-full max-w-[440px] items-center justify-center border max-lg:max-w-[320px]"
style={{ style={{
@@ -183,35 +182,51 @@ export default function Hero() {
className="absolute left-6 top-5 text-[9px] tracking-[0.14em]" className="absolute left-6 top-5 text-[9px] tracking-[0.14em]"
style={{ color: "rgba(255,255,255,.32)" }} style={{ color: "rgba(255,255,255,.32)" }}
> >
D / SLASH MARK EV / LOGO MARK
</span> </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 <span
className="absolute bottom-5 right-6 text-[9px] tracking-[0.14em]" 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 CONCEITO ESCOLHIDO
</span> </span>
</div> </div>
<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)" }} style={{ color: "rgba(255,255,255,.32)" }}
> >
<span <span
className="h-px w-6" className="h-px w-6"
style={{ background: "rgba(255,255,255,.18)" }} style={{ background: "rgba(255,255,255,.18)" }}
/> />
D / Slash mark · conceito final EV / Full mark · identidade visual
</div> </div>
<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)" }} style={{ color: "rgba(255,255,255,.32)" }}
> >
<span <span
className="h-px w-6" className="h-px w-6"
style={{ background: "rgba(255,255,255,.18)" }} style={{ background: "rgba(255,255,255,.18)" }}
/> />
Escala: 280x200 · viewbox Escala: 280x280 · svg viewbox
</div> </div>
</div> </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 { useMemo } from "react";
import { usePortfolioData } from "@/lib/usePortfolioData"; import { usePortfolioData } from "@/lib/usePortfolioData";
@@ -21,8 +21,8 @@ export default function Projects() {
<Section <Section
id="projects" id="projects"
eyebrow="PRODUCAO_INTELECTUAL" eyebrow="PRODUCAO_INTELECTUAL"
title="Projetos com recorte técnico claro e impacto observável" 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." description="Seleção de produtos e ferramentas onde performance, confiabilidade e clareza de arquitetura foram requisitos reais."
dark dark
> >
<div className="ds-grid-2"> <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 <Section
id="skills" id="skills"
eyebrow="MODULE_REGISTRY.lib" eyebrow="MODULE_REGISTRY.lib"
title="Stack modular para produto, operação e escala" title="Stack modular para produto, operação e escala"
description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção." description="Ferramentas escolhidas por previsibilidade operacional, velocidade de entrega e facilidade de manutenção."
dark dark
> >
<div className="ds-grid-2"> <div className="ds-grid-2">
+225 -81
View File
@@ -1,108 +1,252 @@
"use client"; "use client";
import { useState } from "react";
import { Section } from "./Section"; 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", id: "discovery",
value: "R$ 35 - 70/hora", badge: "Entrada / Baixo Risco",
badge: "Agilidade", level: "01",
idealFor: title: "Auditoria & Diagnóstico Técnico",
"Resolução de gargalos técnicos específicos e consultoria de arquitetura imediata.", tagline: "Fotografia clara do problema antes de qualquer comprometimento.",
description: description:
"Engenharia sob demanda com foco em troubleshooting, performance e decisões rápidas com rastreabilidade.", "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.",
benefits: [ deliverables: [
"Diagnóstico sênior imediato", "Relatório de gargalos e dívida técnica",
"Sem compromisso de longo prazo", "Mapeamento de riscos e dependências",
"Documentação de alterações", "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", id: "cycles",
value: "Eficiência financeira", badge: "Mais Escolhido",
badge: "Mais escolhido", level: "02",
idealFor: title: "Ciclos de Engenharia",
"Times que precisam de entrega contínua com previsibilidade de orçamento e prioridade técnica.", tagline: "Capacidade produtiva garantida. Entrega a cada 2 semanas.",
description: description:
"Pacotes de horas pré-pagas com redução de custo unitário e espaço reservado na agenda para evolução constante.", "Desenvolvimento contínuo com a previsibilidade de orçamento do escopo fechado e a agilidade de sprints. Milestones claros, sem amarras de contrato longo.",
benefits: [ deliverables: [
"Taxa horária reduzida", "Features em produção a cada ciclo",
"Garantia de disponibilidade", "Refatoração iterativa com rastreabilidade",
"Prioridade em bugs críticos", "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", id: "fractional",
value: "A partir de R$ 2.800/mês", badge: "Premium / High-Ticket",
badge: "Estratégico", level: "03",
idealFor: title: "Fractional Tech Lead",
"Empresas que precisam de liderança técnica contínua, coordenação de backlog e direção arquitetural.", tagline: "Liderança técnica senior. Sem o custo de um CTO full-time.",
description: description:
"Parceria recorrente para sustentação, evolução de sistema, revisão de dívida técnica e tomada de decisão.", "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.",
benefits: [ deliverables: [
"SLA prioritário", "SLA prioritário Zero-Risk",
"Gestão de débito técnico", "Code review e liderança do time interno",
"Consultoria estratégica", "Decisões de infra: Cloud, VPS, CI/CD",
], "Consultoria estratégica recorrente",
},
{
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",
], ],
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() { export function WorkModalities() {
return ( return (
<Section <Section
id="modalities" id="modalities"
eyebrow="CONTRACT_PROTOCOLS.sys" eyebrow="CONTRACT_PROTOCOLS.sys"
title="Formatos de contratação para contextos técnicos distintos" title="Engenharia sob demanda. Previsibilidade de entrega sem overhead."
description="Modelos pensados para reduzir overhead, preservar velocidade de decisão e manter previsibilidade." 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"> <div
{[ style={{
["Agências tradicionais", "Alto overhead"], display: "grid",
["Consultorias big tech", "Custo inflacionado"], gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
["Freelancers generalistas", "Risco técnico"], gap: "var(--sp-6)",
["Engenheiro especialista", "High ROI"], }}
].map(([label, value]) => ( >
<div key={label} className="ds-panel"> {TIERS.map((tier) => (
<div className="ds-kicker">{label}</div> <TierCard key={tier.id} tier={tier} />
<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> </div>
</Section> </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"); @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 base;
@tailwind components; @tailwind components;
@tailwind utilities; @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 { :root {
--ink: #0a0a0a; --ink: #0a0a0a;
--ink-80: rgba(10, 10, 10, 0.8); --ink-80: rgba(10, 10, 10, 0.8);
@@ -35,6 +66,7 @@
--r-lg: 16px; --r-lg: 16px;
--r-full: 999px; --r-full: 999px;
--font-display: "Syne", sans-serif; --font-display: "Syne", sans-serif;
--font-hero: 'Monument Extended', sans-serif;
--font-mono: "Geist Mono", monospace; --font-mono: "Geist Mono", monospace;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1);
@@ -100,7 +132,7 @@ textarea {
border-bottom: 1px solid var(--border); 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); background: var(--paper-dark);
} }
+17 -14
View File
@@ -4,7 +4,8 @@ import GoogleAnalytics from "./components/GoogleAnalytics";
import "./globals.css"; import "./globals.css";
export const metadata: Metadata = { 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: 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.", "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: [ keywords: [
@@ -42,7 +43,8 @@ export const metadata: Metadata = {
canonical: "/", canonical: "/",
}, },
openGraph: { openGraph: {
title: "Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)", title:
"Evandro Fonseca Junior | Engenheiro de Software (Next.js, Tauri & Rust)",
description: 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.", "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", url: "https://evandro.dev.br",
@@ -60,7 +62,8 @@ export const metadata: Metadata = {
}, },
twitter: { twitter: {
card: "summary_large_image", 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: 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.", "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", creator: "@evandrodevbr",
@@ -94,17 +97,17 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "ProfilePage", "@type": "ProfilePage",
"mainEntity": { mainEntity: {
"@type": "Person", "@type": "Person",
"name": "Evandro Fonseca Junior", name: "Evandro Fonseca Junior",
"alternateName": "Evandro.dev", alternateName: "Evandro.dev",
"jobTitle": "Engenheiro de Software", jobTitle: "Engenheiro de Software",
"url": "https://evandro.dev.br", url: "https://evandro.dev.br",
"sameAs": [ sameAs: [
"https://github.com/evandrodevbr", "https://github.com/evandrodevbr",
"https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/", "https://www.linkedin.com/in/evandro-fonseca-junior-0969aa1b0/",
], ],
"knowsAbout": [ knowsAbout: [
"Software Engineering", "Software Engineering",
"Next.js", "Next.js",
"React", "React",
@@ -119,11 +122,11 @@ export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
"PostgreSQL", "PostgreSQL",
"MySQL", "MySQL",
], ],
"worksFor": { worksFor: {
"@type": "Organization", "@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.", "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> </body>
</html> </html>
); );
} }
+20 -19
View File
@@ -1,29 +1,30 @@
import { MetadataRoute } from 'next' import { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest { export default function manifest(): MetadataRoute.Manifest {
return { return {
name: 'Evandro Fonseca Junior - Portfólio', name: "Evandro Fonseca Junior - Portfólio",
short_name: 'Evandro.dev', short_name: "Evandro.dev",
description: 'Portfólio profissional de Evandro Fonseca Junior, Desenvolvedor Full-Stack', description:
start_url: '/', "Portfólio profissional de Evandro Fonseca Junior, Desenvolvedor Full-Stack",
display: 'standalone', start_url: "/",
background_color: '#000000', display: "standalone",
theme_color: '#000000', background_color: "#000000",
orientation: 'portrait-primary', theme_color: "#000000",
categories: ['portfolio', 'developer', 'technology'], orientation: "portrait-primary",
lang: 'pt-BR', categories: ["portfolio", "developer", "technology"],
dir: 'ltr', lang: "pt-BR",
dir: "ltr",
icons: [ icons: [
{ {
src: '/favicon.ico', src: "/favicon.ico",
sizes: 'any', sizes: "any",
type: 'image/x-icon', type: "image/x-icon",
}, },
{ {
src: '/icon.svg', src: "/icon.svg",
sizes: 'any', sizes: "any",
type: 'image/svg+xml', 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="max-w-2xl mx-auto text-center">
<div className="mb-8"> <div className="mb-8">
<Code2 className="w-24 h-24 text-blue-500 mx-auto mb-4 opacity-50" /> <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"> <h2 className="text-2xl md:text-3xl font-bold text-slate-400 mb-4">
Página não encontrada Página não encontrada
</h2> </h2>
@@ -15,7 +17,7 @@ export default function NotFound() {
A página que você está procurando não existe ou foi movida. A página que você está procurando não existe ou foi movida.
</p> </p>
</div> </div>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link <Link
href="/" 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 runtime = "edge";
export const alt = 'Evandro Fonseca Junior - Engenheiro de Software' export const alt = "Evandro Fonseca Junior - Engenheiro de Software";
export const size = { export const size = {
width: 1200, width: 1200,
height: 630, height: 630,
} };
export const contentType = 'image/png' export const contentType = "image/png";
export default async function Image() { export default async function Image() {
return new ImageResponse( 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 <div
style={{ style={{
background: '#030712', fontSize: 64,
width: '100%', fontWeight: "bold",
height: '100%', marginBottom: 20,
display: 'flex', textAlign: "center",
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontFamily: 'system-ui',
}} }}
> >
<div Evandro Fonseca Junior
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>
</div> </div>
), <div
{ ...size } 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() { export default function Home() {
return ( return (
<div> <div className="flex min-h-screen flex-col overflow-x-hidden">
<Header /> <Header />
<main className="ds-main"> <main className="ds-main">
<Hero /> <Hero />
+6 -6
View File
@@ -1,14 +1,14 @@
import { MetadataRoute } from 'next' import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
return { return {
rules: [ rules: [
{ {
userAgent: '*', userAgent: "*",
allow: '/', allow: "/",
disallow: ['/api/', '/_next/', '/admin/'], 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 { export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://evandro.dev.br' const baseUrl = "https://evandro.dev.br";
const currentDate = new Date().toISOString() const currentDate = new Date().toISOString();
return [ return [
{ {
url: baseUrl, url: baseUrl,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 1.0, priority: 1.0,
}, },
{ {
url: `${baseUrl}/#about`, url: `${baseUrl}/#about`,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
{ {
url: `${baseUrl}/#skills`, url: `${baseUrl}/#skills`,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
{ {
url: `${baseUrl}/#experience`, url: `${baseUrl}/#experience`,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
{ {
url: `${baseUrl}/#projects`, url: `${baseUrl}/#projects`,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
{ {
url: `${baseUrl}/#contact`, url: `${baseUrl}/#contact`,
lastModified: currentDate, lastModified: currentDate,
changeFrequency: 'monthly', changeFrequency: "monthly",
priority: 0.8, priority: 0.8,
}, },
] ];
} }
+41 -27
View File
@@ -1,7 +1,7 @@
'use client'; "use client";
import React, { useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useRef, useEffect, useCallback, useMemo } from "react";
import { gsap } from 'gsap'; import { gsap } from "gsap";
import { InertiaPlugin } from 'gsap/InertiaPlugin'; import { InertiaPlugin } from "gsap/InertiaPlugin";
gsap.registerPlugin(InertiaPlugin); gsap.registerPlugin(InertiaPlugin);
@@ -46,15 +46,15 @@ function hexToRgb(hex: string) {
return { return {
r: parseInt(m[1], 16), r: parseInt(m[1], 16),
g: parseInt(m[2], 16), g: parseInt(m[2], 16),
b: parseInt(m[3], 16) b: parseInt(m[3], 16),
}; };
} }
const DotGrid: React.FC<DotGridProps> = ({ const DotGrid: React.FC<DotGridProps> = ({
dotSize = 16, dotSize = 16,
gap = 32, gap = 32,
baseColor = '#5227FF', baseColor = "#5227FF",
activeColor = '#5227FF', activeColor = "#5227FF",
proximity = 150, proximity = 150,
speedTrigger = 100, speedTrigger = 100,
shockRadius = 250, shockRadius = 250,
@@ -62,8 +62,8 @@ const DotGrid: React.FC<DotGridProps> = ({
maxSpeed = 5000, maxSpeed = 5000,
resistance = 750, resistance = 750,
returnDuration = 1.5, returnDuration = 1.5,
className = '', className = "",
style style,
}) => { }) => {
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -76,14 +76,14 @@ const DotGrid: React.FC<DotGridProps> = ({
speed: 0, speed: 0,
lastTime: 0, lastTime: 0,
lastX: 0, lastX: 0,
lastY: 0 lastY: 0,
}); });
const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]); const baseRgb = useMemo(() => hexToRgb(baseColor), [baseColor]);
const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]); const activeRgb = useMemo(() => hexToRgb(activeColor), [activeColor]);
const circlePath = useMemo(() => { const circlePath = useMemo(() => {
if (typeof window === 'undefined' || !window.Path2D) return null; if (typeof window === "undefined" || !window.Path2D) return null;
const p = new Path2D(); const p = new Path2D();
p.arc(0, 0, dotSize / 2, 0, Math.PI * 2); p.arc(0, 0, dotSize / 2, 0, Math.PI * 2);
@@ -102,7 +102,7 @@ const DotGrid: React.FC<DotGridProps> = ({
canvas.height = height * dpr; canvas.height = height * dpr;
canvas.style.width = `${width}px`; canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`; canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (ctx) ctx.scale(dpr, dpr); if (ctx) ctx.scale(dpr, dpr);
const cols = Math.floor((width + gap) / (dotSize + gap)); const cols = Math.floor((width + gap) / (dotSize + gap));
@@ -138,7 +138,7 @@ const DotGrid: React.FC<DotGridProps> = ({
const draw = () => { const draw = () => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -178,15 +178,15 @@ const DotGrid: React.FC<DotGridProps> = ({
useEffect(() => { useEffect(() => {
buildGrid(); buildGrid();
let ro: ResizeObserver | null = null; let ro: ResizeObserver | null = null;
if ('ResizeObserver' in window) { if ("ResizeObserver" in window) {
ro = new ResizeObserver(buildGrid); ro = new ResizeObserver(buildGrid);
wrapperRef.current && ro.observe(wrapperRef.current); wrapperRef.current && ro.observe(wrapperRef.current);
} else { } else {
(window as Window).addEventListener('resize', buildGrid); (window as Window).addEventListener("resize", buildGrid);
} }
return () => { return () => {
if (ro) ro.disconnect(); if (ro) ro.disconnect();
else window.removeEventListener('resize', buildGrid); else window.removeEventListener("resize", buildGrid);
}; };
}, [buildGrid]); }, [buildGrid]);
@@ -231,10 +231,10 @@ const DotGrid: React.FC<DotGridProps> = ({
xOffset: 0, xOffset: 0,
yOffset: 0, yOffset: 0,
duration: returnDuration, duration: returnDuration,
ease: 'elastic.out(1,0.75)' ease: "elastic.out(1,0.75)",
}); });
dot._inertiaApplied = false; dot._inertiaApplied = false;
} },
}); });
} }
} }
@@ -259,29 +259,43 @@ const DotGrid: React.FC<DotGridProps> = ({
xOffset: 0, xOffset: 0,
yOffset: 0, yOffset: 0,
duration: returnDuration, duration: returnDuration,
ease: 'elastic.out(1,0.75)' ease: "elastic.out(1,0.75)",
}); });
dot._inertiaApplied = false; dot._inertiaApplied = false;
} },
}); });
} }
} }
}; };
const throttledMove = throttle(onMove, 50); const throttledMove = throttle(onMove, 50);
window.addEventListener('mousemove', throttledMove, { passive: true }); window.addEventListener("mousemove", throttledMove, { passive: true });
window.addEventListener('click', onClick); window.addEventListener("click", onClick);
return () => { return () => {
window.removeEventListener('mousemove', throttledMove); window.removeEventListener("mousemove", throttledMove);
window.removeEventListener('click', onClick); window.removeEventListener("click", onClick);
}; };
}, [maxSpeed, speedTrigger, proximity, resistance, returnDuration, shockRadius, shockStrength]); }, [
maxSpeed,
speedTrigger,
proximity,
resistance,
returnDuration,
shockRadius,
shockStrength,
]);
return ( 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"> <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> </div>
</section> </section>
); );
+3 -3
View File
@@ -284,7 +284,7 @@ export default function FaultyTerminal({
const ditherValue = useMemo( const ditherValue = useMemo(
() => (typeof dither === "boolean" ? (dither ? 1 : 0) : dither), () => (typeof dither === "boolean" ? (dither ? 1 : 0) : dither),
[dither] [dither],
); );
const handleMouseMove = useCallback((e: MouseEvent) => { const handleMouseMove = useCallback((e: MouseEvent) => {
@@ -316,7 +316,7 @@ export default function FaultyTerminal({
value: new Color( value: new Color(
gl.canvas.width, gl.canvas.width,
gl.canvas.height, gl.canvas.height,
gl.canvas.width / gl.canvas.height gl.canvas.width / gl.canvas.height,
), ),
}, },
uScale: { value: scale }, uScale: { value: scale },
@@ -354,7 +354,7 @@ export default function FaultyTerminal({
program.uniforms.iResolution.value = new Color( program.uniforms.iResolution.value = new Color(
gl.canvas.width, gl.canvas.width,
gl.canvas.height, 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 = export type LogoItem =
| { | {
@@ -21,7 +27,7 @@ export type LogoItem =
export interface LogoLoopProps { export interface LogoLoopProps {
logos: LogoItem[]; logos: LogoItem[];
speed?: number; speed?: number;
direction?: 'left' | 'right'; direction?: "left" | "right";
width?: number | string; width?: number | string;
logoHeight?: number; logoHeight?: number;
gap?: number; gap?: number;
@@ -37,28 +43,29 @@ export interface LogoLoopProps {
const ANIMATION_CONFIG = { const ANIMATION_CONFIG = {
SMOOTH_TAU: 0.25, SMOOTH_TAU: 0.25,
MIN_COPIES: 2, MIN_COPIES: 2,
COPY_HEADROOM: 2 COPY_HEADROOM: 2,
} as const; } as const;
const toCssLength = (value?: number | string): string | undefined => 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 = ( const useResizeObserver = (
callback: () => void, callback: () => void,
elements: Array<React.RefObject<Element | null>>, elements: Array<React.RefObject<Element | null>>,
dependencies: React.DependencyList dependencies: React.DependencyList,
) => { ) => {
useEffect(() => { useEffect(() => {
if (!window.ResizeObserver) { if (!window.ResizeObserver) {
const handleResize = () => callback(); const handleResize = () => callback();
window.addEventListener('resize', handleResize); window.addEventListener("resize", handleResize);
callback(); 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; if (!ref.current) return null;
const observer = new ResizeObserver(callback); const observer = new ResizeObserver(callback);
observer.observe(ref.current); observer.observe(ref.current);
@@ -68,7 +75,7 @@ const useResizeObserver = (
callback(); callback();
return () => { return () => {
observers.forEach(observer => observer?.disconnect()); observers.forEach((observer) => observer?.disconnect());
}; };
}, dependencies); }, dependencies);
}; };
@@ -76,10 +83,10 @@ const useResizeObserver = (
const useImageLoader = ( const useImageLoader = (
seqRef: React.RefObject<HTMLUListElement | null>, seqRef: React.RefObject<HTMLUListElement | null>,
onLoad: () => void, onLoad: () => void,
dependencies: React.DependencyList dependencies: React.DependencyList,
) => { ) => {
useEffect(() => { useEffect(() => {
const images = seqRef.current?.querySelectorAll('img') ?? []; const images = seqRef.current?.querySelectorAll("img") ?? [];
if (images.length === 0) { if (images.length === 0) {
onLoad(); onLoad();
@@ -94,20 +101,20 @@ const useImageLoader = (
} }
}; };
images.forEach(img => { images.forEach((img) => {
const htmlImg = img as HTMLImageElement; const htmlImg = img as HTMLImageElement;
if (htmlImg.complete) { if (htmlImg.complete) {
handleImageLoad(); handleImageLoad();
} else { } else {
htmlImg.addEventListener('load', handleImageLoad, { once: true }); htmlImg.addEventListener("load", handleImageLoad, { once: true });
htmlImg.addEventListener('error', handleImageLoad, { once: true }); htmlImg.addEventListener("error", handleImageLoad, { once: true });
} }
}); });
return () => { return () => {
images.forEach(img => { images.forEach((img) => {
img.removeEventListener('load', handleImageLoad); img.removeEventListener("load", handleImageLoad);
img.removeEventListener('error', handleImageLoad); img.removeEventListener("error", handleImageLoad);
}); });
}; };
}, dependencies); }, dependencies);
@@ -118,7 +125,7 @@ const useAnimationLoop = (
targetVelocity: number, targetVelocity: number,
seqWidth: number, seqWidth: number,
isHovered: boolean, isHovered: boolean,
pauseOnHover: boolean pauseOnHover: boolean,
) => { ) => {
const rafRef = useRef<number | null>(null); const rafRef = useRef<number | null>(null);
const lastTimestampRef = useRef<number | null>(null); const lastTimestampRef = useRef<number | null>(null);
@@ -130,17 +137,18 @@ const useAnimationLoop = (
if (!track) return; if (!track) return;
const prefersReduced = const prefersReduced =
typeof window !== 'undefined' && typeof window !== "undefined" &&
window.matchMedia && window.matchMedia &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches; window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (seqWidth > 0) { 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)`; track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
} }
if (prefersReduced) { if (prefersReduced) {
track.style.transform = 'translate3d(0, 0, 0)'; track.style.transform = "translate3d(0, 0, 0)";
return () => { return () => {
lastTimestampRef.current = null; lastTimestampRef.current = null;
}; };
@@ -151,12 +159,14 @@ const useAnimationLoop = (
lastTimestampRef.current = timestamp; lastTimestampRef.current = timestamp;
} }
const deltaTime = Math.max(0, timestamp - lastTimestampRef.current) / 1000; const deltaTime =
Math.max(0, timestamp - lastTimestampRef.current) / 1000;
lastTimestampRef.current = timestamp; lastTimestampRef.current = timestamp;
const target = pauseOnHover && isHovered ? 0 : targetVelocity; 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; velocityRef.current += (target - velocityRef.current) * easingFactor;
if (seqWidth > 0) { if (seqWidth > 0) {
@@ -187,72 +197,87 @@ export const LogoLoop = React.memo<LogoLoopProps>(
({ ({
logos, logos,
speed = 120, speed = 120,
direction = 'left', direction = "left",
width = '100%', width = "100%",
logoHeight = 28, logoHeight = 28,
gap = 32, gap = 32,
pauseOnHover = true, pauseOnHover = true,
fadeOut = false, fadeOut = false,
fadeOutColor, fadeOutColor,
scaleOnHover = false, scaleOnHover = false,
ariaLabel = 'Partner logos', ariaLabel = "Partner logos",
className, className,
style style,
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const trackRef = useRef<HTMLDivElement>(null); const trackRef = useRef<HTMLDivElement>(null);
const seqRef = useRef<HTMLUListElement>(null); const seqRef = useRef<HTMLUListElement>(null);
const [seqWidth, setSeqWidth] = useState<number>(0); 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 [isHovered, setIsHovered] = useState<boolean>(false);
const targetVelocity = useMemo(() => { const targetVelocity = useMemo(() => {
const magnitude = Math.abs(speed); const magnitude = Math.abs(speed);
const directionMultiplier = direction === 'left' ? 1 : -1; const directionMultiplier = direction === "left" ? 1 : -1;
const speedMultiplier = speed < 0 ? -1 : 1; const speedMultiplier = speed < 0 ? -1 : 1;
return magnitude * directionMultiplier * speedMultiplier; return magnitude * directionMultiplier * speedMultiplier;
}, [speed, direction]); }, [speed, direction]);
const updateDimensions = useCallback(() => { const updateDimensions = useCallback(() => {
const containerWidth = containerRef.current?.clientWidth ?? 0; const containerWidth = containerRef.current?.clientWidth ?? 0;
const sequenceWidth = seqRef.current?.getBoundingClientRect?.()?.width ?? 0; const sequenceWidth =
seqRef.current?.getBoundingClientRect?.()?.width ?? 0;
if (sequenceWidth > 0) { if (sequenceWidth > 0) {
setSeqWidth(Math.ceil(sequenceWidth)); 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)); 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]); useImageLoader(seqRef, updateDimensions, [logos, gap, logoHeight]);
useAnimationLoop(trackRef, targetVelocity, seqWidth, isHovered, pauseOnHover); useAnimationLoop(
trackRef,
targetVelocity,
seqWidth,
isHovered,
pauseOnHover,
);
const cssVariables = useMemo( const cssVariables = useMemo(
() => () =>
({ ({
'--logoloop-gap': `${gap}px`, "--logoloop-gap": `${gap}px`,
'--logoloop-logoHeight': `${logoHeight}px`, "--logoloop-logoHeight": `${logoHeight}px`,
...(fadeOutColor && { '--logoloop-fadeColor': fadeOutColor }) ...(fadeOutColor && { "--logoloop-fadeColor": fadeOutColor }),
}) as React.CSSProperties, }) as React.CSSProperties,
[gap, logoHeight, fadeOutColor] [gap, logoHeight, fadeOutColor],
); );
const rootClasses = useMemo( const rootClasses = useMemo(
() => () =>
cx( cx(
'relative overflow-x-hidden group', "relative overflow-x-hidden group",
'[--logoloop-gap:32px]', "[--logoloop-gap:32px]",
'[--logoloop-logoHeight:28px]', "[--logoloop-logoHeight:28px]",
'[--logoloop-fadeColorAuto:#ffffff]', "[--logoloop-fadeColorAuto:#ffffff]",
'dark:[--logoloop-fadeColorAuto:#0b0b0b]', "dark:[--logoloop-fadeColorAuto:#0b0b0b]",
scaleOnHover && 'py-[calc(var(--logoloop-logoHeight)*0.1)]', scaleOnHover && "py-[calc(var(--logoloop-logoHeight)*0.1)]",
className className,
), ),
[scaleOnHover, className] [scaleOnHover, className],
); );
const handleMouseEnter = useCallback(() => { const handleMouseEnter = useCallback(() => {
@@ -265,15 +290,15 @@ export const LogoLoop = React.memo<LogoLoopProps>(
const renderLogoItem = useCallback( const renderLogoItem = useCallback(
(item: LogoItem, key: React.Key) => { (item: LogoItem, key: React.Key) => {
const isNodeItem = 'node' in item; const isNodeItem = "node" in item;
const content = isNodeItem ? ( const content = isNodeItem ? (
<span <span
className={cx( className={cx(
'inline-flex items-center', "inline-flex items-center",
'motion-reduce:transition-none', "motion-reduce:transition-none",
scaleOnHover && 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} aria-hidden={!!(item as any).href && !(item as any).ariaLabel}
> >
@@ -282,19 +307,19 @@ export const LogoLoop = React.memo<LogoLoopProps>(
) : ( ) : (
<img <img
className={cx( className={cx(
'h-[var(--logoloop-logoHeight)] w-auto block object-contain', "h-[var(--logoloop-logoHeight)] w-auto block object-contain",
'[-webkit-user-drag:none] pointer-events-none', "[-webkit-user-drag:none] pointer-events-none",
'[image-rendering:-webkit-optimize-contrast]', "[image-rendering:-webkit-optimize-contrast]",
'motion-reduce:transition-none', "motion-reduce:transition-none",
scaleOnHover && 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} src={(item as any).src}
srcSet={(item as any).srcSet} srcSet={(item as any).srcSet}
sizes={(item as any).sizes} sizes={(item as any).sizes}
width={(item as any).width} width={(item as any).width}
height={(item as any).height} height={(item as any).height}
alt={(item as any).alt ?? ''} alt={(item as any).alt ?? ""}
title={(item as any).title} title={(item as any).title}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
@@ -309,13 +334,13 @@ export const LogoLoop = React.memo<LogoLoopProps>(
const inner = (item as any).href ? ( const inner = (item as any).href ? (
<a <a
className={cx( className={cx(
'inline-flex items-center no-underline rounded', "inline-flex items-center no-underline rounded",
'transition-opacity duration-200 ease-linear', "transition-opacity duration-200 ease-linear",
'hover:opacity-80', "hover:opacity-80",
'focus-visible:outline focus-visible:outline-current focus-visible:outline-offset-2' "focus-visible:outline focus-visible:outline-current focus-visible:outline-offset-2",
)} )}
href={(item as any).href} href={(item as any).href}
aria-label={itemAriaLabel || 'logo link'} aria-label={itemAriaLabel || "logo link"}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
@@ -328,8 +353,8 @@ export const LogoLoop = React.memo<LogoLoopProps>(
return ( return (
<li <li
className={cx( className={cx(
'flex-none mr-[var(--logoloop-gap)] text-[length:var(--logoloop-logoHeight)] leading-[1]', "flex-none mr-[var(--logoloop-gap)] text-[length:var(--logoloop-logoHeight)] leading-[1]",
scaleOnHover && 'overflow-visible group/item' scaleOnHover && "overflow-visible group/item",
)} )}
key={key} key={key}
role="listitem" role="listitem"
@@ -338,7 +363,7 @@ export const LogoLoop = React.memo<LogoLoopProps>(
</li> </li>
); );
}, },
[scaleOnHover] [scaleOnHover],
); );
const logoLists = useMemo( const logoLists = useMemo(
@@ -351,19 +376,21 @@ export const LogoLoop = React.memo<LogoLoopProps>(
aria-hidden={copyIndex > 0} aria-hidden={copyIndex > 0}
ref={copyIndex === 0 ? seqRef : undefined} ref={copyIndex === 0 ? seqRef : undefined}
> >
{logos.map((item, itemIndex) => renderLogoItem(item, `${copyIndex}-${itemIndex}`))} {logos.map((item, itemIndex) =>
renderLogoItem(item, `${copyIndex}-${itemIndex}`),
)}
</ul> </ul>
)), )),
[copyCount, logos, renderLogoItem] [copyCount, logos, renderLogoItem],
); );
const containerStyle = useMemo( const containerStyle = useMemo(
(): React.CSSProperties => ({ (): React.CSSProperties => ({
width: toCssLength(width) ?? '100%', width: toCssLength(width) ?? "100%",
...cssVariables, ...cssVariables,
...style ...style,
}), }),
[width, cssVariables, style] [width, cssVariables, style],
); );
return ( return (
@@ -381,33 +408,36 @@ export const LogoLoop = React.memo<LogoLoopProps>(
<div <div
aria-hidden aria-hidden
className={cx( className={cx(
'pointer-events-none absolute inset-y-0 left-0 z-[1]', "pointer-events-none absolute inset-y-0 left-0 z-[1]",
'w-[clamp(24px,8%,120px)]', "w-[clamp(24px,8%,120px)]",
'bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]' "bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]",
)} )}
/> />
<div <div
aria-hidden aria-hidden
className={cx( className={cx(
'pointer-events-none absolute inset-y-0 right-0 z-[1]', "pointer-events-none absolute inset-y-0 right-0 z-[1]",
'w-[clamp(24px,8%,120px)]', "w-[clamp(24px,8%,120px)]",
'bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]' "bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]",
)} )}
/> />
</> </>
)} )}
<div <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} ref={trackRef}
> >
{logoLists} {logoLists}
</div> </div>
</div> </div>
); );
} },
); );
LogoLoop.displayName = 'LogoLoop'; LogoLoop.displayName = "LogoLoop";
export default LogoLoop; export default LogoLoop;
+1 -1
View File
@@ -80,7 +80,7 @@ const Squares: React.FC<SquaresProps> = ({
0, 0,
canvas.width / 2, canvas.width / 2,
canvas.height / 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(0, "rgba(0, 0, 0, 0)");
gradient.addColorStop(1, "#060010"); 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 {
import { gsap } from 'gsap'; ElementType,
useEffect,
useRef,
useState,
createElement,
useMemo,
useCallback,
} from "react";
import { gsap } from "gsap";
interface TextTypeProps { interface TextTypeProps {
className?: string; className?: string;
@@ -26,17 +34,17 @@ interface TextTypeProps {
const TextType = ({ const TextType = ({
text, text,
as: Component = 'div', as: Component = "div",
typingSpeed = 50, typingSpeed = 50,
initialDelay = 0, initialDelay = 0,
pauseDuration = 2000, pauseDuration = 2000,
deletingSpeed = 30, deletingSpeed = 30,
loop = true, loop = true,
className = '', className = "",
showCursor = true, showCursor = true,
hideCursorWhileTyping = false, hideCursorWhileTyping = false,
cursorCharacter = '|', cursorCharacter = "|",
cursorClassName = '', cursorClassName = "",
cursorBlinkDuration = 0.5, cursorBlinkDuration = 0.5,
textColors = [], textColors = [],
variableSpeed, variableSpeed,
@@ -45,7 +53,7 @@ const TextType = ({
reverseMode = false, reverseMode = false,
...props ...props
}: TextTypeProps & React.HTMLAttributes<HTMLElement>) => { }: TextTypeProps & React.HTMLAttributes<HTMLElement>) => {
const [displayedText, setDisplayedText] = useState(''); const [displayedText, setDisplayedText] = useState("");
const [currentCharIndex, setCurrentCharIndex] = useState(0); const [currentCharIndex, setCurrentCharIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [currentTextIndex, setCurrentTextIndex] = useState(0); const [currentTextIndex, setCurrentTextIndex] = useState(0);
@@ -53,7 +61,10 @@ const TextType = ({
const cursorRef = useRef<HTMLSpanElement>(null); const cursorRef = useRef<HTMLSpanElement>(null);
const containerRef = useRef<HTMLElement>(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(() => { const getRandomSpeed = useCallback(() => {
if (!variableSpeed) return typingSpeed; if (!variableSpeed) return typingSpeed;
@@ -62,7 +73,7 @@ const TextType = ({
}, [variableSpeed, typingSpeed]); }, [variableSpeed, typingSpeed]);
const getCurrentTextColor = () => { const getCurrentTextColor = () => {
if (textColors.length === 0) return '#ffffff'; if (textColors.length === 0) return "#ffffff";
return textColors[currentTextIndex % textColors.length]; return textColors[currentTextIndex % textColors.length];
}; };
@@ -70,14 +81,14 @@ const TextType = ({
if (!startOnVisible || !containerRef.current) return; if (!startOnVisible || !containerRef.current) return;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
entries => { (entries) => {
entries.forEach(entry => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setIsVisible(true); setIsVisible(true);
} }
}); });
}, },
{ threshold: 0.1 } { threshold: 0.1 },
); );
observer.observe(containerRef.current); observer.observe(containerRef.current);
@@ -92,7 +103,7 @@ const TextType = ({
duration: cursorBlinkDuration, duration: cursorBlinkDuration,
repeat: -1, repeat: -1,
yoyo: true, yoyo: true,
ease: 'power2.inOut' ease: "power2.inOut",
}); });
} }
}, [showCursor, cursorBlinkDuration]); }, [showCursor, cursorBlinkDuration]);
@@ -103,11 +114,13 @@ const TextType = ({
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
const currentText = textArray[currentTextIndex]; const currentText = textArray[currentTextIndex];
const processedText = reverseMode ? currentText.split('').reverse().join('') : currentText; const processedText = reverseMode
? currentText.split("").reverse().join("")
: currentText;
const executeTypingAnimation = () => { const executeTypingAnimation = () => {
if (isDeleting) { if (isDeleting) {
if (displayedText === '') { if (displayedText === "") {
setIsDeleting(false); setIsDeleting(false);
if (currentTextIndex === textArray.length - 1 && !loop) { if (currentTextIndex === textArray.length - 1 && !loop) {
return; return;
@@ -117,22 +130,24 @@ const TextType = ({
onSentenceComplete(textArray[currentTextIndex], currentTextIndex); onSentenceComplete(textArray[currentTextIndex], currentTextIndex);
} }
setCurrentTextIndex(prev => (prev + 1) % textArray.length); setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
setCurrentCharIndex(0); setCurrentCharIndex(0);
timeout = setTimeout(() => {}, pauseDuration); timeout = setTimeout(() => {}, pauseDuration);
} else { } else {
timeout = setTimeout(() => { timeout = setTimeout(() => {
setDisplayedText(prev => prev.slice(0, -1)); setDisplayedText((prev) => prev.slice(0, -1));
}, deletingSpeed); }, deletingSpeed);
} }
} else { } else {
if (currentCharIndex < processedText.length) { if (currentCharIndex < processedText.length) {
timeout = setTimeout( timeout = setTimeout(
() => { () => {
setDisplayedText(prev => prev + processedText[currentCharIndex]); setDisplayedText(
setCurrentCharIndex(prev => prev + 1); (prev) => prev + processedText[currentCharIndex],
);
setCurrentCharIndex((prev) => prev + 1);
}, },
variableSpeed ? getRandomSpeed() : typingSpeed variableSpeed ? getRandomSpeed() : typingSpeed,
); );
} else if (textArray.length > 1) { } else if (textArray.length > 1) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
@@ -142,7 +157,7 @@ const TextType = ({
} }
}; };
if (currentCharIndex === 0 && !isDeleting && displayedText === '') { if (currentCharIndex === 0 && !isDeleting && displayedText === "") {
timeout = setTimeout(executeTypingAnimation, initialDelay); timeout = setTimeout(executeTypingAnimation, initialDelay);
} else { } else {
executeTypingAnimation(); executeTypingAnimation();
@@ -163,18 +178,19 @@ const TextType = ({
isVisible, isVisible,
reverseMode, reverseMode,
variableSpeed, variableSpeed,
onSentenceComplete onSentenceComplete,
]); ]);
const shouldHideCursor = const shouldHideCursor =
hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting); hideCursorWhileTyping &&
(currentCharIndex < textArray[currentTextIndex].length || isDeleting);
return createElement( return createElement(
Component, Component,
{ {
ref: containerRef, ref: containerRef,
className: `inline-block whitespace-pre-wrap tracking-tight ${className}`, className: `inline-block whitespace-pre-wrap tracking-tight ${className}`,
...props ...props,
}, },
<span className="inline" style={{ color: getCurrentTextColor() }}> <span className="inline" style={{ color: getCurrentTextColor() }}>
{displayedText} {displayedText}
@@ -182,11 +198,11 @@ const TextType = ({
showCursor && ( showCursor && (
<span <span
ref={cursorRef} ref={cursorRef}
className={`ml-1 inline-block opacity-100 ${shouldHideCursor ? 'hidden' : ''} ${cursorClassName}`} className={`ml-1 inline-block opacity-100 ${shouldHideCursor ? "hidden" : ""} ${cursorClassName}`}
> >
{cursorCharacter} {cursorCharacter}
</span> </span>
) ),
); );
}; };
@@ -16,7 +16,7 @@ export function AnimatedGradientText({
<motion.span <motion.span
className={cn( className={cn(
"bg-gradient-to-r from-cyan-400 via-blue-500 to-blue-600 bg-clip-text text-transparent font-bold", "bg-gradient-to-r from-cyan-400 via-blue-500 to-blue-600 bg-clip-text text-transparent font-bold",
className className,
)} )}
animate={{ animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"], backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
+1 -1
View File
@@ -13,7 +13,7 @@ export function AuroraText({ children, className }: AuroraTextProps) {
<motion.span <motion.span
className={cn( className={cn(
"bg-gradient-to-r from-blue-400 via-cyan-500 to-blue-500 bg-clip-text text-transparent", "bg-gradient-to-r from-blue-400 via-cyan-500 to-blue-500 bg-clip-text text-transparent",
className className,
)} )}
animate={{ animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"], backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
+2 -2
View File
@@ -20,7 +20,7 @@ export function BentoGrid({ children, className }: BentoGridProps) {
<div <div
className={cn( className={cn(
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto justify-center", "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-6xl mx-auto justify-center",
className className,
)} )}
> >
{children} {children}
@@ -39,7 +39,7 @@ export function BentoGridItem({
className={cn( className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm p-6", "relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm p-6",
`col-span-${colSpan} row-span-${rowSpan}`, `col-span-${colSpan} row-span-${rowSpan}`,
className className,
)} )}
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
+1 -1
View File
@@ -13,7 +13,7 @@ export function MagicCard({ children, className }: MagicCardProps) {
<motion.div <motion.div
className={cn( className={cn(
"relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm", "relative overflow-hidden rounded-lg border border-white/20 bg-white/5 backdrop-blur-sm",
className className,
)} )}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
+1 -1
View File
@@ -18,7 +18,7 @@ export function ShimmerButton({
<motion.button <motion.button
className={cn( 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", "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 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} 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); positions.set([x * r, y * r, z * r], i * 3);
randoms.set( randoms.set(
[Math.random(), Math.random(), Math.random(), Math.random()], [Math.random(), Math.random(), Math.random(), Math.random()],
i * 4 i * 4,
); );
const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]); const col = hexToRgb(palette[Math.floor(Math.random() * palette.length)]);
colors.set(col, i * 3); colors.set(col, i * 3);
+3 -3
View File
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { 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": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -23,9 +19,7 @@
} }
], ],
"paths": { "paths": {
"@/*": [ "@/*": ["./*"]
"./*"
]
} }
}, },
"include": [ "include": [
@@ -35,7 +29,5 @@
".next/types/**/*.ts", ".next/types/**/*.ts",
".next/dev/types/**/*.ts" ".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }