import { useId, useRef, useState } from 'react'; import { apiClient } from '../../lib/api-client'; import { formatDuration, formatNumber } from '../../lib/formatters'; import { useModalFocus } from '../../hooks/useModalFocus'; import { AnimeCoverImage } from './AnimeCoverImage'; import type { AnimeLibraryItem } from '../../types/stats'; interface AnimeMergeDialogProps { entries: AnimeLibraryItem[]; onClose: () => void; onMerged: (survivingAnimeId: number) => void; } /** Biggest entry first: the one most likely to carry the right title and art. */ function pickDefaultKeeper(entries: AnimeLibraryItem[]): number { const best = [...entries].sort( (a, b) => b.episodeCount - a.episodeCount || b.totalActiveMs - a.totalActiveMs, )[0]; return best?.animeId ?? 0; } export function AnimeMergeDialog({ entries, onClose, onMerged }: AnimeMergeDialogProps) { const headingId = useId(); const dialogRef = useRef(null); const closeButtonRef = useRef(null); const [keeperId, setKeeperId] = useState(() => pickDefaultKeeper(entries)); const [merging, setMerging] = useState(false); const [error, setError] = useState(null); const totalEpisodes = entries.reduce((sum, entry) => sum + entry.episodeCount, 0); const totalCards = entries.reduce((sum, entry) => sum + entry.totalCards, 0); const totalActiveMs = entries.reduce((sum, entry) => sum + entry.totalActiveMs, 0); useModalFocus({ dialogRef, initialFocusRef: closeButtonRef, dismissDisabled: merging, onDismiss: onClose, }); const handleMerge = async () => { const sourceAnimeIds = entries .map((entry) => entry.animeId) .filter((animeId) => animeId !== keeperId); if (sourceAnimeIds.length === 0) return; setMerging(true); setError(null); try { const result = await apiClient.mergeAnime(keeperId, sourceAnimeIds); onMerged(result.animeId); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to merge these entries.'); setMerging(false); } }; // Dismissing mid-request would leave the caller unaware of a merge that is // still going to land, so the backdrop and close button are inert until it // resolves. const handleDismiss = () => { if (!merging) onClose(); }; return (
e.stopPropagation()} >

Merge {entries.length} Library Entries

Pick the entry to keep. Every episode moves onto it and the others are removed; no sessions or mined cards are deleted.

{entries.map((entry) => ( ))}
{error ? (
{error}
) : null}
Result: {totalEpisodes} episode{totalEpisodes !== 1 ? 's' : ''} ·{' '} {formatDuration(totalActiveMs)} · {formatNumber(totalCards)} cards
); }