'use client'; import { useState, useMemo } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { Loader2, X, ChevronDown, ChevronRight } from 'lucide-react'; import { fetchPushPendingPreview, confirmBulkPush, type PushPendingPreviewItem, } from '@/lib/api'; import styles from './PushPendingModal.module.css'; const FIELD_LABELS: Record = { title: 'Title', description: 'Description', privacyStatus: 'Visibility', tags: 'Tags', categoryId: 'Category', defaultLanguage: 'Language', defaultAudioLanguage: 'Audio Language', selfDeclaredMadeForKids: 'Made for Kids', embeddable: 'Embeddable', license: 'License', recordingDate: 'Recording Date', }; function FieldChip({ field }: { field: string }) { return {FIELD_LABELS[field] ?? field}; } function TagsDiff({ before, after }: { before: string[]; after: string[] }) { const beforeSet = new Set(before); const afterSet = new Set(after); const removed = before.filter((t) => !afterSet.has(t)); const added = after.filter((t) => !beforeSet.has(t)); const kept = after.filter((t) => beforeSet.has(t)); return (
{kept.map((t) => {t})} {removed.map((t) => {t})} {added.map((t) => {t})}
); } function DiffRow({ field, item }: { field: string; item: PushPendingPreviewItem }) { const label = FIELD_LABELS[field] ?? field; const d = item.diff[field as keyof typeof item.diff]; if (!d) return null; if (field === 'tags') { return ( {label} {(d.before as string[]).join(', ') || '—'} ); } if (field === 'description') { return ( {label}
{d.before ?? '—'}
{d.after ?? '—'}
); } const fmt = (v: string | number | boolean | string[] | null | undefined) => { if (v === null || v === undefined) return '—'; if (typeof v === 'boolean') return v ? 'Yes' : 'No'; return String(v); }; return ( {label} {fmt(d.before)} {fmt(d.after)} ); } function VideoRow({ item, selected, onToggle, }: { item: PushPendingPreviewItem; selected: boolean; onToggle: () => void; }) { const [expanded, setExpanded] = useState(false); const thumb = item.thumbnailUrl ?? `https://img.youtube.com/vi/${item.videoId}/mqdefault.jpg`; return ( <> {/* eslint-disable-next-line @next/next/no-img-element */} {item.title}
{item.firstSync ? First sync : item.changedFields.map((f) => ) }
{expanded && ( {item.changedFields.map((f) => ( ))}
Field Before After
)} ); } export default function PushPendingModal({ onClose, sort, order }: { onClose: () => void; sort?: string; order?: string }) { const router = useRouter(); const { data: items = [], isLoading } = useQuery({ queryKey: ['pushPendingPreview', sort, order], queryFn: () => fetchPushPendingPreview(sort, order), staleTime: 30_000, }); // null = untouched (treat as all selected); Set = explicit selection state const [selected, setSelected] = useState | null>(null); const effectiveSelected = useMemo( () => selected ?? new Set(items.map((i) => i.videoId)), [items, selected], ); const allSelected = items.length > 0 && effectiveSelected.size === items.length; const toggleAll = () => { if (allSelected) setSelected(new Set()); else setSelected(new Set(items.map((i) => i.videoId))); }; const toggleOne = (id: string) => { const next = new Set(effectiveSelected); if (next.has(id)) next.delete(id); else next.add(id); setSelected(next); }; const confirmMut = useMutation({ mutationFn: () => confirmBulkPush([...effectiveSelected]), onSuccess: () => { setSelected(null); onClose(); router.push('/bulk-jobs'); }, }); const selectedCount = effectiveSelected.size; return (
e.target === e.currentTarget && onClose()}>

Push Pending Videos

{isLoading && (
Loading pending changes…
)} {!isLoading && items.length === 0 && (
No pending videos found.
)} {!isLoading && items.length > 0 && ( {items.map((item) => ( toggleOne(item.videoId)} /> ))}
Video
)}
{!isLoading && items.length > 0 && (
Videos are pushed in order, respecting YouTube quota limits.
)}
); }