109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
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<string, { id: string; name: string; type: string }> | undefined
|
|
}
|
|
|
|
const ROLE_LABELS: Record<Role, string> = {
|
|
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<TmdbCombinedCreditCast | TmdbCombinedCreditCrew>).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<string>()
|
|
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 (
|
|
<ContentRow
|
|
title={title}
|
|
subtitle={subtitle}
|
|
items={mapped}
|
|
layoutKey={`from_same_${role}_${personId}`}
|
|
/>
|
|
)
|
|
}
|