"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Image from "next/image"; import Link from "next/link"; import { ReviewPlayer, type ReviewPlayerRef } from "@/components/player/ReviewPlayer"; import { useReviewStore } from "@/hooks/use-review-player"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { Film, Loader2, CheckCircle2, AlertCircle, Clock, ExternalLink, } from "lucide-react"; interface Project { id: string; name: string; code: string; projectType: string; } interface PlaylistVersion { id: string; versionNumber: number; fileUrl: string; thumbnailUrl: string | null; posterUrl: string | null; fps: number; approvalStatus: string; } interface PlaylistShot { id: string; shotCode: string; episode: string | null; status: string; thumbnailUrl: string | null; latestVersion: PlaylistVersion; } interface PlaylistClientProps { projects: Project[]; } const APPROVAL_COLORS: Record = { PENDING_REVIEW: "bg-amber-500/10 text-amber-400 border-amber-500/20", APPROVED: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", REJECTED: "bg-red-500/10 text-red-400 border-red-500/20", NEEDS_CHANGES: "bg-orange-500/10 text-orange-400 border-orange-500/20", }; const APPROVAL_ICONS: Record = { PENDING_REVIEW: Clock, APPROVED: CheckCircle2, REJECTED: AlertCircle, NEEDS_CHANGES: AlertCircle, }; export function PlaylistClient({ projects }: PlaylistClientProps) { const playerRef = useRef(null); const reset = useReviewStore((s) => s.reset); const [projectId, setProjectId] = useState(null); const [shots, setShots] = useState([]); const [activeShot, setActiveShot] = useState(null); const [isLoading, setIsLoading] = useState(false); // Restore last project from localStorage on mount useEffect(() => { const saved = localStorage.getItem("playlist:lastProjectId"); if (saved && projects.some((p) => p.id === saved)) { setProjectId(saved); } }, [projects]); // Fetch shots whenever project changes useEffect(() => { if (!projectId) { setShots([]); setActiveShot(null); return; } let cancelled = false; setIsLoading(true); fetch(`/api/playlist?projectId=${projectId}`) .then((r) => r.json()) .then((data) => { if (cancelled) return; const list: PlaylistShot[] = data.shots ?? []; setShots(list); // Auto-select the first shot, or keep the current one if it still exists setActiveShot((prev) => { const stillExists = prev && list.some((s) => s.id === prev.id); return stillExists ? prev : (list[0] ?? null); }); }) .catch(() => { if (!cancelled) setShots([]); }) .finally(() => { if (!cancelled) setIsLoading(false); }); return () => { cancelled = true; }; }, [projectId]); const handleProjectChange = (id: string) => { localStorage.setItem("playlist:lastProjectId", id); setProjectId(id); }; const handleSelectShot = useCallback((shot: PlaylistShot) => { reset(); setActiveShot(shot); }, [reset]); // Group shots by episode for episodic projects const selectedProject = projects.find((p) => p.id === projectId); const isEpisodic = selectedProject?.projectType === "EPISODIC"; const episodeGroups: [string, PlaylistShot[]][] = isEpisodic ? (() => { const map = new Map(); for (const shot of shots) { const key = shot.episode ?? "(No Episode)"; if (!map.has(key)) map.set(key, []); map.get(key)!.push(shot); } return Array.from(map.entries()); })() : []; return (
{/* Toolbar */}
Playlist
{activeShot && (
{activeShot.shotCode} {(() => { const Icon = APPROVAL_ICONS[activeShot.latestVersion.approvalStatus] ?? Clock; return ; })()} {activeShot.latestVersion.approvalStatus.replace(/_/g, " ")}
)}
{/* Body: player + shot panel */}
{/* Player */}
{activeShot ? ( ) : (

{!projectId ? "Select a project to start" : isLoading ? "" : "No shots with video versions found"}

{isLoading && }
)}
{/* Shot panel */}
Shots {shots.length > 0 && ( {shots.length} )}
{isLoading ? (
) : !projectId ? (

Select a project

) : shots.length === 0 ? (

No video versions found

) : isEpisodic ? (
{episodeGroups.map(([episode, episodeShots]) => (
Ep {episode}
{episodeShots.map((shot) => ( handleSelectShot(shot)} /> ))}
))}
) : (
{shots.map((shot) => ( handleSelectShot(shot)} /> ))}
)}
); } function ShotThumbnail({ shot, isActive, onClick, }: { shot: PlaylistShot; isActive: boolean; onClick: () => void; }) { const thumb = shot.latestVersion.thumbnailUrl ?? shot.latestVersion.posterUrl ?? shot.thumbnailUrl; const ApprovalIcon = APPROVAL_ICONS[shot.latestVersion.approvalStatus] ?? Clock; return ( ); }