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
| 12 | 12 | import { useEffect, useRef } from "react"; | |
| 13 | + | import Sortable from "sortablejs"; | |
| 13 | 14 | import type { SpinSlot } from "@/lib/listening-bar/types"; | |
| 14 | 15 | ||
| 15 | 16 | type Props = { slots: SpinSlot[]; onReorder: (ids: string[]) => void }; | |
| 16 | 17 | ||
| 17 | 18 | export function SpinQueue({ slots, onReorder }: Props) { | |
| 19 | + | const listRef = useRef<HTMLUListElement>(null); | |
| 18 | 20 | ||
| 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]); | |
| 19 | 34 | ||
| 20 | 35 | return ( | |
| 21 | - | <ul className="spin-queue"> | |
| 36 | + | <ul ref={listRef} className="spin-queue" aria-label="Tonight's demo spins"> | |
| 22 | 37 | {slots.map((slot) => ( | |
| 23 | 38 | <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 | + | /> | |
| 25 | 45 | <span className="minutes">{slot.runtimeMin}m</span> | |
| 26 | 46 | </li> | |
| 27 | 47 | ))} | |
| 28 | 48 | </ul> | |
| 29 | 49 | ); | |
| 30 | 50 | } |