"use client"; import { useState, useCallback } from "react"; import { createPortal } from "react-dom"; import { useRouter } from "next/navigation"; import { Button } from "./ui"; import { useTranslation } from "../../lib/i18n/context"; import type { AnilistMediaResultDto, AnilistSeriesLinkDto } from "../../lib/api"; interface ReadingStatusModalProps { libraryId: string; seriesName: string; readingStatusProvider: string | null; existingLink: AnilistSeriesLinkDto | null; } type ModalStep = "idle" | "searching" | "results" | "linked"; export function ReadingStatusModal({ libraryId, seriesName, readingStatusProvider, existingLink, }: ReadingStatusModalProps) { const { t } = useTranslation(); const router = useRouter(); const [isOpen, setIsOpen] = useState(false); const [step, setStep] = useState(existingLink ? "linked" : "idle"); const [query, setQuery] = useState(seriesName); const [candidates, setCandidates] = useState([]); const [error, setError] = useState(null); const [link, setLink] = useState(existingLink); const [isLinking, setIsLinking] = useState(false); const [isUnlinking, setIsUnlinking] = useState(false); const handleOpen = useCallback(() => { setIsOpen(true); setStep(link ? "linked" : "idle"); setQuery(seriesName); setCandidates([]); setError(null); }, [link, seriesName]); const handleClose = useCallback(() => setIsOpen(false), []); async function handleSearch() { setStep("searching"); setError(null); try { const resp = await fetch("/api/anilist/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Search failed"); setCandidates(data); setStep("results"); } catch (e) { setError(e instanceof Error ? e.message : "Search failed"); setStep("idle"); } } async function handleLink(candidate: AnilistMediaResultDto) { setIsLinking(true); setError(null); try { const resp = await fetch( `/api/anilist/series/${libraryId}/${encodeURIComponent(seriesName)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ anilist_id: candidate.id }), } ); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Link failed"); setLink(data); setStep("linked"); router.refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Link failed"); } finally { setIsLinking(false); } } async function handleUnlink() { setIsUnlinking(true); setError(null); try { const resp = await fetch( `/api/anilist/series/${libraryId}/${encodeURIComponent(seriesName)}`, { method: "DELETE" } ); if (!resp.ok) throw new Error("Unlink failed"); setLink(null); setStep("idle"); router.refresh(); } catch (e) { setError(e instanceof Error ? e.message : "Unlink failed"); } finally { setIsUnlinking(false); } } if (!readingStatusProvider) return null; const providerLabel = readingStatusProvider === "anilist" ? "AniList" : readingStatusProvider; return ( <> {isOpen && createPortal( <>
{/* Header */}
{providerLabel} — {seriesName}
{/* Linked state */} {step === "linked" && link && (

{link.anilist_title ?? seriesName}

{link.anilist_url && ( {link.anilist_url} )}

ID: {link.anilist_id} · {t(`readingStatus.status.${link.status}` as any) || link.status}

{providerLabel}
)} {/* Search form */} {(step === "idle" || step === "results") && (
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSearch(); }} placeholder={t("readingStatus.searchPlaceholder")} className="flex-1 text-sm border border-border rounded-lg px-3 py-2 bg-background focus:outline-none focus:ring-2 focus:ring-ring" />
{step === "results" && candidates.length === 0 && (

{t("readingStatus.noResults")}

)} {step === "results" && candidates.length > 0 && (
{candidates.map((c) => (

{c.title_romaji ?? c.title_english}

{c.title_english && c.title_english !== c.title_romaji && (

{c.title_english}

)}
{c.volumes && {c.volumes} vol.} {c.status && {c.status}}
))}
)}
)} {/* Searching spinner */} {step === "searching" && (
{t("readingStatus.searching")}
)} {error &&

{error}

}
, document.body )} ); }