import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "~/shared/lib/api"; import type { ReviewGroup, ReviewGroupsResponse } from "~/shared/lib/types"; import { ColumnShell } from "./ColumnShell"; import { PipelineCard } from "./PipelineCard"; import { SeriesCard } from "./SeriesCard"; const PAGE_SIZE = 25; interface ReviewColumnProps { initialResponse: ReviewGroupsResponse; totalItems: number; jellyfinUrl: string; onMutate: () => void; } export function ReviewColumn({ initialResponse, totalItems, jellyfinUrl, onMutate }: ReviewColumnProps) { const [groups, setGroups] = useState(initialResponse.groups); const [hasMore, setHasMore] = useState(initialResponse.hasMore); const [loadingMore, setLoadingMore] = useState(false); const sentinelRef = useRef(null); // Reset when the parent refetches page 0 (after approve/skip actions). useEffect(() => { setGroups(initialResponse.groups); setHasMore(initialResponse.hasMore); }, [initialResponse]); const loadMore = useCallback(async () => { if (loadingMore || !hasMore) return; setLoadingMore(true); try { const res = await api.get(`/api/review/groups?offset=${groups.length}&limit=${PAGE_SIZE}`); setGroups((prev) => [...prev, ...res.groups]); setHasMore(res.hasMore); } finally { setLoadingMore(false); } }, [groups.length, hasMore, loadingMore]); useEffect(() => { if (!hasMore || !sentinelRef.current) return; const observer = new IntersectionObserver( (entries) => { if (entries[0]?.isIntersecting) loadMore(); }, { rootMargin: "200px" }, ); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [hasMore, loadMore]); const skipAll = async () => { if (!confirm(`Skip all ${totalItems} pending items? They won't be processed unless you unskip them.`)) return; await api.post("/api/review/skip-all"); onMutate(); }; const autoApprove = async () => { const res = await api.post<{ ok: boolean; count: number }>("/api/review/auto-approve"); onMutate(); if (res.count === 0) alert("No high-confidence items to auto-approve."); }; const approveItem = async (itemId: number) => { await api.post(`/api/review/${itemId}/approve`); onMutate(); }; const skipItem = async (itemId: number) => { await api.post(`/api/review/${itemId}/skip`); onMutate(); }; const approveBatch = async (itemIds: number[]) => { if (itemIds.length === 0) return; await api.post<{ ok: boolean; count: number }>("/api/review/approve-batch", { itemIds }); onMutate(); }; // Compute ids per visible group for "Approve above" const idsByGroup: number[][] = groups.map((g) => g.kind === "movie" ? [g.item.item_id] : g.seasons.flatMap((s) => s.episodes.map((ep) => ep.item_id)), ); const priorIds = (index: number): number[] => idsByGroup.slice(0, index).flat(); const actions = totalItems > 0 ? [ { label: "Auto Review", onClick: autoApprove, primary: true }, { label: "Skip all", onClick: skipAll }, ] : undefined; return (
{groups.map((group, index) => { const prior = index > 0 ? priorIds(index) : null; const onApproveUpToHere = prior && prior.length > 0 ? () => approveBatch(prior) : undefined; if (group.kind === "movie") { return ( { await api.patch(`/api/review/${group.item.item_id}/stream/${streamId}`, { action }); onMutate(); }} onApprove={() => approveItem(group.item.item_id)} onSkip={() => skipItem(group.item.item_id)} onApproveUpToHere={onApproveUpToHere} /> ); } return ( ); })} {groups.length === 0 &&

No items to review

} {hasMore && (
{loadingMore ? "Loading more…" : ""}
)}
); }