193 lines
6.2 KiB
TypeScript
193 lines
6.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { useHasTmdbKey } from '../hooks/use-external'
|
|
import LazyMount from '../components/ui/LazyMount'
|
|
import { usePreferencesStore } from '../stores/preferences-store'
|
|
import { regionForUser } from '../lib/format'
|
|
import DiscoverFilters, {
|
|
DEFAULT_FILTERS,
|
|
hasAnyActiveFilters,
|
|
type DiscoverFilterState,
|
|
} from '../components/discover/DiscoverFilters'
|
|
import TonightHero from '../components/discover/TonightHero'
|
|
import { MoodChips, MoodRow } from '../components/discover/MoodChips'
|
|
import Roulette from '../components/discover/Roulette'
|
|
import DecadeStrip from '../components/discover/DecadeStrip'
|
|
import CanonicalLists from '../components/discover/CanonicalLists'
|
|
import OnThisDay from '../components/discover/OnThisDay'
|
|
import LibraryGapFinder from '../components/discover/LibraryGapFinder'
|
|
import {
|
|
BrowseSection,
|
|
genreTiles,
|
|
languageTiles,
|
|
studioTiles,
|
|
networkTiles,
|
|
type BrowseKey,
|
|
} from '../components/discover/BrowseGrid'
|
|
import { NoKey, Hero } from './discover/chrome'
|
|
import { FilteredGrid } from './discover/filtered-grid'
|
|
import {
|
|
TrendingDayRow,
|
|
TrendingWeekRow,
|
|
PopularMoviesRow,
|
|
PopularTvRow,
|
|
UpcomingMoviesRow,
|
|
TopRatedMoviesRow,
|
|
TopRatedTvRow,
|
|
CultClassicsRow,
|
|
} from './discover/rows'
|
|
|
|
/**
|
|
* Discover page - intent-driven entry to TMDB content the user doesn't
|
|
* already have.
|
|
*
|
|
* Layout:
|
|
* 1. Compact page header
|
|
* 2. Sticky filter bar (movie/tv + advanced filters)
|
|
* 3. Editorial spotlight - one big "pick of the day"
|
|
* 4. Mood chips - 10 vibes that resolve to a single content row
|
|
* 5. 3-4 curated rows (Trending / Acclaimed / Coming soon / etc.)
|
|
* 6. Browse-by sections: genre, language, studio, network - compact
|
|
* tile grids with inline expansion when a tile is clicked
|
|
*
|
|
* Active filters take over the page with the existing FilteredGrid - that
|
|
* mode is unchanged so the filter workflow stays familiar.
|
|
*/
|
|
export default function DiscoverPage() {
|
|
const hasTmdb = useHasTmdbKey()
|
|
const prefs = usePreferencesStore()
|
|
const region = prefs.region || regionForUser()
|
|
const [filters, setFilters] = useState<DiscoverFilterState>(() => ({
|
|
...DEFAULT_FILTERS,
|
|
watchRegion: region,
|
|
}))
|
|
const [moodId, setMoodId] = useState<string | null>(null)
|
|
const [decade, setDecade] = useState<string | null>(null)
|
|
const [expanded, setExpanded] = useState<BrowseKey | null>(null)
|
|
|
|
if (!hasTmdb) return <NoKey />
|
|
|
|
const filterMode = hasAnyActiveFilters(filters)
|
|
const kind = filters.kind
|
|
|
|
// Reset the mood / expansion when toggling movie<->tv since both are
|
|
// kind-scoped and the previously-active mood may not exist on the
|
|
// other side.
|
|
function onFiltersChange(next: DiscoverFilterState) {
|
|
if (next.kind !== filters.kind) {
|
|
setMoodId(null)
|
|
setDecade(null)
|
|
setExpanded(null)
|
|
}
|
|
setFilters(next)
|
|
}
|
|
|
|
return (
|
|
<div className="pb-12 overflow-x-hidden" style={{ isolation: 'isolate' }}>
|
|
<Hero />
|
|
|
|
<div
|
|
className="sticky top-0 py-3 -mt-1 mb-3 border-b border-border/60"
|
|
style={{ zIndex: 25, backgroundColor: 'var(--color-void)' }}
|
|
>
|
|
<DiscoverFilters filters={filters} onChange={onFiltersChange} region={region} />
|
|
</div>
|
|
|
|
{filterMode ? (
|
|
<FilteredGrid filters={filters} />
|
|
) : (
|
|
<>
|
|
<LazyMount><TonightHero kind={kind} /></LazyMount>
|
|
|
|
<div className="px-7 mb-2 flex items-center justify-end">
|
|
<Roulette kind={kind} moodId={moodId} />
|
|
</div>
|
|
|
|
<MoodChips activeId={moodId} onChange={setMoodId} kind={kind} />
|
|
|
|
{moodId && (
|
|
<div className="mb-4">
|
|
<MoodRow moodId={moodId} kind={kind} />
|
|
</div>
|
|
)}
|
|
|
|
<DecadeStrip kind={kind} active={decade} onChange={setDecade} />
|
|
|
|
<div className="relative z-10">
|
|
{kind === 'movie' ? (
|
|
<>
|
|
<LazyMount><TrendingDayRow /></LazyMount>
|
|
<LazyMount><PopularMoviesRow /></LazyMount>
|
|
<LazyMount><UpcomingMoviesRow region={region} /></LazyMount>
|
|
<LazyMount><TopRatedMoviesRow /></LazyMount>
|
|
<LazyMount><CultClassicsRow /></LazyMount>
|
|
</>
|
|
) : (
|
|
<>
|
|
<LazyMount><TrendingWeekRow /></LazyMount>
|
|
<LazyMount><PopularTvRow /></LazyMount>
|
|
<LazyMount><TopRatedTvRow /></LazyMount>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{kind === 'movie' && <LazyMount><OnThisDay /></LazyMount>}
|
|
|
|
{kind === 'movie' && <LazyMount><LibraryGapFinder /></LazyMount>}
|
|
|
|
{kind === 'movie' && <LazyMount><CanonicalLists /></LazyMount>}
|
|
|
|
<div className="mt-4 mb-2 px-7">
|
|
<div className="flex items-center gap-3 text-text-4">
|
|
<span className="flex-1 h-px bg-border" />
|
|
<span className="text-[10.5px] uppercase tracking-[0.2em] font-semibold">Browse by</span>
|
|
<span className="flex-1 h-px bg-border" />
|
|
</div>
|
|
</div>
|
|
|
|
<BrowseSection
|
|
eyebrow="By genre"
|
|
title="Genres"
|
|
subtitle="Top of each genre, missing from your shelves"
|
|
tiles={genreTiles()}
|
|
expanded={expanded}
|
|
onSelect={setExpanded}
|
|
/>
|
|
|
|
{kind === 'movie' && (
|
|
<BrowseSection
|
|
eyebrow="By language"
|
|
title="Around the world"
|
|
subtitle="Top-rated cinema by original language"
|
|
tiles={languageTiles()}
|
|
expanded={expanded}
|
|
onSelect={setExpanded}
|
|
/>
|
|
)}
|
|
|
|
{kind === 'movie' && (
|
|
<BrowseSection
|
|
eyebrow="By studio"
|
|
title="From the studios"
|
|
subtitle="Films grouped by who made them"
|
|
tiles={studioTiles()}
|
|
expanded={expanded}
|
|
onSelect={setExpanded}
|
|
/>
|
|
)}
|
|
|
|
{kind === 'tv' && (
|
|
<BrowseSection
|
|
eyebrow="By network"
|
|
title="On the networks"
|
|
subtitle="Shows grouped by where they air"
|
|
tiles={networkTiles()}
|
|
expanded={expanded}
|
|
onSelect={setExpanded}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|