// components.jsx — Mother's Day Trivia for Demetria // ───────────────────────────── Sleeve (crate item) ───────────────────────────── function Sleeve({ q, onPick, played, locked }) { const isBonus = q.bonus; // Played tracks remain replayable for fun; only locked sleeves block clicks. const isDisabled = locked; // Read real audio duration via metadata preload const [dur, setDur] = React.useState(null); React.useEffect(() => { const a = new Audio(); a.preload = "metadata"; const fmt = (d) => { if (!isFinite(d) || d <= 0) return null; const m = Math.floor(d / 60); const s = Math.floor(d % 60).toString().padStart(2, "0"); return `${m}:${s}`; }; const onLoaded = () => { const f = fmt(a.duration); if (f) setDur(f); else { // Fallback for VBR mp3 with no header duration: seek to end to force scan try { a.currentTime = 1e101; a.addEventListener("timeupdate", function once() { a.removeEventListener("timeupdate", once); const f2 = fmt(a.duration); if (f2) setDur(f2); try { a.currentTime = 0; } catch (e) {} }); } catch (e) {} } }; const onDurChange = () => { const f = fmt(a.duration); if (f) setDur(f); }; a.addEventListener("loadedmetadata", onLoaded); a.addEventListener("durationchange", onDurChange); a.src = `assets/audio/track-${q.id}.mp3`; return () => { a.removeEventListener("loadedmetadata", onLoaded); a.removeEventListener("durationchange", onDurChange); a.src = ""; }; }, [q.id]); return ( ); } // ───────────────────────────── Turntable ───────────────────────────── function Turntable({ q, playing }) { const accent = q.side === "B" ? "sage" : ""; const sideLabel = `SIDE ${q.side} · 33⅓`; return (
); } function Tonearm({ playing }) { return (
); } // ───────────────────────────── Waveform ───────────────────────────── function Waveform({ active }) { const heights = [10, 20, 14, 28, 18, 32, 16, 24, 12, 30, 22, 18, 26, 14, 22, 16, 28, 18]; return (
{heights.map((h, i) => ( ))}
); } // ───────────────────────────── Question Card ───────────────────────────── function QuestionCard({ q, onAnswer, picked }) { return (
Track {q.n} · Side {q.side} {q.voiced} asks
"{q.q}"
{q.answers.map((a, i) => { let cls = "ans"; if (picked != null) { if (i === q.correct) cls += " correct"; else if (i === picked) cls += " wrong"; else cls += " dim"; } const key = ["A","B","C","D"][i]; return ( ); })}
); } // ───────────────────────────── Notes Pill (in topbar) ───────────────────────────── function NotesPill({ notes, bumped, onClick }) { const inner = ( <> {notes} note{notes === 1 ? "" : "s"} ); if (onClick) { return ( ); } return (
{inner}
); } // ───────────────────────────── Settings cog (in topbar) ───────────────────────────── function SettingsCog({ onClick }) { return ( ); } // ───────────────────────────── Topbar ───────────────────────────── function TopBar({ left, right }) { return (
{left}
{right}
); } function Brand({ side = "A", onClick }) { const label = `Side ${side} · For Demetria`; if (onClick) { return ( ); } return (
{label}
); } function BackBtn({ label = "Back to crate", onClick }) { return ( ); } // ───────────────────────────── Certificate (shop item) ───────────────────────────── function Certificate({ item, owned, affordable, onBuy }) { const cls = `cert ${owned ? "purchased" : ""} ${(!owned && !affordable) ? "locked" : ""}`; return ( ); } Object.assign(window, { Sleeve, Turntable, Tonearm, Waveform, QuestionCard, NotesPill, SettingsCog, TopBar, Brand, BackBtn, Certificate, });