"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { previewWindow, type PhraseHit } from "@/lib/phrase-types"; import { SHORTLIST_EVENT, type Pick, type Shortlist } from "@/lib/shortlist-types"; // The rows, and ONE island for all of them. // // The obvious shape is a component per row. It is also the doubling // ProvenancePanel warns about: 50 rows of HTML plus 50 sets of serialised // island props is the page sent twice, and every row carries a title, two // context lines and an archive URL. So this takes the array once. // // It FETCHES NOTHING for the list. Ordering, filtering, paging and totals are // server decisions, arrived at over a URL that is shareable and back-button // correct. The only network this component does is the audio and picture for // the one hit being auditioned. const fmtTime = (t: number) => { const s = Math.floor(t); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; }; export default function PhraseConsole({ hits, q, list, starred, }: { hits: PhraseHit[]; /** The query that found these, snapshotted into every pick. */ q: string; /** The active shortlist's slug, or null -- `s` needs somewhere to put it. */ list: string | null; /** Which of these hits are already on that list, decided by the server. */ starred: string[]; }) { const [cursor, setCursor] = useState(0); const [stars, setStars] = useState>(() => new Set(starred)); const [starring, setStarring] = useState(false); const [playing, setPlaying] = useState(null); const [playhead, setPlayhead] = useState(null); // The id whose picture is mounted. Never set on load -- see below. const [picture, setPicture] = useState(null); const [error, setError] = useState(null); const audio = useRef(null); const objectUrl = useRef(null); const origin = useRef(0); const raf = useRef(null); const rows = useRef<(HTMLLIElement | null)[]>([]); const stop = useCallback(() => { audio.current?.pause(); audio.current = null; if (objectUrl.current) URL.revokeObjectURL(objectUrl.current); objectUrl.current = null; if (raf.current !== null) cancelAnimationFrame(raf.current); raf.current = null; setPlaying(null); setPlayhead(null); }, []); // THE PREVIEW, and it reuses the clip routes verbatim rather than growing a // media route of its own. // // const key = `${h.video}@${h.start.toFixed(2)}` // // That key is a media ADDRESS, not the hit's identity -- the identity is // `