feat: Initialize portfolio project with core pages, UI components, and configuration files.
This commit is contained in:
1 parent
aa07d6faeb
commit
0523052864
38 files changed
+1165
-460
No files matched your search
+13
-13
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
@@ -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
@@ -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%"],
|
||||||
|
|||||||
@@ -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%"],
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
@@ -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));
|
||||||
}
|
}
|
||||||
@@ -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&family=Geist+Mono:wght@400;500&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 |
@@ -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 |
@@ -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&family=Geist+Mono:wght@400;500;600&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
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user