discover components

This commit is contained in:
2026-03-29 05:55:12 +03:00
parent a039249ede
commit 02d65fbeeb
10 changed files with 2161 additions and 0 deletions
+145
View File
@@ -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`}
/>
)
}