discover components
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { useMemo } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { useTmdbDiscoverMovies, useTmdbDiscoverTv } from '../../hooks/use-tmdb'
|
||||
import { useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
||||
import { usePreferencesStore } from '../../stores/preferences-store'
|
||||
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
||||
import { DISCOVER_MOODS, type DiscoverMood } from '../../lib/discover-moods'
|
||||
import { filterToMissing } from '../../pages/discover/helpers'
|
||||
import ContentRow from '../ui/ContentRow'
|
||||
|
||||
interface Props {
|
||||
activeId: string | null
|
||||
onChange: (id: string | null) => void
|
||||
kind: 'movie' | 'tv'
|
||||
}
|
||||
|
||||
export function MoodChips({ activeId, onChange, kind }: Props) {
|
||||
return (
|
||||
<div className="px-7 mb-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="w-1 h-3 rounded-full bg-accent" />
|
||||
<span className="text-[10.5px] uppercase tracking-[0.18em] font-semibold text-text-3">
|
||||
In the mood for
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative -mx-7 px-7 overflow-x-auto hide-scrollbar">
|
||||
<ul className="flex items-center gap-2 pb-1 min-w-max">
|
||||
{DISCOVER_MOODS.map(mood => {
|
||||
const available = kind === 'movie' ? !!mood.movieParams : !!mood.tvParams
|
||||
return (
|
||||
<MoodChipButton
|
||||
key={mood.id}
|
||||
mood={mood}
|
||||
active={activeId === mood.id && available}
|
||||
disabled={!available}
|
||||
onClick={() => available && onChange(activeId === mood.id ? null : mood.id)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MoodChipButton({
|
||||
mood,
|
||||
active,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
mood: DiscoverMood
|
||||
active: boolean
|
||||
disabled: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
const Icon = mood.icon
|
||||
return (
|
||||
<li>
|
||||
<motion.button
|
||||
onClick={onClick}
|
||||
whileTap={disabled ? undefined : { scale: 0.96 }}
|
||||
aria-pressed={active}
|
||||
aria-disabled={disabled}
|
||||
title={disabled ? `${mood.label} - movies only` : mood.blurb}
|
||||
className={`relative inline-flex items-center gap-2 h-10 pl-3 pr-4 rounded-full border text-[12.5px] font-medium tracking-tight transition-colors duration-200 focus-ring ${
|
||||
active
|
||||
? 'bg-accent text-void border-accent shadow-[0_8px_20px_-8px_rgba(245,182,66,0.55)]'
|
||||
: disabled
|
||||
? 'bg-elevated/20 text-text-4 border-border/60 cursor-not-allowed'
|
||||
: 'bg-elevated/50 text-text-2 border-border hover:border-border-strong hover:text-text-1 hover:bg-elevated/80'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
size={14}
|
||||
stroke={active ? 2.2 : 1.75}
|
||||
className={active ? 'text-void' : disabled ? 'text-text-5' : 'text-text-2'}
|
||||
/>
|
||||
{mood.label}
|
||||
</motion.button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The row that materialises beneath the chip strip when a mood is picked.
|
||||
* Movie-only moods show just one row; the few that have TV recipes show
|
||||
* two rows (movies then shows).
|
||||
*/
|
||||
export function MoodRow({ moodId, kind }: { moodId: string; kind: 'movie' | 'tv' }) {
|
||||
const mood = DISCOVER_MOODS.find(m => m.id === moodId)
|
||||
if (!mood) return null
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={moodId + ':' + kind}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -6 }}
|
||||
transition={{ duration: 0.28, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
{kind === 'movie' && <MoodMovieRow mood={mood} />}
|
||||
{kind === 'tv' && mood.tvParams && <MoodTvRow mood={mood} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
function MoodMovieRow({ mood }: { mood: DiscoverMood }) {
|
||||
const movies = useTmdbDiscoverMovies(mood.movieParams)
|
||||
const lib = useLibraryByTmdbId()
|
||||
const hideAdult = usePreferencesStore(s => s.hideAdult)
|
||||
const items = useMemo(() => {
|
||||
let raw = (movies.data?.results || []).map(m => ({ ...m, media_type: 'movie' }))
|
||||
if (mood.extra) raw = raw.filter(mood.extra)
|
||||
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!(m as any).adult), lib.data)
|
||||
}, [movies.data, lib.data, hideAdult, mood])
|
||||
if (items.length === 0) return null
|
||||
return (
|
||||
<ContentRow
|
||||
title={mood.label}
|
||||
subtitle={mood.blurb}
|
||||
items={items}
|
||||
layoutKey={`mood_${mood.id}_movie`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MoodTvRow({ mood }: { mood: DiscoverMood }) {
|
||||
const tv = useTmdbDiscoverTv(mood.tvParams || {})
|
||||
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={mood.label}
|
||||
subtitle={mood.blurb}
|
||||
items={items}
|
||||
layoutKey={`mood_${mood.id}_tv`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user