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