import { useMemo } from 'react' import { useTmdbPerson } from '../../hooks/use-tmdb' import { mapTmdbToJf } from '../../lib/tmdb-mapping' import ContentRow from '../ui/ContentRow' import type { TmdbCombinedCreditCrew, TmdbCombinedCreditCast } from '../../api/tmdb' type Role = 'director' | 'writer' | 'composer' | 'actor' interface Props { personId: number personName: string role: Role /** TMDB id of the current item so it gets filtered out of the row. */ excludeTmdbId?: number | string | null /** Filter the credit list to the same media kind as the current page. */ kind: 'movie' | 'tv' libraryMap?: Map | undefined } const ROLE_LABELS: Record = { director: 'Director', writer: 'Writer', composer: 'Composer', actor: 'Top-billed cast', } /** * Row of other titles by a named person (director, writer, composer, or * actor). Hides itself when there's too little material to be useful - * one or two credits feels like a leak, four-plus reads as intentional. * * Filters to the same media kind (movie or tv) as the current detail * page so a series page doesn't get drowned in director's movie work. */ export default function FromSameRow({ personId, personName, role, excludeTmdbId, kind, libraryMap, }: Props) { const personQuery = useTmdbPerson(personId) const items = useMemo(() => { const credits = personQuery.data?.combined_credits if (!credits) return [] const excludeStr = excludeTmdbId != null ? String(excludeTmdbId) : null const source = role === 'actor' ? credits.cast : credits.crew const matched = (source as Array).filter(c => { if (c.media_type !== kind) return false if (excludeStr && String(c.id) === excludeStr) return false if (!c.poster_path) return false if (role === 'actor') return true const crew = c as TmdbCombinedCreditCrew if (role === 'director') return crew.job === 'Director' if (role === 'writer') return crew.department === 'Writing' if (role === 'composer') { return ( crew.job === 'Original Music Composer' || crew.job === 'Music' || crew.job === 'Composer' ) } return false }) // De-dup by tmdb id (writers especially get credited multiple times // on the same title under different jobs). const seen = new Set() const deduped = matched.filter(c => { const k = String(c.id) if (seen.has(k)) return false seen.add(k) return true }) return deduped .sort((a, b) => (b.vote_average || 0) * (b.popularity || 0) - (a.vote_average || 0) * (a.popularity || 0)) .slice(0, 20) }, [personQuery.data, role, excludeTmdbId, kind]) // Skip rows that would look anemic - fewer than 3 entries doesn't // earn its keep on the page. if (items.length < 3) return null const mapped = mapTmdbToJf( items.map(it => ({ ...it, adult: false, // mapTmdbToJf reads media_type to pick kind, which is already set. })), libraryMap, ) const title = role === 'actor' ? `More with ${personName}` : `From ${personName}` const subtitle = `${ROLE_LABELS[role]} on this title - other ${kind === 'movie' ? 'films' : 'shows'} they've worked on` return ( ) }