import { useEffect, useId, useMemo, useRef, useState } from 'react'; import { apiClient } from '../../lib/api-client'; import { formatDuration } from '../../lib/formatters'; import { useModalFocus } from '../../hooks/useModalFocus'; import { AnimeCoverImage } from './AnimeCoverImage'; import type { AnimeLibraryItem } from '../../types/stats'; interface LibraryEntryPickerProps { heading: string; /** Entries that cannot be picked, typically the one being moved away from. */ excludeAnimeIds?: number[]; initialQuery?: string; busyAnimeId?: number | null; error?: string | null; onSelect: (entry: AnimeLibraryItem) => void; onClose: () => void; } export function LibraryEntryPicker({ heading, excludeAnimeIds = [], initialQuery = '', busyAnimeId = null, error = null, onSelect, onClose, }: LibraryEntryPickerProps) { const [entries, setEntries] = useState(null); const [loadFailed, setLoadFailed] = useState(false); const [query, setQuery] = useState(initialQuery); const inputRef = useRef(null); const dialogRef = useRef(null); const headingId = useId(); const searchId = useId(); const busy = busyAnimeId !== null; useEffect(() => { let cancelled = false; apiClient .getAnimeLibrary() .then((data) => { if (!cancelled) setEntries(data); }) .catch(() => { // Distinct from an empty library: telling the user "no other titles" // when the request failed hides a retryable error. if (cancelled) return; setEntries([]); setLoadFailed(true); }); return () => { cancelled = true; }; }, []); useModalFocus({ dialogRef, initialFocusRef: inputRef, dismissDisabled: busy, onDismiss: onClose, }); const handleDismiss = () => { if (!busy) onClose(); }; const excluded = useMemo(() => new Set(excludeAnimeIds), [excludeAnimeIds]); const visible = useMemo(() => { const term = query.trim().toLowerCase(); return (entries ?? []) .filter((entry) => !excluded.has(entry.animeId)) .filter((entry) => !term || entry.canonicalTitle.toLowerCase().includes(term)) .sort((a, b) => b.lastWatchedMs - a.lastWatchedMs); }, [entries, excluded, query]); return (
e.stopPropagation()} >

{heading}

setQuery(e.target.value)} placeholder="Search library..." className="w-full bg-ctp-surface0 border border-ctp-surface1 rounded-lg px-3 py-2 text-sm text-ctp-text placeholder:text-ctp-overlay2 focus:outline-none focus:border-ctp-blue" /> {error ? (
{error}
) : null}
{entries === null &&
Loading...
} {loadFailed && (
Could not load the library. Close this dialog and try again.
)} {!loadFailed && entries !== null && visible.length === 0 && (
{query.trim() ? 'No matches' : 'No other titles'}
)} {visible.map((entry) => ( ))}
); }