"use client"; import { useState, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { ReviewPlayer, type ReviewPlayerRef } from "@/components/player/ReviewPlayer"; import { CommentPanel } from "@/components/comments/CommentPanel"; import { VersionList } from "@/components/versions/VersionList"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { submitApproval } from "@/actions/approvals"; import { useToast } from "@/components/ui/use-toast"; import { ShareWithClientButton } from "@/components/versions/ShareWithClientButton"; import { CheckCircle2, XCircle, AlertCircle, ChevronDown, ArrowLeft, Film, } from "lucide-react"; interface ReviewPageClientProps { version: { id: string; versionNumber: number; fileUrl: string; fps: number; duration: number | null; frameCount: number | null; approvalStatus: string; notes: string | null; isClientVisible: boolean; shot?: { id: string; shotCode: string; project: { id: string; name: string; code: string }; versions: { id: string; versionNumber: number; approvalStatus: string; isLatest: boolean; fps: number; duration: number | null; notes: string | null; fileSize: number | null; createdAt: Date; }[]; } | null; task?: { id: string; title: string; project: { id: string; name: string; code: string }; shot?: { id: string; shotCode: string } | null; asset?: { id: string; assetCode: string; name: string } | null; versions: { id: string; versionNumber: number; approvalStatus: string; isLatest: boolean; fps: number; duration: number | null; notes: string | null; fileSize: number | null; createdAt: Date; }[]; } | null; artist: { id: string; name: string | null; image: string | null; email: string } | null; }; comments: any[]; annotations: any[]; canApprove: boolean; canShare: boolean; currentUserId: string; } const APPROVAL_STATUS_STYLES: 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", }; export function ReviewPageClient({ version, comments: initialComments, annotations: initialAnnotations, canApprove, canShare, currentUserId, }: ReviewPageClientProps) { const [comments, setComments] = useState(initialComments); const [annotations, setAnnotations] = useState(initialAnnotations); const [pendingFrame, setPendingFrame] = useState(null); const [approvalDialog, setApprovalDialog] = useState<{ open: boolean; status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES" | null; }>({ open: false, status: null }); const [approvalNotes, setApprovalNotes] = useState(""); const [isSubmittingApproval, setIsSubmittingApproval] = useState(false); const [showVersions, setShowVersions] = useState(false); const playerRef = useRef(null); const { toast } = useToast(); const router = useRouter(); const handleAddComment = useCallback(() => { // Pause player first playerRef.current?.pause(); const frame = playerRef.current ? undefined : undefined; // Frame is tracked in ReviewPlayer store — CommentPanel reads from there setPendingFrame(Date.now()); // trigger effect with timestamp trick }, []); const handleCommentsChange = useCallback(async () => { try { const res = await fetch(`/api/versions/${version.id}/comments`); if (res.ok) { const data = await res.json(); setComments(data.comments); } } catch { // silently ignore } }, [version.id]); const handleAnnotationSaved = useCallback(async () => { // Refresh both annotations (for timeline markers) and comments (for the auto-comment) try { const [annRes, cmtRes] = await Promise.all([ fetch(`/api/versions/${version.id}/annotations`), fetch(`/api/versions/${version.id}/comments`), ]); if (annRes.ok) setAnnotations((await annRes.json()).annotations); if (cmtRes.ok) setComments((await cmtRes.json()).comments); } catch { // silently ignore } }, [version.id]); const handleApprovalSubmit = async () => { if (!approvalDialog.status) return; setIsSubmittingApproval(true); try { await submitApproval({ versionId: version.id, status: approvalDialog.status, notes: approvalNotes, }); toast({ title: approvalDialog.status === "APPROVED" ? "Version approved!" : approvalDialog.status === "REJECTED" ? "Version rejected" : "Changes requested", }); setApprovalDialog({ open: false, status: null }); setApprovalNotes(""); router.refresh(); } catch (err) { toast({ title: "Failed to submit approval", description: (err as Error).message, variant: "destructive", }); } finally { setIsSubmittingApproval(false); } }; const openApproval = (status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES") => { setApprovalDialog({ open: true, status }); }; // Resolve context: shot or task const project = version.shot?.project ?? version.task?.project; const contextCode = version.shot?.shotCode ?? version.task?.shot?.shotCode ?? version.task?.asset?.assetCode ?? null; const backHref = version.shot ? `/projects/${project?.id}/shots/${version.shot.id}` : version.task ? `/tasks/${version.task.id}` : `/dashboard`; const versionList = version.shot?.versions ?? version.task?.versions ?? []; return (
{/* Top bar */}
{project?.code} / {contextCode && ( <> {contextCode} / )} {/* Version selector */} {versionList.map((v) => ( v{String(v.versionNumber).padStart(3, "0")} {v.isLatest && " (latest)"} ))}
{/* Approval status */} {/* Share with Client */} {canShare && ( {/* router.refresh() handled inside component */}} /> )} {/* Approval actions */} {canApprove && (
)}
{/* Main content — player + comments */}
{/* Player — 70% */}
{/* Comment panel — 30%, min 280px */}
setPendingFrame(null)} onSeekToFrame={(frame) => playerRef.current?.seekToFrame(frame)} />
{/* Approval Dialog */} !o && setApprovalDialog({ open: false, status: null })} > {approvalDialog.status === "APPROVED" ? "Approve Version" : approvalDialog.status === "REJECTED" ? "Reject Version" : "Request Changes"}