"use client"; import { useState, useEffect } from "react"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, FormField, FormInput, Icon } from "@/app/components/ui"; import { UserDto, AnilistStatusDto, AnilistSyncReportDto, AnilistPullReportDto, AnilistSyncPreviewItemDto, AnilistSyncItemDto, AnilistPullItemDto } from "@/lib/api"; import { useTranslation } from "@/lib/i18n/context"; export function AnilistTab({ handleUpdateSetting, users, }: { handleUpdateSetting: (key: string, value: unknown) => Promise; users: UserDto[]; }) { const { t } = useTranslation(); const [origin, setOrigin] = useState(""); useEffect(() => { setOrigin(window.location.origin); }, []); const [clientId, setClientId] = useState(""); const [token, setToken] = useState(""); const [userId, setUserId] = useState(""); const [localUserId, setLocalUserId] = useState(""); const [isTesting, setIsTesting] = useState(false); const [viewer, setViewer] = useState(null); const [testError, setTestError] = useState(null); const [isSyncing, setIsSyncing] = useState(false); const [syncReport, setSyncReport] = useState(null); const [isPulling, setIsPulling] = useState(false); const [pullReport, setPullReport] = useState(null); const [actionError, setActionError] = useState(null); const [isPreviewing, setIsPreviewing] = useState(false); const [previewItems, setPreviewItems] = useState(null); useEffect(() => { fetch("/api/settings/anilist") .then((r) => r.ok ? r.json() : null) .then((data) => { if (data) { if (data.client_id) setClientId(String(data.client_id)); if (data.access_token) setToken(data.access_token); if (data.user_id) setUserId(String(data.user_id)); if (data.local_user_id) setLocalUserId(String(data.local_user_id)); } }) .catch(() => {}); }, []); function buildAnilistSettings() { return { client_id: clientId || undefined, access_token: token || undefined, user_id: userId ? Number(userId) : undefined, local_user_id: localUserId || undefined, }; } function handleConnect() { if (!clientId) return; // Save client_id first, then open OAuth URL handleUpdateSetting("anilist", buildAnilistSettings()).then(() => { window.location.href = `https://anilist.co/api/v2/oauth/authorize?client_id=${encodeURIComponent(clientId)}&response_type=token`; }); } async function handleSaveToken() { await handleUpdateSetting("anilist", buildAnilistSettings()); } async function handleTestConnection() { setIsTesting(true); setViewer(null); setTestError(null); try { // Save token first so the API reads the current value await handleUpdateSetting("anilist", buildAnilistSettings()); const resp = await fetch("/api/anilist/status"); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Connection failed"); setViewer(data); if (!userId && data.user_id) setUserId(String(data.user_id)); } catch (e) { setTestError(e instanceof Error ? e.message : "Connection failed"); } finally { setIsTesting(false); } } async function handlePreview() { setIsPreviewing(true); setPreviewItems(null); setActionError(null); try { const resp = await fetch("/api/anilist/sync/preview"); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Preview failed"); setPreviewItems(data); } catch (e) { setActionError(e instanceof Error ? e.message : "Preview failed"); } finally { setIsPreviewing(false); } } async function handleSync() { setIsSyncing(true); setSyncReport(null); setActionError(null); try { const resp = await fetch("/api/anilist/sync", { method: "POST" }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Sync failed"); setSyncReport(data); } catch (e) { setActionError(e instanceof Error ? e.message : "Sync failed"); } finally { setIsSyncing(false); } } async function handlePull() { setIsPulling(true); setPullReport(null); setActionError(null); try { const resp = await fetch("/api/anilist/pull", { method: "POST" }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || "Pull failed"); setPullReport(data); } catch (e) { setActionError(e instanceof Error ? e.message : "Pull failed"); } finally { setIsPulling(false); } } return ( <> {t("settings.anilistTitle")} {t("settings.anilistDesc")}

{t("settings.anilistConnectDesc")}

{/* Redirect URL info */}

{t("settings.anilistRedirectUrlLabel")}

{origin ? `${origin}/anilist/callback` : "/anilist/callback"}

{t("settings.anilistRedirectUrlHint")}

setClientId(e.target.value)} placeholder={t("settings.anilistClientIdPlaceholder")} />
{viewer && ( {t("settings.anilistConnected")} {viewer.username} {" · "} AniList )} {token && !viewer && ( {t("settings.anilistTokenPresent")} )} {testError && {testError}}
{t("settings.anilistManualToken")}
setToken(e.target.value)} placeholder={t("settings.anilistTokenPlaceholder")} /> setUserId(e.target.value)} placeholder={t("settings.anilistUserIdPlaceholder")} />

{t("settings.anilistLocalUserTitle")}

{t("settings.anilistLocalUserDesc")}

{t("settings.anilistSyncTitle")}

{t("settings.anilistSyncDesc")}

{syncReport && (
{t("settings.anilistSynced", { count: String(syncReport.synced) })} {syncReport.skipped > 0 && {t("settings.anilistSkipped", { count: String(syncReport.skipped) })}} {syncReport.errors.length > 0 && {t("settings.anilistErrors", { count: String(syncReport.errors.length) })}}
{syncReport.items.length > 0 && (
{syncReport.items.map((item: AnilistSyncItemDto) => (
{item.anilist_title ?? item.series_name}
{item.status} {item.progress_volumes > 0 && ( {item.progress_volumes} vol. )}
))}
)} {syncReport.errors.map((err: string, i: number) => (

{err}

))}
)}

{t("settings.anilistPullDesc")}

{pullReport && (
{t("settings.anilistUpdated", { count: String(pullReport.updated) })} {pullReport.skipped > 0 && {t("settings.anilistSkipped", { count: String(pullReport.skipped) })}} {pullReport.errors.length > 0 && {t("settings.anilistErrors", { count: String(pullReport.errors.length) })}}
{pullReport.items.length > 0 && (
{pullReport.items.map((item: AnilistPullItemDto) => (
{item.anilist_title ?? item.series_name}
{item.anilist_status} {item.books_updated} {t("dashboard.books").toLowerCase()}
))}
)} {pullReport.errors.map((err: string, i: number) => (

{err}

))}
)}
{actionError &&

{actionError}

} {previewItems !== null && (
{t("settings.anilistPreviewTitle", { count: String(previewItems.length) })}
{previewItems.length === 0 ? (

{t("settings.anilistPreviewEmpty")}

) : (
{previewItems.map((item) => (
{item.anilist_title ?? item.series_name} {item.anilist_title && item.anilist_title !== item.series_name && ( — {item.series_name} )}
{item.books_read}/{item.book_count} {item.status}
))}
)}
)}
); }