discover components
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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'
|
||||
|
||||
interface CanonicalList {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
params: Record<string, string>
|
||||
/** Optional client-side filter applied on top of TMDB results. */
|
||||
extra?: (m: { original_language?: string; vote_count?: number }) => 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' }))
|
||||
if (list.extra) raw = raw.filter(list.extra as any)
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).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}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user