"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { ChevronLeft, ChevronRight, Copy, Plus, Check, Loader2, Trash2, AlertCircle, ChevronDown, ChevronUp, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { TakeImageGallery } from "./TakeImageGallery"; import { PreviousTakePanel } from "./PreviousTakePanel"; import type { TakeQuality } from "@prisma/client"; // ─── Types ──────────────────────────────────────────────────────────────────── export interface TakeAttachmentData { id: string; fileUrl: string; fileName: string; fileType: string; category: string; caption: string | null; sortOrder: number; } export interface FullTake { id: string; setupId: string; takeNumber: number; scene: string | null; shotLabel: string | null; unitLabel: string | null; cameraLetter: string | null; clipName: string | null; roll: string | null; cameraModel: string | null; resolution: string | null; codec: string | null; fps: number | null; shutter: string | null; iso: number | null; whiteBalance: number | null; colourSpace: string | null; lensSet: string | null; lens: string | null; tStop: string | null; filters: string | null; isAnamorphic: boolean; hasHdri: boolean; hasChromeBall: boolean; hasGreyBall: boolean; hasMacbeth: boolean; hasCleanPlate: boolean; hasSurvey: boolean; hasLidar: boolean; hasWitnessCamera: boolean; hasLensGrid: boolean; hasTexturePhotos: boolean; hasPhotogrammetry: boolean; weather: string | null; sunDirection: string | null; artificialLights: string | null; supervisorNotes: string | null; continuityNotes: string | null; vfxRequirements: string | null; quality: TakeQuality; attachments: TakeAttachmentData[]; setup: { takes: { id: string; takeNumber: number }[]; shootDay: { id: string; date: string; unit: string; label: string | null; projectId: string }; }; } // ─── Quality config ─────────────────────────────────────────────────────────── const QUALITIES: { value: TakeQuality; label: string; color: string; active: string }[] = [ { value: "FALSE_START", label: "False Start", color: "border-zinc-700 text-zinc-500", active: "bg-zinc-700 text-zinc-200 border-zinc-600" }, { value: "NO_GOOD", label: "No Good", color: "border-red-900/60 text-red-500", active: "bg-red-900/60 text-red-200 border-red-700" }, { value: "PRINT", label: "Print", color: "border-amber-800/60 text-amber-500", active: "bg-amber-800/60 text-amber-200 border-amber-600" }, { value: "GOOD", label: "Good", color: "border-blue-800/60 text-blue-400", active: "bg-blue-900/60 text-blue-200 border-blue-600" }, { value: "HERO", label: "Hero ★", color: "border-green-800/60 text-green-500", active: "bg-green-900/60 text-green-200 border-green-600" }, ]; const TRACKING_ITEMS: { key: keyof FullTake; label: string }[] = [ { key: "hasHdri", label: "HDRI" }, { key: "hasChromeBall", label: "Chrome Ball" }, { key: "hasGreyBall", label: "Grey Ball" }, { key: "hasMacbeth", label: "Macbeth" }, { key: "hasCleanPlate", label: "Clean Plate" }, { key: "hasSurvey", label: "Survey" }, { key: "hasLidar", label: "LiDAR" }, { key: "hasWitnessCamera", label: "Witness Cam" }, { key: "hasLensGrid", label: "Lens Grid" }, { key: "hasTexturePhotos", label: "Texture Photos" }, { key: "hasPhotogrammetry", label: "Photogrammetry" }, ]; // ─── Autosave hook ──────────────────────────────────────────────────────────── type SaveStatus = "clean" | "dirty" | "saving" | "saved" | "error"; function useAutosave(takeId: string) { const [status, setStatus] = useState("clean"); const timerRef = useRef | null>(null); const pendingRef = useRef>({}); const flush = useCallback(async () => { if (Object.keys(pendingRef.current).length === 0) return; const patch = { ...pendingRef.current }; pendingRef.current = {}; setStatus("saving"); try { const res = await fetch(`/api/shoot-log/takes/${takeId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), }); if (!res.ok) throw new Error("save failed"); setStatus("saved"); setTimeout(() => setStatus("clean"), 2000); } catch { setStatus("error"); } }, [takeId]); const queue = useCallback( (patch: Record) => { pendingRef.current = { ...pendingRef.current, ...patch }; setStatus("dirty"); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(flush, 800); }, [flush] ); // Flush immediately when takeId changes useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, [takeId]); return { status, queue, flush }; } // ─── Field components ───────────────────────────────────────────────────────── function Field({ label, children, className, }: { label: string; children: React.ReactNode; className?: string; }) { return (
{children}
); } function TF({ value, onChange, placeholder, type = "text", className, }: { value: string; onChange: (v: string) => void; placeholder?: string; type?: string; className?: string; }) { return ( onChange(e.target.value)} placeholder={placeholder} className={cn("h-9 bg-zinc-900 border-zinc-700 text-sm", className)} /> ); } function SectionHeader({ title }: { title: string }) { return (
{title}
); } // ─── Main component ─────────────────────────────────────────────────────────── interface Props { take: FullTake; previousTake: FullTake | null; onPrev: (() => void) | null; onNext: (() => void) | null; onDuplicate: () => void; onNewTake: () => void; onDelete: () => void; onAttachmentsChange: () => void; } export function TakeEditorPanel({ take, previousTake, onPrev, onNext, onDuplicate, onNewTake, onDelete, onAttachmentsChange, }: Props) { const { status, queue } = useAutosave(take.id); // Local field state — initialised from take prop, synced when take.id changes const [fields, setFields] = useState(take); const prevIdRef = useRef(take.id); useEffect(() => { if (take.id !== prevIdRef.current) { setFields(take); prevIdRef.current = take.id; } }, [take]); const [showPrevPanel, setShowPrevPanel] = useState(false); function set(key: K, value: FullTake[K]) { setFields((f) => ({ ...f, [key]: value })); queue({ [key]: value }); } const str = (v: string | null) => v ?? ""; const num = (v: number | null) => (v !== null && v !== undefined ? String(v) : ""); // Keyboard shortcuts useEffect(() => { function handleKey(e: KeyboardEvent) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === "ArrowLeft" && onPrev) onPrev(); if (e.key === "ArrowRight" && onNext) onNext(); if (e.key === "d" && !e.metaKey && !e.ctrlKey) onDuplicate(); if (e.key === "n" && !e.metaKey && !e.ctrlKey) onNewTake(); } window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [onPrev, onNext, onDuplicate, onNewTake]); return (
{/* ── Nav bar ── */}

Setup {take.setup.shootDay.unit} · Take {take.takeNumber}

{new Date(take.setup.shootDay.date).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short", year: "numeric", })} {take.setup.shootDay.label ? ` · ${take.setup.shootDay.label}` : ""}

{/* Save status */}
{status === "saving" && } {status === "saved" && } {status === "dirty" && } {status === "error" && }
{/* ── Quality bar ── */}
{QUALITIES.map((q) => ( ))}
{/* ── Scrollable form ── */}
{/* General */}
set("scene", v || null)} placeholder="e.g. 12" /> set("shotLabel", v || null)} placeholder="e.g. 12A" /> set("unitLabel", v || null)} placeholder="e.g. A" /> {}} placeholder="—" className="opacity-60 cursor-default" />
{/* Camera */}
set("cameraLetter", v || null)} placeholder="A" /> set("clipName", v || null)} placeholder="A001_C001" /> set("roll", v || null)} placeholder="A001" /> set("cameraModel", v || null)} placeholder="Alexa Mini LF" /> set("resolution", v || null)} placeholder="4.5K" /> set("codec", v || null)} placeholder="ARRIRAW" /> set("fps", v ? Number(v) : null)} placeholder="24" /> set("shutter", v || null)} placeholder="180°" /> set("iso", v ? Number(v) : null)} placeholder="800" /> set("whiteBalance", v ? Number(v) : null)} placeholder="5600" /> set("colourSpace", v || null)} placeholder="LogC3 AWG3" />
{/* Lens */}
set("lensSet", v || null)} placeholder="Cooke S4" /> set("lens", v || null)} placeholder="50mm" /> set("tStop", v || null)} placeholder="T2.8" /> set("filters", v || null)} placeholder="IRND 0.6" />
{/* Tracking */}
{TRACKING_ITEMS.map(({ key, label }) => { const checked = fields[key] as boolean; return ( ); })}
{/* Environment */}
set("weather", v || null)} placeholder="Overcast" /> set("sunDirection", v || null)} placeholder="NW high" />