import { useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { motion } from 'framer-motion' import { useTmdbPerson } from '../../hooks/use-tmdb' import { useLibraryByTmdbId } from '../../hooks/use-jellyfin' import { mapTmdbToJf } from '../../lib/tmdb-mapping' import { getTmdbImageUrl } from '../../api/tmdb' import ContentRow from './ContentRow' interface Props { /** TMDB person id. */ personId: number /** Display name; pre-known from the spotlight aggregation. */ name: string /** "Director" or "Actor" - drives the credit filter. */ role: 'director' | 'actor' profilePath?: string | null /** How many of this person's titles the user has already watched. */ watchedCount: number } const COMMERCIAL_DEPARTMENTS = ['Acting', 'Directing'] /** * Renders a personal-pick row sourced from a TMDB person's filmography. * * Strategy: * - Director: pull from `combined_credits.crew` filtered to job="Director", * sorted by release date descending. * - Actor: pull from `combined_credits.cast`, dropping low-billed * appearances (one-line cameos), sorted by popularity then date. * * In-library matches show the accent ring naturally via PosterCard's * `_inLibrary` honoring; missing items render in the row beside them. */ export default function PersonSpotlightRow({ personId, name, role, profilePath, watchedCount }: Props) { const person = useTmdbPerson(personId) const libraryByTmdbId = useLibraryByTmdbId() const items = useMemo(() => { const credits = person.data?.combined_credits if (!credits) return [] const list = role === 'director' ? (credits.crew || []).filter(c => c.job === 'Director') : (credits.cast || []) // Drop trivia / archival appearances and ultra-deep cuts. .filter(c => COMMERCIAL_DEPARTMENTS.includes((c as { department?: string }).department || 'Acting')) // De-dupe by id (a director may have multiple crew credits on one film // when they're also writer; a cast member may appear twice for episodic // tv guest spots). const seen = new Set() const unique = list.filter(c => { if (seen.has(c.id)) return false seen.add(c.id) return true }) // Sort: newest first, but with low-popularity entries pushed to the end. const sorted = [...unique].sort((a, b) => { const ap = a.popularity ?? 0 const bp = b.popularity ?? 0 // Bucket popularity so date dominates within "famous-enough" tiers. const ab = ap >= 5 ? 0 : 1 const bb = bp >= 5 ? 0 : 1 if (ab !== bb) return ab - bb const ad = (a.release_date || a.first_air_date || '').slice(0, 10) const bd = (b.release_date || b.first_air_date || '').slice(0, 10) return bd.localeCompare(ad) }) return mapTmdbToJf(sorted.slice(0, 24), libraryByTmdbId.data) }, [person.data, libraryByTmdbId.data, role]) if (items.length === 0) return null const totalCount = items.length const remaining = Math.max(0, totalCount - watchedCount) return (
{profilePath && ( )}

{role === 'director' ? 'Director spotlight' : 'Following the actor'}

{watchedCount} watched {' ยท '} {remaining} remaining

) } function PersonName({ personId, name }: { personId: number; name: string }) { const navigate = useNavigate() return ( ) }