import { useEffect, useMemo, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useQueries } from '@tanstack/react-query' import { Trash2 } from '../../lib/icons' import { fetchLetterboxdList, type LetterboxdListItem } from '../../api/letterboxd' import { getMovieFull } from '../../api/tmdb' import { useLibraryByTmdbId } from '../../hooks/use-jellyfin' import { mapTmdbToJf } from '../../lib/tmdb-mapping' import { useLetterboxdLists, type SavedLetterboxdList } from '../../stores/letterboxd-lists-store' import ContentRow from './ContentRow' interface Props { saved: SavedLetterboxdList } const RSS_STALE = 6 * 60 * 60 * 1000 /** * One imported Letterboxd list rendered as a content row. Lazy-mounts * once scrolled near the viewport so importing 5 lists doesn't burn * 100+ TMDB requests on home page load. */ export default function LetterboxdListRow({ saved }: Props) { const containerRef = useRef(null) const [near, setNear] = useState(false) useEffect(() => { if (!containerRef.current) return const el = containerRef.current const obs = new IntersectionObserver( entries => { for (const e of entries) { if (e.isIntersecting) { setNear(true) obs.disconnect() return } } }, { rootMargin: '200px' }, ) obs.observe(el) return () => obs.disconnect() }, []) return (
{near ? : }
) } function Mounted({ saved }: Props) { const remove = useLetterboxdLists(s => s.remove) const rss = useQuery({ queryKey: ['letterboxd', 'list', saved.url], queryFn: () => fetchLetterboxdList(saved.url), staleTime: RSS_STALE, }) const data = rss.data // Cap at 30 entries to keep the request burst reasonable; serious // lists with 100 films would slam TMDB on first mount otherwise. const entries: LetterboxdListItem[] = useMemo( () => (data?.items || []).slice(0, 30), [data?.items], ) const ids = useMemo( () => entries.map(e => e.tmdbId).filter((x): x is string => !!x).map(Number), [entries], ) const tmdbQueries = useQueries({ queries: ids.map(id => ({ queryKey: ['tmdb', 'movie-full', id], queryFn: () => getMovieFull(id), staleTime: 24 * 60 * 60 * 1000, })), }) const libraryByTmdbId = useLibraryByTmdbId() const items = useMemo(() => { const tmdbMap = new Map() ids.forEach((id, i) => { const d = tmdbQueries[i]?.data if (d) tmdbMap.set(id, { ...d, media_type: 'movie' }) }) // Preserve list order; drop items the proxy returned without a TMDB id. const ordered: any[] = [] for (const e of entries) { if (!e.tmdbId) continue const d = tmdbMap.get(Number(e.tmdbId)) if (d) ordered.push(d) } return mapTmdbToJf(ordered, libraryByTmdbId.data) }, [entries, ids, tmdbQueries, libraryByTmdbId.data]) if (rss.isLoading) return if (!data) { return (

{saved.customTitle || 'Letterboxd list'}

Couldn't load this list. The proxy may be down, or the URL is private.

) } if (items.length === 0) return null const matched = items.filter(i => (i as any)._inLibrary).length const subtitle = `${matched} of ${items.length} in your library ยท from Letterboxd` return (
) } function Placeholder({ saved }: Props) { return (

{saved.customTitle || 'Letterboxd list'}

{saved.url}

{Array.from({ length: 6 }).map((_, j) => (
))}
) }