'use client'; import { useState, useRef, useEffect } from 'react'; import { Menu, Search, Sun, Moon, Eye, LogOut, Loader2, CloudUpload, CheckCircle2, XCircle, Clock, Loader } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useUIStore } from '@/store/useUIStore'; import { useAuthStore } from '@/store/useAuthStore'; import { useTheme } from '@/hooks/useTheme'; import apiClient from '@/lib/api-client'; import { fetchSavedViews, fetchTemplates, bulkPreview, bulkApply, fetchSyncQueueStatus, type BulkActionType, type BulkPreviewResponse } from '@/lib/api'; import Modal from './Modal'; import f from './FormField.module.css'; import styles from './Header.module.css'; // ─── Bulk Action Modal ──────────────────────────────────────────────────────── const ACTION_LABELS: Record = { SET_PRIVACY: 'Set Privacy Status', SET_TEMPLATE: 'Assign Template', ADD_TAGS: 'Add Tags', REMOVE_TAGS: 'Remove Tags', SEARCH_REPLACE_TITLE: 'Search & Replace in Title', }; function BulkModal({ onClose }: { onClose: () => void }) { const [step, setStep] = useState<'configure' | 'preview'>('configure'); const [actionType, setActionType] = useState('SET_PRIVACY'); const [targetType, setTargetType] = useState<'all' | 'view'>('all'); const [savedViewId, setSavedViewId] = useState(''); const [payload, setPayload] = useState>({}); const [preview, setPreview] = useState(null); const { data: savedViews = [] } = useQuery({ queryKey: ['saved-views'], queryFn: fetchSavedViews }); const { data: templates = [] } = useQuery({ queryKey: ['templates'], queryFn: fetchTemplates }); const buildPayload = () => { if ((actionType === 'ADD_TAGS' || actionType === 'REMOVE_TAGS') && typeof payload.tags === 'string') { return { ...payload, tags: payload.tags.split(',').map((t) => t.trim()).filter(Boolean) }; } return payload; }; const previewMut = useMutation({ mutationFn: () => bulkPreview({ type: actionType, payload: buildPayload(), savedViewId: targetType === 'view' ? savedViewId : undefined, }), onSuccess: (data) => { setPreview(data); setStep('preview'); }, }); const applyMut = useMutation({ mutationFn: () => bulkApply({ type: actionType, payload: buildPayload(), savedViewId: targetType === 'view' ? savedViewId : undefined, }), onSuccess: onClose, }); const setP = (key: string, val: string) => setPayload((p) => ({ ...p, [key]: val })); return ( {step === 'configure' && ( <>
{actionType === 'SET_PRIVACY' && (
)} {actionType === 'SET_TEMPLATE' && (
)} {(actionType === 'ADD_TAGS' || actionType === 'REMOVE_TAGS') && (
setP('tags', e.target.value)} placeholder="tag1, tag2, tag3" />
)} {actionType === 'SEARCH_REPLACE_TITLE' && (
setP('search', e.target.value)} placeholder="text to find" />
setP('replace', e.target.value)} placeholder="replacement" />
)}
{targetType === 'view' && (
)} {previewMut.isError &&

Preview failed — check the backend logs.

}
)} {step === 'preview' && preview && ( <>

{preview.count} video{preview.count !== 1 ? 's' : ''} will be affected by {ACTION_LABELS[preview.type]}.

{preview.previews.slice(0, 50).map((p) => ( ))}
Video Before After
{p.before.title} {preview.type === 'SET_PRIVACY' && p.before.privacyStatus} {preview.type === 'ADD_TAGS' && (p.before.tags ?? []).join(', ')} {preview.type === 'REMOVE_TAGS' && (p.before.tags ?? []).join(', ')} {preview.type === 'SEARCH_REPLACE_TITLE' && p.before.title} {preview.type === 'SET_TEMPLATE' && (p.before.templateId ?? '—')} {preview.type === 'SET_PRIVACY' && p.after.privacyStatus} {preview.type === 'ADD_TAGS' && (p.after.tags ?? []).join(', ')} {preview.type === 'REMOVE_TAGS' && (p.after.tags ?? []).join(', ')} {preview.type === 'SEARCH_REPLACE_TITLE' && p.after.title} {preview.type === 'SET_TEMPLATE' && (p.after.templateId ?? '—')}
{preview.previews.length > 50 && (

+ {preview.previews.length - 50} more videos not shown

)}
{applyMut.isError &&

Apply failed — check the backend logs.

}
)}
); } // ─── Sync Status Indicator ──────────────────────────────────────────────────── function SyncStatusIndicator() { const [open, setOpen] = useState(false); const ref = useRef(null); const { data } = useQuery({ queryKey: ['syncQueueStatus'], queryFn: fetchSyncQueueStatus, refetchInterval: (query) => { const d = query.state.data; if (d?.active.length) return 3_000; if (d?.waiting.length) return 8_000; return 30_000; }, }); useEffect(() => { if (!open) return; function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); } document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, [open]); const activeCount = data?.active.length ?? 0; const waitingCount = data?.waiting.length ?? 0; const pendingCount = activeCount + waitingCount; const hasFailures = (data?.recentFailed.length ?? 0) > 0; function formatRelative(iso: string | null) { if (!iso) return ''; const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60_000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; return `${Math.floor(mins / 60)}h ago`; } return (
{open && (

YouTube sync queue

{activeCount === 0 && waitingCount === 0 && data?.recentFailed.length === 0 && data?.recentCompleted.length === 0 && (

Queue is empty.

)} {activeCount > 0 && (

Processing

{data!.active.map((j) => (
{j.videoTitle}
))}
)} {waitingCount > 0 && (

Queued

{data!.waiting.map((j) => (
{j.videoTitle}
))}
)} {(data?.recentFailed.length ?? 0) > 0 && (

Failed

{data!.recentFailed.map((j) => (
{j.videoTitle} {j.failedReason && {j.failedReason}}
{formatRelative(j.failedAt)}
))}
)} {(data?.recentCompleted.length ?? 0) > 0 && (

Completed

{data!.recentCompleted.map((j) => (
{j.videoTitle} {formatRelative(j.completedAt)}
))}
)}
)}
); } // ─── Header ─────────────────────────────────────────────────────────────────── export default function Header() { const { toggleSidebar } = useUIStore(); const { theme, toggleTheme } = useTheme(); const { user, clearAuth } = useAuthStore(); const router = useRouter(); const [bulkOpen, setBulkOpen] = useState(false); const searchRef = useRef(null); async function handleLogout() { try { await apiClient.post('/auth/logout'); } catch {} clearAuth(); document.cookie = 'sf_session=; path=/; max-age=0'; router.replace('/login'); } function handleSearch(e: React.KeyboardEvent) { if (e.key === 'Enter') { const q = (e.target as HTMLInputElement).value.trim(); if (q) router.push(`/videos?search=${encodeURIComponent(q)}`); } } return ( <>
{user && (
{user.teamRole} {user.name}
)}
{bulkOpen && setBulkOpen(false)} />} ); }