feat: Implement initial portfolio website structure and core components including main page, header, hero, contact, and work modalities.
This commit is contained in:
1 parent
0523052864
commit
6299ae040a
7 files changed
+332
-206
No files matched your search
+172
-111
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user