// 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 (
);
}
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,
});