Files
achraf-portfolio/components/Hero.tsx
2026-03-31 00:28:00 +02:00

414 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import SafeImage from "./SafeImage";
interface Task {
name: string;
description: string;
status: string;
technologies: string[];
}
interface ProjectLink {
name: string;
url: string;
}
interface Project {
name: string;
description: string;
logo?: string;
links?: ProjectLink[];
tasks: Task[];
}
interface Experience {
company: string;
position: string;
startDate: string;
endDate: string;
logo?: string;
}
interface Education {
degree: string;
school: string;
startDate: string;
endDate: string;
}
interface HeroProps {
name: string;
lastName: string;
title: string;
description: string;
skills: string[];
projects: Project[];
experiences: Experience[];
education: Education[];
languages: { name: string; level: string }[];
interests: string[];
location: string;
}
// ─── BentoCard ───────────────────────────────────────────────────────────────
function BentoCard({
num,
label,
href,
children,
accent = false,
delay = 0,
}: {
num: string;
label: string;
href: string;
children: React.ReactNode;
accent?: boolean;
delay?: number;
}) {
return (
<a
href={href}
style={{
animationName: "fadeUp",
animationDuration: "0.7s",
animationTimingFunction: "ease",
animationFillMode: "both",
animationDelay: `${delay}ms`,
display: "flex",
flexDirection: "column",
padding: "1.75rem",
border: "1px solid #1c1f26",
background: accent ? "rgba(200,169,110,0.03)" : "#0e1014",
textDecoration: "none",
color: "inherit",
position: "relative",
overflow: "hidden",
cursor: "pointer",
transition: "border-color 0.3s ease, background 0.3s ease, transform 0.3s ease",
minHeight: "200px",
}}
onMouseEnter={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.borderColor = "#6b5730";
el.style.background = accent ? "rgba(200,169,110,0.07)" : "rgba(200,169,110,0.03)";
el.style.transform = "translateY(-2px)";
const arrow = el.querySelector(".card-arrow") as HTMLElement | null;
if (arrow) arrow.style.opacity = "1";
const shine = el.querySelector(".card-shine") as HTMLElement | null;
if (shine) shine.style.opacity = "1";
}}
onMouseLeave={(e) => {
const el = e.currentTarget as HTMLElement;
el.style.borderColor = "#1c1f26";
el.style.background = accent ? "rgba(200,169,110,0.03)" : "#0e1014";
el.style.transform = "translateY(0)";
const arrow = el.querySelector(".card-arrow") as HTMLElement | null;
if (arrow) arrow.style.opacity = "0";
const shine = el.querySelector(".card-shine") as HTMLElement | null;
if (shine) shine.style.opacity = "0";
}}
>
<div
className="card-shine"
style={{
position: "absolute",
inset: 0,
background: "linear-gradient(135deg, rgba(200,169,110,0.06) 0%, transparent 50%)",
opacity: 0,
transition: "opacity 0.3s",
pointerEvents: "none",
}}
/>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "auto" }}>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.2em", color: "#c8a96e", opacity: 0.7 }}>
{num} · {label}
</div>
<div className="card-arrow" style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.75rem", color: "#c8a96e", opacity: 0, transition: "opacity 0.2s" }}>
</div>
</div>
<div style={{ marginTop: "1rem", flex: 1 }}>{children}</div>
</a>
);
}
// ─── Hero ─────────────────────────────────────────────────────────────────────
export default function Hero({
name,
lastName,
title,
description,
skills,
projects,
experiences,
education,
languages,
interests,
location,
}: HeroProps) {
const feat1 = projects[2]; // FOON
const feat2 = projects[0]; // Gazelle
const topSkills = skills.slice(0, 10);
const levelColor = (level: string) => {
if (level === "Native") return "#c8a96e";
if (level === "C2") return "#3a7a5a";
return "#4a6050";
};
return (
<section
id="hero"
className="hero-section"
style={{ minHeight: "100vh", position: "relative" }}
>
<div style={{ maxWidth: "1200px", margin: "0 auto", position: "relative", zIndex: 3 }}>
{/* ── Headline ─────────────────────────────────────────────────── */}
<div
className="animate-fade-up delay-1"
style={{ marginBottom: "2.5rem" }}
>
<h1
style={{
fontFamily: "var(--font-lora), Georgia, serif",
fontStyle: "italic",
fontWeight: 400,
fontSize: "clamp(2.6rem, 7vw, 7rem)",
lineHeight: 1.05,
letterSpacing: "-0.01em",
color: "#e2e4e9",
margin: 0,
userSelect: "none",
}}
>
Let&apos;s bend
<br />
<span style={{ color: "#c8a96e" }}>spacetime</span>
</h1>
</div>
{/* ── Name + description strip ──────────────────────────────── */}
<div className="hero-header-strip animate-fade-up delay-2">
<div>
<div
className="section-label"
style={{ marginBottom: "0.5rem", display: "flex", alignItems: "center", gap: "0.6rem" }}
>
<span style={{ display: "inline-block", width: "24px", height: "1px", background: "#c8a96e" }} />
{title}
</div>
<div
style={{
fontFamily: "var(--font-bebas), Impact, sans-serif",
fontSize: "clamp(1.6rem, 4vw, 3rem)",
letterSpacing: "0.04em",
color: "#6b7280",
lineHeight: 1,
}}
>
{name}{" "}
<span style={{ color: "#4a5060" }}>{lastName}</span>
</div>
</div>
<div style={{ maxWidth: "360px" }}>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.88rem", lineHeight: 1.75, color: "#6b7280", margin: 0 }}>
{description}
</p>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginTop: "0.6rem" }}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#4a5060" strokeWidth="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", letterSpacing: "0.1em", color: "#4a5060" }}>
{location}
</span>
</div>
</div>
</div>
{/* ── Divider ───────────────────────────────────────────────── */}
<div
className="animate-fade-up delay-3"
style={{ height: "1px", background: "#1c1f26", marginBottom: "2rem" }}
/>
{/* ── 3 × 2 Bento grid ──────────────────────────────────────── */}
<div className="hero-bento-grid">
{/* 01 — FOON */}
<BentoCard num="01" label="TypeScript SDK" href="#projects" accent delay={500}>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}>
{feat1.logo && (
<SafeImage src={feat1.logo} alt={feat1.name} width={28} height={28}
style={{ width: "22px", height: "22px", objectFit: "contain", filter: "brightness(0.9)" }}
/>
)}
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}>
{feat1.name}
</h3>
</div>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.82rem", lineHeight: 1.65, color: "#6b7280", marginBottom: "1rem" }}>
{feat1.description}
</p>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem", marginBottom: "0.75rem" }}>
{feat1.tasks.flatMap((t) => t.technologies).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).map((tech) => (
<span key={tech} className="tech-tag">{tech}</span>
))}
</div>
{feat1.links && (
<div style={{ display: "flex", gap: "0.6rem", flexWrap: "wrap", marginTop: "auto" }}>
{feat1.links.slice(0, 2).map((l) => (
<span key={l.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.6rem", letterSpacing: "0.1em", color: "#c8a96e", textTransform: "uppercase" }}>
{l.name}
</span>
))}
</div>
)}
</BentoCard>
{/* 02 — Gazelle */}
<BentoCard num="02" label="Java · Healthcare" href="#projects" delay={600}>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.75rem" }}>
{feat2.logo && (
<SafeImage src={feat2.logo} alt={feat2.name} width={28} height={28}
fallbackLabel={feat2.name.slice(0, 2).toUpperCase()}
style={{ width: "22px", height: "22px", objectFit: "contain", filter: "brightness(0.75) grayscale(0.3)" }}
/>
)}
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1 }}>
{feat2.name}
</h3>
</div>
<p style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.82rem", lineHeight: 1.65, color: "#6b7280", marginBottom: "1rem" }}>
{feat2.description}
</p>
<div style={{ display: "flex", flexDirection: "column", gap: "0.4rem" }}>
{feat2.tasks.map((task) => (
<div key={task.name} style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
<span style={{ width: "5px", height: "5px", borderRadius: "50%", background: task.status === "Done" ? "#3a7a5a" : "#c8954a", flexShrink: 0 }} />
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", color: "#6b7280", letterSpacing: "0.04em" }}>
{task.name}
</span>
<span className={task.status === "Done" ? "status-done" : "status-progress"}>{task.status}</span>
</div>
))}
</div>
</BentoCard>
{/* 03 — Experience */}
<BentoCard num="03" label="Career" href="#experience" delay={700}>
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
Experience
</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.65rem" }}>
{experiences.map((exp, i) => (
<div key={exp.company + exp.position} style={{ display: "flex", alignItems: "flex-start", gap: "0.75rem", opacity: i === 0 ? 1 : i === 1 ? 0.6 : 0.35 }}>
<div style={{ width: "6px", height: "6px", borderRadius: "50%", background: i === 0 ? "#c8a96e" : "#1c1f26", border: i === 0 ? "none" : "1px solid #4a5060", marginTop: "5px", flexShrink: 0 }} />
<div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.7rem", color: i === 0 ? "#e2e4e9" : "#6b7280", letterSpacing: "0.04em", lineHeight: 1.3 }}>
{exp.position}
</div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.12em", color: i === 0 ? "#c8a96e" : "#4a5060", textTransform: "uppercase", marginTop: "1px" }}>
{exp.company} · {exp.endDate === "current" ? "Present" : exp.endDate.split("-")[2]}
</div>
</div>
</div>
))}
</div>
</BentoCard>
{/* 04 — Education */}
<BentoCard num="04" label="Academic" href="#education" delay={800}>
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
Education
</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.85rem" }}>
{education.map((edu, i) => (
<div key={edu.degree} style={{ borderLeft: "2px solid #1c1f26", paddingLeft: "0.75rem" }}>
<div style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "0.88rem", letterSpacing: "0.06em", color: i === 0 ? "#e2e4e9" : "#6b7280", lineHeight: 1.3, marginBottom: "0.2rem" }}>
{edu.degree}
</div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.1em", color: "#4a5060", textTransform: "uppercase" }}>
{edu.school.split(" ").slice(-2).join(" ")} · {edu.endDate}
</div>
</div>
))}
</div>
</BentoCard>
{/* 05 — Skills */}
<BentoCard num="05" label="Stack" href="#skills" delay={900}>
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
{skills.length} Skills
</h3>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
{topSkills.map((skill, i) => (
<span key={skill} className="tech-tag" style={{ opacity: 1 - i * 0.06, fontSize: i < 3 ? "0.68rem" : "0.6rem" }}>
{skill}
</span>
))}
{skills.length > 10 && (
<span className="tech-tag" style={{ color: "#c8a96e", borderColor: "#6b5730" }}>
+{skills.length - 10}
</span>
)}
</div>
</BentoCard>
{/* 06 — About */}
<BentoCard num="06" label="Profile" href="#contact" accent delay={1000}>
<h3 style={{ fontFamily: "var(--font-bebas), sans-serif", fontSize: "1.8rem", letterSpacing: "0.04em", color: "#e2e4e9", lineHeight: 1, marginBottom: "1rem" }}>
About
</h3>
<div style={{ marginBottom: "0.85rem" }}>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.56rem", letterSpacing: "0.18em", color: "#4a5060", textTransform: "uppercase", marginBottom: "0.45rem" }}>
Languages
</div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{languages.map((lang) => (
<span key={lang.name} style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.62rem", padding: "2px 8px", border: `1px solid ${levelColor(lang.level)}`, color: levelColor(lang.level), letterSpacing: "0.06em" }}>
{lang.name} {lang.level}
</span>
))}
</div>
</div>
<div>
<div style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.56rem", letterSpacing: "0.18em", color: "#4a5060", textTransform: "uppercase", marginBottom: "0.45rem" }}>
Interests
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.35rem" }}>
{interests.map((interest, i) => (
<span key={interest} style={{ fontFamily: "var(--font-lora), serif", fontSize: "0.78rem", fontStyle: "italic", color: "#6b7280" }}>
{interest}{i < interests.length - 1 && <span style={{ color: "#1c1f26", marginLeft: "0.35rem" }}>·</span>}
</span>
))}
</div>
</div>
</BentoCard>
</div>
{/* ── Scroll hint ───────────────────────────────────────────── */}
<div
className="animate-fade-up delay-6"
style={{ marginTop: "2rem", display: "flex", alignItems: "center", justifyContent: "center", gap: "0.75rem" }}
>
<div style={{ height: "1px", width: "40px", background: "#1c1f26" }} />
<span style={{ fontFamily: "var(--font-jetbrains), monospace", fontSize: "0.58rem", letterSpacing: "0.2em", color: "#4a5060", textTransform: "uppercase" }}>
Scroll to explore
</span>
<div style={{ height: "1px", width: "40px", background: "#1c1f26" }} />
</div>
</div>
</section>
);
}