"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { displayJobItemError, isYouTubeUploadLimitError } from "@/lib/youtube/errors"; import type { JobResponse } from "@/lib/types"; import { YouTubeLimitBanner } from "./YouTubeLimitBanner"; const STATUS_LABELS: Record = { PENDING: "Queued", ENCODING: "Encoding", UPLOADING: "Uploading", COMPLETED: "Completed", FAILED: "Failed", }; function sameDay(a: Date, b: Date) { return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); } function formatDayLabel(date: Date) { const today = new Date(); const yesterday = new Date(); yesterday.setDate(today.getDate() - 1); if (sameDay(date, today)) return "Today"; if (sameDay(date, yesterday)) return "Yesterday"; return date.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric", }); } function groupJobsByDay(jobs: JobResponse[]) { const groups = new Map(); for (const job of jobs) { const date = new Date(job.createdAt); const key = date.toLocaleDateString("en-CA"); const existing = groups.get(key); if (existing) { existing.jobs.push(job); } else { groups.set(key, { label: formatDayLabel(date), jobs: [job] }); } } return Array.from(groups.entries()) .sort(([a], [b]) => b.localeCompare(a)) .map(([, group]) => group); } function statusClass(status: string) { if (status === "COMPLETED") return "bg-green-500/20 text-green-400"; if (status === "FAILED") return "bg-red-500/20 text-red-400"; return "bg-yellow-500/20 text-yellow-400"; } export function JobHistory() { const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let active = true; async function load() { try { const res = await fetch("/api/jobs?limit=100"); if (!res.ok) throw new Error("Failed to load history"); const data = await res.json(); if (active) setJobs(data.jobs); } catch (err) { if (active) setError(err instanceof Error ? err.message : "Error loading history"); } finally { if (active) setLoading(false); } } load(); return () => { active = false; }; }, []); const dayGroups = useMemo(() => groupJobsByDay(jobs), [jobs]); const youtubeLimitHit = useMemo( () => jobs.some((job) => job.items.some( (item) => item.status === "FAILED" && item.error && isYouTubeUploadLimitError(item.error), ), ), [jobs], ); if (loading) { return
Loading history…
; } if (error) { return (
{error}
); } if (dayGroups.length === 0) { return (

No videos created yet.

Create your first video
); } return (
{youtubeLimitHit && } {dayGroups.map((group) => (

{group.label}

{group.jobs.map((job) => (

{new Date(job.createdAt).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", })} {" · "} {job.items.length} video{job.items.length === 1 ? "" : "s"}

Status: {job.status}

View job
{job.items.map((item) => { const itemError = item.status === "FAILED" ? displayJobItemError(item.error) : null; return (

{item.title}

{item.audioFilename}

{item.youtubeVideoId && ( View on YouTube )}
{STATUS_LABELS[item.status] || item.status}
{itemError && (

{itemError}

)}
); })}
))}
))}
); }