feat: Implement initial portfolio website structure and core components including main page, header, hero, contact, and work modalities.

This commit is contained in:
Evandro Fonseca Junior committed 2026-03-07 15:17:05 -03:00
1 parent 0523052864
commit 6299ae040a
7 files changed
+332 -206

No files matched your search

+172 -111
View File
@@ -1,5 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Logo } from "./Logo";
const SOCIAL_LINKS = [
@@ -30,13 +32,103 @@ const SOCIAL_LINKS = [
},
];
const STATS = [
{ value: "4", accent: "px", label: "Base unit" },
{ value: "9", accent: "x", label: "Type scale" },
{ value: "4", accent: "+", label: "Core tokens" },
];
const textToType = "evandro.dev.br";
export default function Hero() {
const [phase, setPhase] = useState<"typing" | "idle" | "glitch" | "logo">(
"typing",
);
const [typedCount, setTypedCount] = useState(0);
useEffect(() => {
let timeout: NodeJS.Timeout;
if (phase === "typing") {
if (typedCount < textToType.length) {
timeout = setTimeout(() => {
setTypedCount((c) => c + 1);
}, 120); // typing speed
} else {
timeout = setTimeout(() => {
setPhase("idle");
}, 1200); // Wait after typing finishes
}
} else if (phase === "idle") {
timeout = setTimeout(() => {
setPhase("glitch");
}, 100); // trigger glitch
} else if (phase === "glitch") {
timeout = setTimeout(() => {
setPhase("logo");
}, 600); // Glitch out duration
} else if (phase === "logo") {
timeout = setTimeout(() => {
setTypedCount(0);
setPhase("typing");
}, 3500); // Logo display and blink duration
}
return () => clearTimeout(timeout);
}, [phase, typedCount]);
const renderTypedText = () => {
const chars = [];
for (let i = 0; i < typedCount; i++) {
const char = textToType[i];
if (i < 7) {
chars.push(
<span key={i} style={{ color: "#FFFFFF" }}>
{char}
</span>,
);
} else if (i === 7) {
chars.push(<br key="br1" />);
chars.push(
<span key={i} style={{ color: "#E8FF00" }}>
{char}
</span>,
);
} else if (i < 11) {
chars.push(
<span key={i} style={{ color: "#E8FF00" }}>
{char}
</span>,
);
} else if (i === 11) {
chars.push(<br key="br2" />);
chars.push(
<span key={i} style={{ color: "#FFFFFF" }}>
{char}
</span>,
);
} else {
chars.push(
<span key={i} style={{ color: "#FFFFFF" }}>
{char}
</span>,
);
}
}
// blinking cursor
if (phase === "typing" || phase === "idle") {
chars.push(
<motion.span
key="cursor"
animate={{ opacity: [1, 0] }}
transition={{ repeat: Infinity, duration: 0.8 }}
style={{
color: "#E8FF00",
display: "inline-block",
marginLeft: "4px",
}}
>
_
</motion.span>,
);
}
return chars;
};
const hour = new Date().getHours();
const greeting =
hour >= 8 && hour < 12
@@ -53,8 +145,13 @@ export default function Hero() {
<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-8 lg:grid-cols-2">
<div>
<div className="relative z-10 mx-auto grid min-h-[calc(100vh)] w-full max-w-[1000px] grid-cols-1 items-center gap-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<div className="mb-sp-8 inline-flex items-center gap-sp-2">
<span
className="h-1.5 w-1.5 animate-pulse rounded-full"
@@ -68,16 +165,61 @@ export default function Hero() {
</span>
</div>
<h1
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
<br />
<span style={{ color: "#E8FF00" }}>.dev</span>
<br />
.br
</h1>
<div className="relative mb-sp-6 flex min-h-[40vw] sm:min-h-[220px] md:min-h-[280px] lg:min-h-[350px] items-center">
<AnimatePresence mode="wait">
{(phase === "typing" || phase === "idle") && (
<motion.h1
key="text"
initial={{ opacity: 0 }}
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(12px)", scale: 1.05 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute 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,
}}
aria-label="evandro.dev.br"
>
{renderTypedText()}
</motion.h1>
)}
{phase === "logo" && (
<motion.div
key="logo"
initial={{ opacity: 0, scale: 0.95 }}
animate={{
opacity: [0, 1, 0, 1, 1, 0.5, 1, 0],
scale: 1,
filter: [
"blur(4px)",
"blur(0px)",
"blur(0px)",
"blur(0px)",
"blur(0px)",
"blur(2px)",
"blur(0px)",
"blur(10px)",
],
}}
transition={{
duration: 3.2,
times: [0, 0.05, 0.1, 0.15, 0.8, 0.85, 0.9, 1],
}}
className="absolute flex items-center justify-start w-full"
>
<Logo
variant="mark"
size="xl"
theme="dark"
className="w-[80%] max-w-[320px] h-auto drop-shadow-[0_0_20px_rgba(232,255,0,0.3)]"
/>
</motion.div>
)}
</AnimatePresence>
</div>
<p
className="mb-sp-10 max-w-[460px] text-[13px] leading-7"
@@ -88,15 +230,24 @@ export default function Hero() {
React, Tauri e arquitetura escalavel.
</p>
<div className="mb-sp-8 flex flex-wrap items-center gap-sp-4">
<div className="mb-sp-8 mt-sp-6 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-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)]"
className="inline-flex h-11 items-center justify-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>
<a
href="/assets/Evandro_Fonseca_Junior_CV.pdf"
download
className="inline-flex h-11 items-center justify-center rounded-sm border px-sp-6 text-[11px] font-semibold uppercase tracking-[0.08em] no-underline transition-all duration-200 hover:-translate-y-0.5 hover:bg-white/5"
style={{ borderColor: "rgba(255,255,255,.18)", color: "#FFFFFF" }}
>
Download CV
</a>
<div className="flex items-center gap-sp-2">
{SOCIAL_LINKS.map((link) => (
<a
@@ -108,9 +259,8 @@ export default function Hero() {
? "noopener noreferrer"
: undefined
}
className="inline-flex h-11 w-11 items-center justify-center rounded-sm border transition-all duration-200 hover:bg-white/10"
className="inline-flex h-11 w-11 items-center justify-center rounded-full transition-all duration-200 hover:bg-white/10 hover:scale-110"
style={{
borderColor: "rgba(255,255,255,.18)",
color: "rgba(255,255,255,.84)",
}}
aria-label={link.label}
@@ -139,96 +289,7 @@ export default function Hero() {
Garuva, SC | Remote worldwide
</div>
<div
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)",
}}
>
{STATS.map((item) => (
<div
key={item.label}
className="p-5"
style={{ background: "rgba(255,255,255,.03)" }}
>
<div
className="font-display text-2xl font-extrabold tracking-[-0.03em]"
style={{ color: "#FFFFFF" }}
>
{item.value}
<span style={{ color: "#E8FF00" }}>{item.accent}</span>
</div>
<div
className="mt-sp-1 text-[9px] uppercase tracking-[0.12em]"
style={{ color: "rgba(255,255,255,.35)" }}
>
{item.label}
</div>
</div>
))}
</div>
</div>
<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={{
borderColor: "rgba(255,255,255,.12)",
background: "rgba(255,255,255,.03)",
}}
>
<span
className="absolute left-6 top-5 text-[9px] tracking-[0.14em]"
style={{ color: "rgba(255,255,255,.32)" }}
>
EV / LOGO MARK
</span>
<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: "var(--slash)", opacity: 0.78 }}
>
CONCEITO ESCOLHIDO
</span>
</div>
<div
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)" }}
/>
EV / Full mark · identidade visual
</div>
<div
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: 280x280 · svg viewbox
</div>
</div>
</motion.div>
</div>
</section>
);