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

196 lines
5.2 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import SafeImage from "./SafeImage";
interface Experience {
name: string;
company: string;
position: string;
description: string;
startDate: string;
endDate: string;
logo?: string;
}
interface ExperienceProps {
experiences: Experience[];
}
function formatDate(d: string) {
if (d === "current") return "Present";
const [, m, y] = d.split("-");
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return `${months[parseInt(m) - 1]} ${y}`;
}
function ExperienceItem({ exp, index }: { exp: Experience; index: number }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => el.classList.add("visible"), index * 100);
obs.disconnect();
}
},
{ threshold: 0.1 }
);
obs.observe(el);
return () => obs.disconnect();
}, [index]);
const isCurrent = exp.endDate === "current";
return (
<div
ref={ref}
className="reveal"
style={{
display: "grid",
gridTemplateColumns: "120px 1px 1fr",
gap: "0 2rem",
paddingBottom: "3rem",
}}
>
{/* Date */}
<div style={{ textAlign: "right", paddingTop: "2px" }}>
<div
className="font-mono"
style={{
fontFamily: "var(--font-jetbrains), monospace",
fontSize: "0.62rem",
letterSpacing: "0.08em",
color: isCurrent ? "#c8a96e" : "#4a5060",
lineHeight: 1.6,
}}
>
{isCurrent && (
<div style={{ color: "#c8a96e", marginBottom: "0.25rem", letterSpacing: "0.14em" }}>
NOW
</div>
)}
{formatDate(exp.startDate)}
<br /><br />
{formatDate(exp.endDate)}
</div>
</div>
{/* Line with dot */}
<div style={{ position: "relative", display: "flex", justifyContent: "center" }}>
<div style={{ width: "1px", background: "#1c1f26", height: "100%" }} />
<div
style={{
position: "absolute",
top: "4px",
width: "8px",
height: "8px",
borderRadius: "50%",
background: isCurrent ? "#c8a96e" : "#1c1f26",
border: isCurrent ? "none" : "1px solid #4a5060",
left: "50%",
transform: "translateX(-50%)",
}}
/>
</div>
{/* Content */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem", marginBottom: "0.5rem" }}>
{exp.logo && (
<SafeImage
src={exp.logo}
alt={exp.company}
width={28}
height={28}
fallbackLabel={exp.company.slice(0, 2).toUpperCase()}
style={{ width: "24px", height: "24px", objectFit: "contain", filter: "brightness(0.7) grayscale(0.4)" }}
/>
)}
<span
className="section-label"
style={{ fontSize: "0.58rem" }}
>
{exp.company}
</span>
</div>
<h3
className="font-display"
style={{
fontFamily: "var(--font-bebas), sans-serif",
fontSize: "1.5rem",
letterSpacing: "0.04em",
color: "#e2e4e9",
marginBottom: "0.5rem",
}}
>
{exp.position}
</h3>
<p
style={{
fontFamily: "var(--font-lora), serif",
fontSize: "0.9rem",
lineHeight: 1.7,
color: "#6b7280",
}}
>
{exp.description}
</p>
</div>
</div>
);
}
export default function Experience({ experiences }: ExperienceProps) {
const headingRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = headingRef.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) { el.classList.add("visible"); obs.disconnect(); }
},
{ threshold: 0.1 }
);
obs.observe(el);
return () => obs.disconnect();
}, []);
return (
<section
id="experience"
style={{ padding: "8rem 2rem", background: "#0a0b0e" }}
>
<div style={{ maxWidth: "1200px", margin: "0 auto" }}>
<div ref={headingRef} className="reveal" style={{ marginBottom: "4rem" }}>
<div className="section-label" style={{ marginBottom: "0.75rem" }}>Career Path</div>
<h2
className="font-display"
style={{
fontFamily: "var(--font-bebas), sans-serif",
fontSize: "clamp(2.5rem, 6vw, 5rem)",
letterSpacing: "0.04em",
color: "#e2e4e9",
lineHeight: 1,
}}
>
Experience
</h2>
<div style={{ width: "48px", height: "1px", background: "#c8a96e", marginTop: "1rem" }} />
</div>
<div>
{experiences.map((exp, i) => (
<ExperienceItem key={exp.name} exp={exp} index={i} />
))}
</div>
</div>
</section>
);
}