22import { useQuery , useQueryClient } from "@tanstack/react-query" ;
33import { createFileRoute } from "@tanstack/react-router" ;
44import { AnimatePresence , motion } from "motion/react" ;
5- import { useEffect } from "react" ;
5+ import { useEffect , useMemo , useState } from "react" ;
66import { BlacklistPanel } from "~/components/blacklist-panel" ;
77import { PublicPlayedHistoryCard } from "~/components/public-played-history-card" ;
8+ import type {
9+ SearchSong ,
10+ SearchSongResultState ,
11+ } from "~/components/song-search-panel" ;
812import { SongSearchPanel } from "~/components/song-search-panel" ;
13+ import { Checkbox } from "~/components/ui/checkbox" ;
14+ import { Label } from "~/components/ui/label" ;
915import { formatSlugTitle , pageTitle } from "~/lib/page-title" ;
1016import { getPickNumbersForQueuedItems } from "~/lib/pick-order" ;
11- import { decodeHtmlEntities } from "~/lib/utils" ;
17+ import { cn , decodeHtmlEntities } from "~/lib/utils" ;
1218
1319type PublicPlaylistItem = {
1420 id : string ;
1521 songTitle : string ;
1622 songArtist ?: string | null ;
23+ songCreator ?: string | null ;
24+ songCatalogSourceId ?: number | null ;
1725 requestedByTwitchUserId ?: string | null ;
1826 requestedByLogin ?: string | null ;
1927 requestedByDisplayName ?: string | null ;
@@ -87,6 +95,7 @@ function toPlaylistItems(
8795function PublicChannelPage ( ) {
8896 const { slug } = Route . useParams ( ) ;
8997 const queryClient = useQueryClient ( ) ;
98+ const [ showBlacklisted , setShowBlacklisted ] = useState ( false ) ;
9099 const { data, isLoading } = useQuery ( {
91100 queryKey : [ "public-channel-page" , slug ] ,
92101 queryFn : async ( ) : Promise < PublicChannelPageData > => {
@@ -145,6 +154,75 @@ function PublicChannelPage() {
145154 } , [ queryClient , slug ] ) ;
146155
147156 const channelDisplayName = data ?. playlist ?. channel ?. displayName ?? slug ;
157+ const publicSearchResultFilter = useMemo (
158+ ( ) => ( song : SearchSong ) =>
159+ showBlacklisted ||
160+ getBlacklistReasons (
161+ {
162+ songCatalogSourceId : song . sourceId ?? null ,
163+ songArtist : song . artist ?? null ,
164+ songCreator : song . creator ?? null ,
165+ } ,
166+ {
167+ artists : data ?. playlist . blacklistArtists ?? [ ] ,
168+ charters : data ?. playlist . blacklistCharters ?? [ ] ,
169+ songs : data ?. playlist . blacklistSongs ?? [ ] ,
170+ }
171+ ) . length === 0 ,
172+ [
173+ data ?. playlist . blacklistArtists ,
174+ data ?. playlist . blacklistCharters ,
175+ data ?. playlist . blacklistSongs ,
176+ showBlacklisted ,
177+ ]
178+ ) ;
179+ const publicSearchResultState = useMemo (
180+ ( ) =>
181+ ( song : SearchSong ) : SearchSongResultState => {
182+ const reasons = getBlacklistReasons (
183+ {
184+ songCatalogSourceId : song . sourceId ?? null ,
185+ songArtist : song . artist ?? null ,
186+ songCreator : song . creator ?? null ,
187+ } ,
188+ {
189+ artists : data ?. playlist . blacklistArtists ?? [ ] ,
190+ charters : data ?. playlist . blacklistCharters ?? [ ] ,
191+ songs : data ?. playlist . blacklistSongs ?? [ ] ,
192+ }
193+ ) ;
194+
195+ return {
196+ disabled : reasons . length > 0 ,
197+ reasons,
198+ } ;
199+ } ,
200+ [
201+ data ?. playlist . blacklistArtists ,
202+ data ?. playlist . blacklistCharters ,
203+ data ?. playlist . blacklistSongs ,
204+ ]
205+ ) ;
206+ const playlistItems = data ?. playlist ?. items ?? [ ] ;
207+ const filteredItems = useMemo (
208+ ( ) =>
209+ playlistItems . filter ( ( item ) => {
210+ const blacklistReasons = getBlacklistReasons ( item , {
211+ artists : data ?. playlist . blacklistArtists ?? [ ] ,
212+ charters : data ?. playlist . blacklistCharters ?? [ ] ,
213+ songs : data ?. playlist . blacklistSongs ?? [ ] ,
214+ } ) ;
215+ return showBlacklisted || blacklistReasons . length === 0 ;
216+ } ) ,
217+ [
218+ data ?. playlist . blacklistArtists ,
219+ data ?. playlist . blacklistCharters ,
220+ data ?. playlist . blacklistSongs ,
221+ playlistItems ,
222+ showBlacklisted ,
223+ ]
224+ ) ;
225+ const hiddenBlacklistedCount = playlistItems . length - filteredItems . length ;
148226
149227 return (
150228 < section className = "grid gap-6" >
@@ -161,13 +239,23 @@ function PublicChannelPage() {
161239 { isLoading ? < p className = "mt-4 px-8" > Loading playlist...</ p > : null }
162240 < div className = "mt-6 grid gap-3 px-8" >
163241 < AnimatePresence initial = { false } mode = "popLayout" >
164- { data ?. playlist ?. items ?. map ( ( item ) => (
165- < PublicPlaylistRow key = { item . id } item = { item } />
242+ { filteredItems . map ( ( item ) => (
243+ < PublicPlaylistRow
244+ key = { item . id }
245+ item = { item }
246+ blacklistReasons = { getBlacklistReasons ( item , {
247+ artists : data ?. playlist . blacklistArtists ?? [ ] ,
248+ charters : data ?. playlist . blacklistCharters ?? [ ] ,
249+ songs : data ?. playlist . blacklistSongs ?? [ ] ,
250+ } ) }
251+ />
166252 ) ) }
167253 </ AnimatePresence >
168- { ! isLoading && ! data ?. playlist ?. items ? .length ? (
254+ { ! isLoading && ! filteredItems . length ? (
169255 < p className = "text-sm text-(--muted)" >
170- This playlist is empty right now.
256+ { playlistItems . length > 0 && ! showBlacklisted
257+ ? "Only blacklisted songs are in the queue right now."
258+ : "This playlist is empty right now." }
171259 </ p >
172260 ) : null }
173261 </ div >
@@ -177,21 +265,48 @@ function PublicChannelPage() {
177265 title = "Search to add a song"
178266 description = "Copy the request command and use it in Twitch chat."
179267 placeholder = { `Search songs for ${ channelDisplayName } ` }
268+ resultFilter = { publicSearchResultFilter }
269+ resultState = { publicSearchResultState }
270+ advancedFiltersContent = {
271+ < div className = "inline-flex flex-wrap items-center gap-3 rounded-full border border-(--border) bg-(--panel) px-4 py-2.5" >
272+ < Checkbox
273+ id = "show-blacklisted-public-playlist"
274+ checked = { showBlacklisted }
275+ onCheckedChange = { ( checked ) =>
276+ setShowBlacklisted ( checked === true )
277+ }
278+ />
279+ < Label
280+ htmlFor = "show-blacklisted-public-playlist"
281+ className = "cursor-pointer text-sm font-medium text-(--text)"
282+ >
283+ Show blacklisted songs
284+ </ Label >
285+ { ! showBlacklisted && hiddenBlacklistedCount > 0 ? (
286+ < span className = "text-xs text-(--muted)" >
287+ Hiding { hiddenBlacklistedCount }
288+ </ span >
289+ ) : null }
290+ </ div >
291+ }
180292 />
181293
182294 < BlacklistPanel
183295 artists = { data ?. playlist . blacklistArtists ?? [ ] }
184296 charters = { data ?. playlist . blacklistCharters ?? [ ] }
185297 songs = { data ?. playlist . blacklistSongs ?? [ ] }
186- description = "These exact artist IDs, charter IDs, and track IDs are blocked for requests in this channel."
298+ description = "These exact artist IDs and track IDs are blocked for requests in this channel."
187299 />
188300
189301 < PublicPlayedHistoryCard slug = { slug } />
190302 </ section >
191303 ) ;
192304}
193305
194- function PublicPlaylistRow ( props : { item : EnrichedPublicPlaylistItem } ) {
306+ function PublicPlaylistRow ( props : {
307+ item : EnrichedPublicPlaylistItem ;
308+ blacklistReasons : string [ ] ;
309+ } ) {
195310 const requesterName =
196311 props . item . requestedByDisplayName ??
197312 props . item . requestedByLogin ??
@@ -210,7 +325,12 @@ function PublicPlaylistRow(props: { item: EnrichedPublicPlaylistItem }) {
210325 animate = { { opacity : 1 , y : 0 , scale : 1 } }
211326 exit = { { opacity : 0 , y : - 12 , scale : 0.985 } }
212327 transition = { publicPlaylistItemTransition }
213- className = "rounded-[24px] border border-(--border) bg-(--panel-soft) px-5 py-4"
328+ className = { cn (
329+ "rounded-[24px] border bg-(--panel-soft) px-5 py-4" ,
330+ props . blacklistReasons . length > 0
331+ ? "border-amber-400/35 bg-amber-500/8"
332+ : "border-(--border)"
333+ ) }
214334 >
215335 < div className = "flex items-start gap-4" >
216336 < StatusColumn
@@ -228,6 +348,9 @@ function PublicPlaylistRow(props: { item: EnrichedPublicPlaylistItem }) {
228348 { props . item . pickNumber && props . item . pickNumber <= 3 ? (
229349 < PickBadge pickNumber = { props . item . pickNumber } />
230350 ) : null }
351+ { props . blacklistReasons . map ( ( reason ) => (
352+ < BlacklistReasonBadge key = { reason } reason = { reason } />
353+ ) ) }
231354 </ div >
232355 </ div >
233356 </ div >
@@ -308,3 +431,63 @@ function PickBadge(props: { pickNumber: number }) {
308431 </ span >
309432 ) ;
310433}
434+
435+ function BlacklistReasonBadge ( props : { reason : string } ) {
436+ return (
437+ < span className = "inline-flex items-center rounded-full border border-amber-400/35 bg-amber-500/12 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-amber-100" >
438+ { props . reason }
439+ </ span >
440+ ) ;
441+ }
442+
443+ function normalizeBlacklistValue ( value ?: string | null ) {
444+ return value ?. trim ( ) . toLowerCase ( ) ?? "" ;
445+ }
446+
447+ function getBlacklistReasons (
448+ item : {
449+ songCatalogSourceId ?: number | null ;
450+ songArtist ?: string | null ;
451+ songCreator ?: string | null ;
452+ } ,
453+ blacklist : {
454+ artists : Array < { artistId : number ; artistName : string } > ;
455+ charters : Array < { charterId : number ; charterName : string } > ;
456+ songs : Array < {
457+ songId : number ;
458+ songTitle : string ;
459+ artistName ?: string | null ;
460+ } > ;
461+ }
462+ ) {
463+ const reasons : string [ ] = [ ] ;
464+ const artistName = normalizeBlacklistValue ( item . songArtist ) ;
465+ const creatorName = normalizeBlacklistValue ( item . songCreator ) ;
466+
467+ if (
468+ item . songCatalogSourceId != null &&
469+ blacklist . songs . some ( ( song ) => song . songId === item . songCatalogSourceId )
470+ ) {
471+ reasons . push ( "Song blacklisted" ) ;
472+ }
473+
474+ if (
475+ artistName &&
476+ blacklist . artists . some (
477+ ( artist ) => normalizeBlacklistValue ( artist . artistName ) === artistName
478+ )
479+ ) {
480+ reasons . push ( "Artist blacklisted" ) ;
481+ }
482+
483+ if (
484+ creatorName &&
485+ blacklist . charters . some (
486+ ( charter ) => normalizeBlacklistValue ( charter . charterName ) === creatorName
487+ )
488+ ) {
489+ reasons . push ( "Creator blacklisted" ) ;
490+ }
491+
492+ return reasons ;
493+ }
0 commit comments