147 lines
4.9 KiB
TypeScript
147 lines
4.9 KiB
TypeScript
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 pb-3 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>
|
|
<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] }}
|
|
className="min-h-[200px]"
|
|
>
|
|
{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' as const }))
|
|
if (mood.extra) raw = raw.filter(mood.extra)
|
|
return mapTmdbToJf(filterToMissing(raw, lib.data, hideAdult, m => !!m.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`}
|
|
/>
|
|
)
|
|
}
|