1// Pitches (song ideas for Jev songs, worker/src/content.ts) and the songs 2// that answer them. The maker of a song says which pitch it answers: a 3// site song in its SPEC.md (`pitch: <id>`, read at build time by 4// songs.mjs), a listener's song by its author (`setSongPitch`, in 5// listeners.mjs). A pitch's author cannot mark it answered: only the song's 6// maker can vouch for that. 7 8const PITCH_ID = /^[A-Za-z0-9_-]{22}$/; 9 10// A spec's `pitch:`, a pitch's id, or null; a malformed one fails the build 11// (songs.mjs) rather than silently never matching. 12export function pitchOf(value, where = 'a song') { 13 if (value === undefined || value === null) return null; 14 if (typeof value === 'string' && PITCH_ID.test(value)) return value; 15 throw new Error(`${where}: pitch must be a pitch's id (22 characters), not ${JSON.stringify(value)}`); 16} 17 18// Every song answering `pitch`: the site's (from this build) and the 19// listeners' the Worker listed with it, site songs first. 20export function answersOf(pitch, siteSongs) { 21 return [ 22 ...siteSongs.filter((s) => s.pitch === pitch.id).map((s) => ({ site: s.id, title: s.title })), 23 ...(pitch.answers ?? []).map((a) => ({ listener: a.id, title: a.title })), 24 ]; 25} 26 27// How the pitches list may be sorted, and what each is called. 28export const PITCH_SORTS = { votes: 'most voted', new: 'newest' };