Files
jellybloom/src/components/discover/CanonicalLists.tsx
T
2026-04-26 07:48:34 +03:00

141 lines
4.4 KiB
TypeScript

import { useMemo } from 'react'
import ContentRow from '../ui/ContentRow'
import { useTmdbDiscoverMovies } from '../../hooks/use-tmdb'
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
import { usePreferencesStore } from '../../stores/preferences-store'
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
import { filterToMissing } from '../../pages/discover/helpers'
import type { TmdbMovie } from '../../api/tmdb'
interface CanonicalList {
id: string
title: string
subtitle: string
params: Record<string, string>
/** Optional client-side filter applied on top of TMDB results. */
extra?: (m: TmdbMovie) => boolean
}
/**
* Hand-curated approximations of the canonical "you should have seen this"
* lists - AFI / Sight & Sound / Oscar winners - built from TMDB discover
* parameters rather than the volatile user-created /list endpoint.
*
* Each entry is one ContentRow. The rows self-hide via the existing
* filterToMissing pipeline when the user already owns everything in them.
*/
const LISTS: CanonicalList[] = [
{
id: 'best-picture-winners',
title: 'Best Picture winners',
subtitle: 'Academy Award winners across the decades',
params: {
// TMDB keyword 210024 = "academy award - best picture winner"
with_keywords: '210024',
sort_by: 'vote_average.desc',
'vote_count.gte': '500',
},
},
{
id: 'top-250',
title: 'The canonical 250',
subtitle: 'Films that have settled into the canon - massive vote count, top scores',
params: {
'vote_count.gte': '10000',
'vote_average.gte': '8',
sort_by: 'vote_average.desc',
},
},
{
id: 'highest-grossing',
title: 'Highest grossing of all time',
subtitle: 'The films that made everyone show up',
params: {
sort_by: 'revenue.desc',
'vote_count.gte': '500',
},
},
{
id: 'modern-classics',
title: 'Modern classics',
subtitle: 'Post-2000 films that already feel essential',
params: {
'primary_release_date.gte': '2000-01-01',
'vote_count.gte': '3000',
'vote_average.gte': '8',
sort_by: 'vote_average.desc',
},
},
{
id: 'foreign-canon',
title: 'Foreign cinema canon',
subtitle: 'Non-English films with critical pedigree',
params: {
'vote_count.gte': '1500',
'vote_average.gte': '7.8',
sort_by: 'vote_average.desc',
},
extra: m => !!m.original_language && m.original_language !== 'en',
},
{
id: 'animation-canon',
title: 'Animation canon',
subtitle: 'Highest-rated animated features across studios',
params: {
with_genres: '16',
'vote_count.gte': '2000',
'vote_average.gte': '7.5',
sort_by: 'vote_average.desc',
},
},
]
/**
* Section wrapper for the canonical-lists block. Only renders on the
* movies tab because the TMDB keywords + revenue sorts only make sense
* for films - TV equivalents would need different queries.
*/
export default function CanonicalLists() {
return (
<section className="pt-2 pb-2">
<div className="px-7 mb-5">
<div className="flex items-center gap-2 mb-1">
<span className="w-1 h-3.5 rounded-full bg-accent" />
<span className="text-[10.5px] font-semibold text-text-3 uppercase tracking-[0.18em]">
Canonical lists
</span>
</div>
<h2 className="font-display text-[20px] font-bold tracking-tight text-text-1 leading-tight">
The books and ballots
</h2>
<p className="text-[12px] text-text-3 mt-0.5">
Films that show up on every "best of" list - filtered to ones you don't own yet.
</p>
</div>
{LISTS.map(list => (
<CanonicalRow key={list.id} list={list} />
))}
</section>
)
}
function CanonicalRow({ list }: { list: CanonicalList }) {
const movies = useTmdbDiscoverMovies(list.params)
const lib = useLibraryByTmdbId()
const hideAdult = usePreferencesStore(s => s.hideAdult)
const items = useMemo(() => {
let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' as const }))
if (list.extra) raw = raw.filter(list.extra)
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.adult), lib.data)
}, [movies.data, lib.data, hideAdult, list])
if (items.length === 0) return null
return (
<ContentRow
title={list.title}
subtitle={list.subtitle}
items={items}
layoutKey={`canonical_${list.id}`}
/>
)
}