discover helpers
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { Compass } from '../../lib/icons'
|
||||
|
||||
export function NoKey() {
|
||||
return (
|
||||
<div className="px-7 py-20 max-w-xl mx-auto text-center">
|
||||
<div className="relative w-14 h-14 mx-auto mb-5">
|
||||
<div className="absolute inset-0 rounded-full bg-accent/10 blur-xl" />
|
||||
<div className="relative w-full h-full rounded-full bg-gradient-to-br from-elevated to-surface ring-1 ring-border grid place-items-center">
|
||||
<Compass size={20} className="text-text-3" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="font-display text-[22px] font-bold text-text-1 tracking-tight mb-2">
|
||||
Discover needs a TMDB key
|
||||
</h1>
|
||||
<p className="text-[13px] text-text-3 leading-relaxed mb-5">
|
||||
This page surfaces films and shows you don't already own. It pulls trending, top-rated, upcoming, by-genre, by-language, by-studio, and by-network rows from TMDB - all of which require an API key to load.
|
||||
</p>
|
||||
<a
|
||||
href="/settings#tmdb"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-full bg-accent text-void font-semibold text-[12.5px] hover:bg-accent-hover transition focus-ring"
|
||||
>
|
||||
Open TMDB settings
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<div className="px-7 pt-7 pb-5">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="w-1 h-3 rounded-full bg-accent" />
|
||||
<span className="text-[10px] font-semibold text-text-3 uppercase tracking-[0.2em]">
|
||||
Discover
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="font-display text-[26px] md:text-[30px] font-bold tracking-tight text-text-1 leading-[1.1] mb-1">
|
||||
Find your next favorite
|
||||
</h1>
|
||||
<p className="text-[12.5px] text-text-3 max-w-xl">
|
||||
Films and shows you don't have yet, sourced from TMDB.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SectionHeader({ eyebrow, title, subtitle }: { eyebrow: string; title: string; subtitle: string }) {
|
||||
return (
|
||||
<div className="px-7 mb-5 pt-6">
|
||||
<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]">{eyebrow}</span>
|
||||
</div>
|
||||
<h2 className="font-display text-[20px] font-bold tracking-tight text-text-1 leading-tight">{title}</h2>
|
||||
<p className="text-[12px] text-text-3 mt-0.5">{subtitle}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTmdbDiscoverMovies, useTmdbDiscoverTv } from '../../hooks/use-tmdb'
|
||||
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
||||
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
||||
import PosterCard from '../../components/ui/PosterCard'
|
||||
import { usePosterGridClasses } from '../../lib/density'
|
||||
import { filtersToTmdbParams, type DiscoverFilterState } from '../../components/discover/DiscoverFilters'
|
||||
import type { BaseItemDto } from '../../api/types'
|
||||
|
||||
/**
|
||||
* Shown when any filter is active or sort has been changed from the default.
|
||||
* Renders a single grid of TMDB results constrained by the filter selections,
|
||||
* with optional library-hide.
|
||||
*/
|
||||
export function FilteredGrid({ filters }: { filters: DiscoverFilterState }) {
|
||||
const navigate = useNavigate()
|
||||
const lib = useLibraryByTmdbId()
|
||||
const grid = usePosterGridClasses()
|
||||
const params = useMemo(() => filtersToTmdbParams(filters), [filters])
|
||||
const moviesQ = useTmdbDiscoverMovies(filters.kind === 'movie' ? params : ({} as Record<string, string>))
|
||||
const tvQ = useTmdbDiscoverTv(filters.kind === 'tv' ? params : ({} as Record<string, string>))
|
||||
const data = filters.kind === 'movie' ? moviesQ.data : tvQ.data
|
||||
const isLoading = filters.kind === 'movie' ? moviesQ.isLoading : tvQ.isLoading
|
||||
|
||||
const items = useMemo(() => {
|
||||
const raw = (data?.results || []).map(m => ({
|
||||
...m,
|
||||
media_type: filters.kind === 'movie' ? 'movie' : 'tv',
|
||||
}))
|
||||
let list = raw
|
||||
if (filters.hideOwned && lib.data) {
|
||||
list = list.filter(m => !lib.data!.has(String(m.id)))
|
||||
}
|
||||
return mapTmdbToJf(list, lib.data) as BaseItemDto[]
|
||||
}, [data, filters.kind, filters.hideOwned, lib.data])
|
||||
|
||||
return (
|
||||
<div className="px-7 pt-6">
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<p className="text-[12px] text-text-3 tracking-tight">
|
||||
{isLoading ? (
|
||||
'Searching...'
|
||||
) : (
|
||||
<>
|
||||
<span className="tabular-nums text-text-1 font-semibold">{items.length}</span>
|
||||
<span className="text-text-3"> matching {filters.kind === 'movie' ? 'movies' : 'shows'}</span>
|
||||
{filters.hideOwned && (
|
||||
<span className="text-text-4"> (excluding your library)</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={grid}>
|
||||
{Array.from({ length: 12 }).map((_, i) => (
|
||||
<div key={i}>
|
||||
<div className="skeleton aspect-[2/3] rounded-lg" />
|
||||
<div className="mt-2.5 space-y-1.5">
|
||||
<div className="skeleton h-3 w-3/4 rounded" />
|
||||
<div className="skeleton h-2.5 w-1/2 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="py-20 text-center max-w-md mx-auto">
|
||||
<p className="font-display text-[18px] text-text-1 font-semibold tracking-tight mb-2">
|
||||
No matches
|
||||
</p>
|
||||
<p className="text-[13px] text-text-3 leading-relaxed">
|
||||
Loosen the filters to see more results. Reduce the minimum rating, expand the year range, or clear genre selections.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={grid}>
|
||||
{items.map((item, i) => (
|
||||
<PosterCard
|
||||
key={item.Id || i}
|
||||
item={item}
|
||||
priority={i < 6}
|
||||
onClick={() => item.Id && navigate(`/item/${item.Id}`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Filter raw TMDB results down to ones the user does NOT own. The library
|
||||
* map keys items by their TMDB id; we drop anything that hits. When no
|
||||
* library map has loaded yet, returns an empty array so rows hide rather
|
||||
* than briefly showing items the user already owns.
|
||||
*/
|
||||
export function filterToMissing<T extends { id: number; original_language?: string }>(
|
||||
raw: T[],
|
||||
lib?: Map<string, unknown> | null,
|
||||
hideAdult = false,
|
||||
adultFlag?: (m: T) => boolean,
|
||||
): T[] {
|
||||
if (!lib) return []
|
||||
let list = raw.filter(m => !lib.has(String(m.id)))
|
||||
if (hideAdult && adultFlag) list = list.filter(m => !adultFlag(m))
|
||||
return list
|
||||
}
|
||||
|
||||
export const GENRE_ROWS: Array<{ label: string; subtitle: string }> = [
|
||||
{ label: 'Action', subtitle: 'Bullets, fists, explosions' },
|
||||
{ label: 'Drama', subtitle: 'Stories that hit deep' },
|
||||
{ label: 'Comedy', subtitle: 'Light watching' },
|
||||
{ label: 'Science Fiction', subtitle: 'Beyond the possible' },
|
||||
{ label: 'Thriller', subtitle: 'Edge-of-your-seat tension' },
|
||||
{ label: 'Horror', subtitle: 'Lights off' },
|
||||
{ label: 'Animation', subtitle: 'Drawn to perfection' },
|
||||
{ label: 'Romance', subtitle: 'Love and longing' },
|
||||
{ label: 'Fantasy', subtitle: 'Worlds of magic' },
|
||||
{ label: 'Mystery', subtitle: 'Puzzles to solve' },
|
||||
]
|
||||
|
||||
export const LANGUAGE_ROWS: Array<{ code: string; label: string; subtitle: string }> = [
|
||||
{ code: 'ja', label: 'Japanese cinema', subtitle: 'From Kurosawa to Kore-eda' },
|
||||
{ code: 'ko', label: 'Korean cinema', subtitle: 'New wave thrillers and drama' },
|
||||
{ code: 'fr', label: 'French cinema', subtitle: 'Auteurs and arthouse' },
|
||||
{ code: 'es', label: 'Spanish-language cinema', subtitle: 'Spain and Latin America' },
|
||||
{ code: 'de', label: 'German cinema', subtitle: 'Berlin school and beyond' },
|
||||
{ code: 'it', label: 'Italian cinema', subtitle: 'Neo-realism to giallo' },
|
||||
{ code: 'zh', label: 'Chinese-language cinema', subtitle: 'Mainland, Hong Kong, Taiwan' },
|
||||
{ code: 'hi', label: 'Hindi cinema', subtitle: 'Bollywood at its best' },
|
||||
]
|
||||
@@ -0,0 +1,337 @@
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
useTmdbTrending,
|
||||
useTmdbDiscoverMovies,
|
||||
useTmdbDiscoverTv,
|
||||
useTmdbUpcoming,
|
||||
useTmdbTopRatedMovies,
|
||||
useTmdbTopRatedTv,
|
||||
} from '../../hooks/use-tmdb'
|
||||
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
||||
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
||||
import { tmdbMovieGenreId, tmdbTvGenreId } from '../../lib/tmdb-genres'
|
||||
import ContentRow from '../../components/ui/ContentRow'
|
||||
import { usePreferencesStore } from '../../stores/preferences-store'
|
||||
import { filterToMissing } from './helpers'
|
||||
|
||||
export function TrendingDayRow() {
|
||||
const trending = useTmdbTrending('all', 'day')
|
||||
const lib = useLibraryByTmdbId()
|
||||
const hideAdult = usePreferencesStore(s => s.hideAdult)
|
||||
const items = useMemo(() => {
|
||||
const raw = (trending.data?.results || [])
|
||||
const filtered = filterToMissing(raw, lib.data, hideAdult, m => !!m.adult)
|
||||
return mapTmdbToJf(filtered, lib.data)
|
||||
}, [trending.data, lib.data, hideAdult])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Trending today"
|
||||
subtitle="Buzz on TMDB right now - things you don't have"
|
||||
items={items}
|
||||
layoutKey="discover_trending_day"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function TrendingWeekRow() {
|
||||
const trending = useTmdbTrending('all', 'week')
|
||||
const lib = useLibraryByTmdbId()
|
||||
const hideAdult = usePreferencesStore(s => s.hideAdult)
|
||||
const items = useMemo(() => {
|
||||
const raw = (trending.data?.results || [])
|
||||
const filtered = filterToMissing(raw, lib.data, hideAdult, m => !!m.adult)
|
||||
return mapTmdbToJf(filtered, lib.data)
|
||||
}, [trending.data, lib.data, hideAdult])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Trending this week"
|
||||
subtitle="The week's biggest titles you're missing"
|
||||
items={items}
|
||||
layoutKey="discover_trending_week"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function PopularMoviesRow() {
|
||||
const movies = useTmdbDiscoverMovies({ sort_by: 'popularity.desc' })
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Popular movies"
|
||||
subtitle="Top of TMDB right now - missing from your library"
|
||||
items={items}
|
||||
layoutKey="discover_popular_movies"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function PopularTvRow() {
|
||||
const tv = useTmdbDiscoverTv({ sort_by: 'popularity.desc' })
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (tv.data?.results || []).map(m => ({ ...m, media_type: 'tv' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [tv.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Popular shows"
|
||||
subtitle="Top of TMDB right now - missing from your library"
|
||||
items={items}
|
||||
layoutKey="discover_popular_tv"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function UpcomingMoviesRow({ region }: { region: string }) {
|
||||
const upcoming = useTmdbUpcoming(region)
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (upcoming.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [upcoming.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Coming soon"
|
||||
subtitle={`New theatrical and streaming releases${region ? ` in ${region}` : ''}`}
|
||||
items={items}
|
||||
layoutKey={`discover_upcoming_${region}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function TopRatedMoviesRow() {
|
||||
const movies = useTmdbTopRatedMovies()
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Critically acclaimed movies"
|
||||
subtitle="The highest-rated films on TMDB you don't yet own"
|
||||
items={items}
|
||||
layoutKey="discover_top_rated_movies"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function TopRatedTvRow() {
|
||||
const tv = useTmdbTopRatedTv()
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (tv.data?.results || []).map(m => ({ ...m, media_type: 'tv' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [tv.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Critically acclaimed shows"
|
||||
subtitle="The highest-rated series on TMDB you don't yet own"
|
||||
items={items}
|
||||
layoutKey="discover_top_rated_tv"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CultClassicsRow() {
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
'vote_count.gte': '10000',
|
||||
'vote_average.gte': '8',
|
||||
'popularity.lte': '20',
|
||||
sort_by: 'vote_average.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Cult classics"
|
||||
subtitle="Highly rated films flying under the radar"
|
||||
items={items}
|
||||
layoutKey="discover_cult"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function HighlyRatedThisYearRow() {
|
||||
const year = new Date().getFullYear()
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
primary_release_year: String(year),
|
||||
'vote_count.gte': '200',
|
||||
sort_by: 'vote_average.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={`Highly rated this year (${year})`}
|
||||
subtitle="The best-rated theatrical and streaming releases of the year"
|
||||
items={items}
|
||||
layoutKey={`discover_year_${year}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ForeignFilmsRow() {
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
'vote_count.gte': '500',
|
||||
sort_by: 'vote_average.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || [])
|
||||
.filter(m => m.original_language && m.original_language !== 'en')
|
||||
.slice(0, 20)
|
||||
.map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Foreign cinema"
|
||||
subtitle="Top-rated non-English films you don't own"
|
||||
items={items}
|
||||
layoutKey="discover_foreign"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentariesRow() {
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
with_genres: '99',
|
||||
'vote_count.gte': '300',
|
||||
sort_by: 'vote_average.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title="Documentaries"
|
||||
subtitle="The best-rated docs on TMDB you don't have"
|
||||
items={items}
|
||||
layoutKey="discover_docs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function GenreRow({ genre }: { genre: { label: string; subtitle: string } }) {
|
||||
const movieGenreId = tmdbMovieGenreId(genre.label)
|
||||
const tvGenreId = tmdbTvGenreId(genre.label)
|
||||
const movies = useTmdbDiscoverMovies(
|
||||
movieGenreId
|
||||
? { with_genres: String(movieGenreId), 'vote_count.gte': '500', sort_by: 'vote_average.desc' }
|
||||
: ({} as Record<string, string>),
|
||||
)
|
||||
const tv = useTmdbDiscoverTv(
|
||||
tvGenreId
|
||||
? { with_genres: String(tvGenreId), 'vote_count.gte': '200', sort_by: 'vote_average.desc' }
|
||||
: ({} as Record<string, string>),
|
||||
)
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const m = (movies.data?.results || []).map(x => ({ ...x, media_type: 'movie' }))
|
||||
const t = (tv.data?.results || []).map(x => ({ ...x, media_type: 'tv' }))
|
||||
const out: { id: number; media_type: string }[] = []
|
||||
const max = Math.max(m.length, t.length)
|
||||
for (let i = 0; i < max && out.length < 20; i++) {
|
||||
if (m[i]) out.push(m[i])
|
||||
if (t[i]) out.push(t[i])
|
||||
}
|
||||
return mapTmdbToJf(filterToMissing(out, lib.data), lib.data)
|
||||
}, [movies.data, tv.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={genre.label}
|
||||
subtitle={genre.subtitle}
|
||||
items={items}
|
||||
layoutKey={`discover_genre_${genre.label}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function LanguageRow({ lang }: { lang: { code: string; label: string; subtitle: string } }) {
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
with_original_language: lang.code,
|
||||
'vote_count.gte': '200',
|
||||
sort_by: 'vote_average.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={lang.label}
|
||||
subtitle={lang.subtitle}
|
||||
items={items}
|
||||
layoutKey={`discover_lang_${lang.code}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function StudioRow({ brandId, label, subtitle }: { brandId: number; label: string; subtitle: string }) {
|
||||
const movies = useTmdbDiscoverMovies({
|
||||
with_companies: String(brandId),
|
||||
sort_by: 'popularity.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (movies.data?.results || []).slice(0, 18).map(m => ({ ...m, media_type: 'movie' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [movies.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={label}
|
||||
subtitle={subtitle}
|
||||
items={items}
|
||||
layoutKey={`discover_studio_${brandId}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function NetworkRow({ brandId, label, subtitle }: { brandId: number; label: string; subtitle: string }) {
|
||||
const tv = useTmdbDiscoverTv({
|
||||
with_networks: String(brandId),
|
||||
sort_by: 'popularity.desc',
|
||||
})
|
||||
const lib = useLibraryByTmdbId()
|
||||
const items = useMemo(() => {
|
||||
const raw = (tv.data?.results || []).slice(0, 18).map(m => ({ ...m, media_type: 'tv' }))
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data), lib.data)
|
||||
}, [tv.data, lib.data])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={label}
|
||||
subtitle={subtitle}
|
||||
items={items}
|
||||
layoutKey={`discover_network_${brandId}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user