// Section components - Hero, Profile, Lab, Simulator, Activities, Selected, // Timeline, Final Reflection, Contact. All read from window.PORTFOLIO_DATA. const D = window.PORTFOLIO_DATA; const SIM = window.SIM_DATA; // Tiny i18n helper: T("english", "dutch", lang) - keeps the JSX compact. const T = (en, nl, lang) => (lang === "nl" ? nl : en); // TR(v, lang) - resolves a value that may be a plain string (legacy) or // a { en, nl } object. Falls back to en if the requested lang is missing. const TR = (v, lang) => v && typeof v === "object" && !Array.isArray(v) ? (v[lang] || v.en) : v; // ───── Hero / Helmet visor ───────────────────────────────────────────────── // Two-stage hero: full-bleed helmet portrait that "zooms into the visor" as // the user scrolls - by the time they hit the Profile section the camera is // inside the light-beam tunnel. window.Hero = function Hero({ lang, accent }) { const [progress, setProgress] = React.useState(0); const ref = React.useRef(null); React.useEffect(() => { const onScroll = () => { const el = ref.current; if (!el) return; const rect = el.getBoundingClientRect(); const vh = window.innerHeight; const total = rect.height - vh; const scrolled = -rect.top; const p = Math.max(0, Math.min(1, scrolled / Math.max(total, 1))); setProgress(p); }; onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, []); const tag = D.taglines.primary[lang] || D.taglines.primary.en; const sub = D.taglines.sub[lang] || D.taglines.sub.en; const tagParts = tag.split(","); const zoom = 1 + progress * 4.0; const offsetY = -progress * 6; const titleFade = 1 - Math.min(1, progress * 2.4); const titleShift = -progress * 60; const vignetteFade = Math.min(1, progress * 1.6); const beamGlow = Math.min(1, progress * 1.4); return (

{tagParts[0]},
{tagParts[1]?.trim()},
{tagParts[2]?.trim()}

{sub}

{T("SCROLL · INTO THE VISOR", "SCROLL · DOOR HET VIZIER", lang)}
); }; // ───── Driver Profile ────────────────────────────────────────────────────── window.Profile = function Profile({ lang }) { const [animated, setAnimated] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { const obs = new IntersectionObserver( (entries) => entries.forEach((e) => { if (e.isIntersecting) setAnimated(true); }), { threshold: 0.3 } ); if (ref.current) obs.observe(ref.current); return () => obs.disconnect(); }, []); // Skill labels can be either a plain string (legacy) or { en, nl }. Resolve // here so EN/NL switching reaches the radar dots and the skill-bar text. const resolveLabel = (s) => typeof s.label === "object" ? (s.label[lang] || s.label.en) : s.label; const cx = 130, cy = 130, R = 96; const N = D.skills.length; const points = D.skills.map((s, i) => { const a = (Math.PI * 2 * i) / N - Math.PI / 2; const r = R * (animated ? s.value : 0); return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a), label: resolveLabel(s), val: s.value, ang: a }; }); const polyPts = points.map((p) => `${p.x},${p.y}`).join(" "); return (
{T("Driver Profile", "Coureur profiel", lang)}

Martijn Van Baelen

{T( "A student wiring motorsport and data into one craft. Here are the numbers behind the profile.", "Een student die motorsport en data combineert. Hier de cijfers achter het profiel.", lang )}

{D.identity.name}

{T("22 years · Student", "22 jaar · Student", lang)}

{D.about[lang] || D.about.en}

{T("Interests", "Interesses", lang)}
{D.interests.map((t) => {t})}
Stack
{D.techStack.map((t) => {t})}

{T("TELEMETRY · SKILLS", "TELEMETRIE · SKILLS", lang)}

{D.skills.map((s) => (
{resolveLabel(s)}
{Math.round(s.value * 100)}
))}
{[0.25, 0.5, 0.75, 1].map((r, i) => ( { const a = (Math.PI * 2 * j) / N - Math.PI / 2; return `${cx + R * r * Math.cos(a)},${cy + R * r * Math.sin(a)}`; }).join(" ")} fill="none" stroke="var(--line)" strokeWidth="0.5" /> ))} {Array.from({ length: N }).map((_, i) => { const a = (Math.PI * 2 * i) / N - Math.PI / 2; return ; })} {points.map((p, i) => ( {p.label.split(" ")[0].toUpperCase()} ))}
); }; // ───── AI / Data Lab ─────────────────────────────────────────────────────── window.Lab = function Lab({ lang }) { return (
{T("Motorsport AI & Data Lab", "Motorsport AI & Data Lab", lang)}

{T("The lab - AI & data in motorsport", "Het lab - AI & data in motorsport", lang)}

{T( "How I think about AI and data analysis in motorsport - especially pitstops, where hundredths decide who exits ahead.", "Hoe ik kijk naar AI en data-analyse in motorsport - vooral in pitstops, waar honderdsten beslissen wie er voor staat.", lang )}

{D.lab.map((c) => { const title = typeof c.title === "object" ? (c.title[lang] || c.title.en) : c.title; const desc = typeof c.desc === "object" ? (c.desc[lang] || c.desc.en) : c.desc; return (

{title}

{desc}

); })}
); }; // ───── Simulator section ─────────────────────────────────────────────────── window.SimulatorSection = function SimulatorSection({ accent, lang }) { const N_STOPS = 10000; const [crewKey, setCrewKey] = React.useState(SIM.defaultCrewKey); const [result, setResult] = React.useState(null); const [animKey, setAnimKey] = React.useState(0); // Re-run the Monte Carlo whenever the user picks a different crew. React.useEffect(() => { const crew = SIM.crews[crewKey]; setResult(null); const id = setTimeout(() => { const r = window.MonteCarlo.runMany(N_STOPS, crew.edges); setResult(r); setAnimKey((k) => k + 1); }, 30); return () => clearTimeout(id); }, [crewKey]); const crew = SIM.crews[crewKey]; const crewKeys = Object.keys(SIM.crews); return (
{T("Featured Project", "Uitgelicht project", lang)}

IndyCar Pitstop Monte Carlo

{T( "Live, in your browser. Pick a pit crew and watch the median pitstop play out.", "Live, in je browser. Kies een pitcrew en bekijk de mediane stop.", lang )}

PIT-WALL DASHBOARD
{T("INTERNSHIP · ASPHALT ANALYTICS · HUNTERSVILLE NC", "STAGE · ASPHALT ANALYTICS · HUNTERSVILLE NC", lang)}
{result && }
{result && (
{T("Median pitstop time", "Mediaan pitstoptijd", lang)} · {crew.name} {result.animation_data.total_time.toFixed(2)}s
)}
); }; // ───── Activities ────────────────────────────────────────────────────────── window.Activities = function Activities({ lang }) { const [filter, setFilter] = React.useState("all"); const [flashId, setFlashId] = React.useState(null); // Cross-section jump: the Timeline dispatches a "scrollToActivity" event // when one of its dots is clicked. Reset the filter to "all" so the target // card is in the DOM, then smooth-scroll to it on the next frame. React.useEffect(() => { const handler = (e) => { const id = e.detail; if (!id) return; setFilter("all"); requestAnimationFrame(() => { const target = document.getElementById("activity-" + id); if (!target) return; target.scrollIntoView({ behavior: "smooth", block: "center" }); setFlashId(id); setTimeout(() => setFlashId((cur) => (cur === id ? null : cur)), 1600); }); }; window.addEventListener("scrollToActivity", handler); return () => window.removeEventListener("scrollToActivity", handler); }, []); const items = filter === "all" ? D.activities : D.activities.filter((a) => a.domain === filter); const counts = D.activities.reduce((acc, a) => ({ ...acc, [a.domain]: (acc[a.domain] || 0) + 1 }), {}); const dateLocale = lang === "nl" ? "nl-BE" : "en-GB"; const filters = [ { k: "all", l: T("All", "Alle", lang) }, { k: "seminar", l: T("Seminars", "Seminaries", lang) }, { k: "innovation", l: T("Innovation", "Innovatie", lang) }, { k: "personal", l: T("Personal Dev", "Persoonlijke ontwikkeling", lang) }, { k: "international", l: T("International", "Internationaal", lang) }, ]; return (
{T("Activities Overview", "Overzicht activiteiten", lang)}

{T("Activities · by domain", "Activiteiten · per domein", lang)}

{T( "Every activity with a short description, location and time.", "Elke activiteit met een korte beschrijving, locatie en tijd.", lang )}

{filters.map((f) => ( ))}
{items.map((a) => (
{new Date(a.date).toLocaleDateString(dateLocale, { day: "2-digit", month: "short", year: "numeric" })} {a.domain}

{TR(a.title, lang)}

{TR(a.desc, lang)}

{T("Location", "Locatie", lang)}
{TR(a.location, lang)}
{T("Time", "Tijd", lang)}
{a.duration}
))}
); }; // ───── Selected highlights ───────────────────────────────────────────────── window.Selected = function Selected({ lang }) { const stage = D.selected["stage-usa"]; const hack = D.selected["hack-future"]; const pw = D.selected["projectweek"]; const labels = { goal: T("Goal of the activity", "Doel van de activiteit", lang), personal: T("My personal goals", "Mijn persoonlijke doelen", lang), did: T("What I did", "Wat ik deed", lang), learned: T("What I learned", "Wat ik geleerd heb", lang), reflection: T("Reflection", "Reflectie", lang), }; return (
{T("Race Highlights", "Race-hoogtepunten", lang)}

{T("Selected activities", "Geselecteerde activiteiten", lang)}

★ {T("RACE OF THE SEASON", "RACE VAN HET SEIZOEN", lang)}

{T("Internship USA · Asphalt Analytics", "Stage USA · Asphalt Analytics", lang)}

{T( "HUNTERSVILLE · NORTH CAROLINA · 12 WEEKS · 09 MAR – 29 MAY 2026", "HUNTERSVILLE · NORTH CAROLINA · 12 WEKEN · 09 MRT – 29 MEI 2026", lang )}

{TR(stage.description, lang)}

HASSELT · BE HUNTERSVILLE · NC ≈ 7,000 KM · BRU → CLT
{T("DARLINGTON · TRACKSIDE", "DARLINGTON · LANGS DE BAAN", lang)}

{T( "With the trophy after Tyler Reddick (#45 · 23XI Racing) took the Goodyear 400 at Darlington Raceway.", "Met de trofee na de overwinning van Tyler Reddick (#45 · 23XI Racing) in de Goodyear 400 op Darlington Raceway.", lang )}

{T( "Trackside in the team garage, right after the win - one of the moments the internship turned from a study placement into something I'll carry with me.", "Langs de baan in de garage, vlak na de overwinning - één van die momenten waarop de stage van schoolopdracht naar iets blijvends omsloeg.", lang )}

Martijn with the Darlington trophy after Tyler Reddick's #45 (23XI Racing) win

{labels.goal}

{TR(stage.goal, lang)}

{labels.personal}

{TR(stage.personalGoal, lang)}

{labels.did}

{TR(stage.did, lang)}

{labels.learned}

{TR(stage.learned, lang)}

{labels.reflection}

{TR(stage.reflection, lang)}

{TR(hack.title, lang)}

{TR(hack.subtitle, lang)}

{TR(hack.description, lang)}

SCORE 67% {T("RANK 9 / 11", "PLAATS 9 / 11", lang)} PYTHON · ML

{labels.goal}

{TR(hack.goal, lang)}

{labels.personal}

{TR(hack.personalGoal, lang)}

{labels.did}

{TR(hack.did, lang)}

{labels.learned}

{TR(hack.learned, lang)}

{labels.reflection}

{TR(hack.reflection, lang)}

{TR(pw.title, lang)}

{TR(pw.subtitle, lang)}

{TR(pw.description, lang)}

{T("TEAM KICK-OFF", "TEAM KICK-OFF", lang)} {T("JOB EVENT", "JOB EVENT", lang)} {T("RESEARCH PROJECT", "RESEARCH PROJECT", lang)}

{labels.goal}

{TR(pw.goal, lang)}

{labels.personal}

{TR(pw.personalGoal, lang)}

{labels.did}

{TR(pw.did, lang)}

{labels.learned}

{TR(pw.learned, lang)}

{labels.reflection}

{TR(pw.reflection, lang)}

); }; // ───── Timeline ──────────────────────────────────────────────────────────── window.Timeline = function Timeline({ lang }) { const yr2 = D.timeline.year2; const yr3 = D.timeline.year3; const renderRow = (events, yearKey, yearLabel, yearDesc) => (
{yearKey} {yearLabel} · {yearDesc}
{events.map((e, i) => { const left = events.length === 1 ? 50 : (i / (events.length - 1)) * 100; const above = i % 2 === 0; const clickable = !!e.activityId; const onJump = clickable ? () => window.dispatchEvent(new CustomEvent("scrollToActivity", { detail: e.activityId })) : undefined; const onKey = clickable ? (ev) => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); onJump(); } } : undefined; return (
{!above &&
}
{above &&
} {e.label} {e.date} {e.desc}
); })}
); return (
{T("Race Season", "Race-seizoen", lang)}

{T("The season calendar", "De seizoenskalender", lang)}

{T( "Two consecutive seasons - 2TIN (year 2) and 3TIN (year 3, starting at the Innovation Route). Every round a milestone.", "Twee seizoenen achter elkaar - 2TIN (jaar 2) en 3TIN (jaar 3, vanaf de Innovation Route). Elke ronde een mijlpaal.", lang )}

{T("Seminar", "Seminar", lang)} {T("Innovation", "Innovatie", lang)} {T("Personal dev", "Persoonlijk", lang)} {T("International", "Internationaal", lang)} {T("Future", "Toekomst", lang)}
{renderRow(yr2, "Y2", T("2TIN · Year 2", "2TIN · Jaar 2", lang), T("Projectweek → seminars", "Projectweek → seminaries", lang) )} {renderRow(yr3, "Y3", T("3TIN · Year 3", "3TIN · Jaar 3", lang), T("Innovation route → US Internship → future", "Innovation route → Stage USA → toekomst", lang) )}
); }; // ───── PXL X-Factor block ────────────────────────────────────────────────── function XFactor({ lang }) { const xf = D.xFactor; return (

{T("PXL framework", "PXL-kader", lang)}

{T("The PXL X-Factor - my four axes", "De PXL X-Factor - mijn vier assen", lang)}

{T("PERSONAL REFLECTION", "EIGEN REFLECTIE", lang)}
{xf.axes.map((a, i) => { const corner = ["tl", "tr", "bl", "br"][i] || "tl"; return (
{TR(a.label, lang)}
{TR(a.note, lang)}
); })}
); } // ───── Final reflection ──────────────────────────────────────────────────── window.Final = function Final({ lang }) { const r = D.finalReflection; return (
{T("Finish Line", "Eindstreep", lang)}

{T("The post-race debrief", "De post-race debrief", lang)}

{T( "Goals reached? Who have I become? Next steps?", "Doelen behaald? Wie ben ik geworden? Volgende stappen?", lang )}

{T("Goals reached", "Doelen behaald", lang)}

{TR(r.goalsReached, lang)}

{T("Who I am now", "Wie ik nu ben", lang)}

{TR(r.whoIAm, lang)}

{T("What I learned", "Wat ik geleerd heb", lang)}

{TR(r.learned, lang)}

{T("Strengths", "Sterktes", lang)}

{TR(r.strengths, lang)}

{T("Growth points", "Groeipunten", lang)}

{TR(r.growth, lang)}

{T("Next steps", "Volgende stappen", lang)}

{TR(r.nextSteps, lang)}

{T("THE FUTURE", "DE TOEKOMST", lang)}

{TR(r.future, lang)}

); }; // ───── Contact ───────────────────────────────────────────────────────────── window.Contact = function Contact({ lang }) { return (
{T("Next race", "Volgende race", lang)}

{T("Ready for the next round.", "Klaar voor de volgende ronde.", lang)}

{T( "Looking for data / AI roles in motorsport, race engineering or pitstop analytics. Open to junior roles and conversations.", "Op zoek naar een data- of AI-rol in motorsport, race-engineering of pitstop-analytics. Open voor junior rollen en gesprekken.", lang )}

); };