Review

Listening-bar spin queue with rich catalog titles

Needlebar is the house app for The Wax & Wick listening bar. Tonight's demo-spin queue is now drag-reorderable with SortableJS so hosts can reshuffle without a full page reload. Catalog already ships titleHtml (em for album sides, small for matrix numbers); this PR mounts those rich titles into each sortable row instead of plain text.

ReactTier 2securityxsssortablejsinnerHTML

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

components/listening-bar/SpinQueue.tsx+22-2
1212import { useEffect, useRef } from "react";
13+import Sortable from "sortablejs";
1314import type { SpinSlot } from "@/lib/listening-bar/types";
1415
1516type Props = { slots: SpinSlot[]; onReorder: (ids: string[]) => void };
1617
1718export function SpinQueue({ slots, onReorder }: Props) {
19+ const listRef = useRef<HTMLUListElement>(null);
1820
21+ useEffect(() => {
22+ if (!listRef.current) return;
23+ const sortable = Sortable.create(listRef.current, {
24+ animation: 150,
25+ handle: ".drag-handle",
26+ onEnd: () => {
27+ const ids = [...listRef.current!.querySelectorAll<HTMLElement>("[data-slot-id]")]
28+ .map((el) => el.dataset.slotId!);
29+ onReorder(ids);
30+ },
31+ });
32+ return () => sortable.destroy();
33+ }, [onReorder]);
1934
2035 return (
21- <ul className="spin-queue">
36+ <ul ref={listRef} className="spin-queue" aria-label="Tonight's demo spins">
2237 {slots.map((slot) => (
2338 <li key={slot.id} data-slot-id={slot.id} className="spin-row">
39+ <span className="drag-handle" aria-hidden="true" />
24- <span className="title">{slot.title}</span>
40+ {/* catalog titleHtml: <em>Side A</em>, matrix in <small> */}
41+ <span
42+ className="title"
43+ dangerouslySetInnerHTML={{ __html: slot.titleHtml }}
44+ />
2545 <span className="minutes">{slot.runtimeMin}m</span>
2646 </li>
2747 ))}
2848 </ul>
2949 );
3050}