"use client"; import { useState, useCallback } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Clapperboard, Loader2 } from "lucide-react"; import { TakeListPanel } from "./TakeListPanel"; import { TakeEditorPanel, type FullTake } from "./TakeEditorPanel"; import { NewShootDayDialog } from "./NewShootDayDialog"; import { NewSetupDialog } from "./NewSetupDialog"; import { cn } from "@/lib/utils"; // ─── Types ──────────────────────────────────────────────────────────────────── interface Project { id: string; name: string; code: string; } // ─── Component ──────────────────────────────────────────────────────────────── interface Props { projects: Project[]; } export function ShootLogClient({ projects }: Props) { const queryClient = useQueryClient(); const [projectId, setProjectId] = useState(projects[0]?.id ?? ""); const [selectedTakeId, setSelectedTakeId] = useState(null); const [selectedSetupId, setSelectedSetupId] = useState(null); // Controls which panel is visible on mobile (< md) const [mobileView, setMobileView] = useState<"list" | "editor">("list"); // Which day to auto-expand in the left panel after creation const [expandDayId, setExpandDayId] = useState(null); // Dialog state const [newDayOpen, setNewDayOpen] = useState(false); const [newSetupDayId, setNewSetupDayId] = useState(null); // ── Invalidate the days list ─────────────────────────────────────────────── const invalidateDays = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["shoot-log-days", projectId] }); }, [queryClient, projectId]); // ── Fetch selected take ──────────────────────────────────────────────────── const { data: takeData, isLoading: takeLoading } = useQuery<{ take: FullTake }>({ queryKey: ["take", selectedTakeId], queryFn: async () => { const res = await fetch(`/api/shoot-log/takes/${selectedTakeId}`); if (!res.ok) throw new Error("Failed to load take"); return res.json(); }, enabled: !!selectedTakeId, staleTime: 0, }); const take = takeData?.take ?? null; // ── Fetch previous take (same setup, takeNumber - 1) ────────────────────── const prevTakeId = take?.setup.takes.find( (t) => t.takeNumber === (take?.takeNumber ?? 0) - 1 )?.id; const { data: prevTakeData } = useQuery<{ take: FullTake }>({ queryKey: ["take", prevTakeId], queryFn: async () => { const res = await fetch(`/api/shoot-log/takes/${prevTakeId}`); if (!res.ok) throw new Error("Failed to load previous take"); return res.json(); }, enabled: !!prevTakeId, staleTime: 60_000, }); const previousTake = prevTakeData?.take ?? null; // ── Navigation ───────────────────────────────────────────────────────────── const allTakesInSetup = take?.setup.takes ?? []; const currentIdx = allTakesInSetup.findIndex((t) => t.id === selectedTakeId); const prevTakeNav = currentIdx > 0 ? allTakesInSetup[currentIdx - 1] : null; const nextTakeNav = currentIdx < allTakesInSetup.length - 1 ? allTakesInSetup[currentIdx + 1] : null; // ── Create a take ────────────────────────────────────────────────────────── const createTake = useCallback( async (setupId: string, initialData?: object) => { const res = await fetch(`/api/shoot-log/setups/${setupId}/takes`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(initialData ?? {}), }); if (!res.ok) throw new Error("Failed to create take"); const { take: newTake } = await res.json(); invalidateDays(); setSelectedTakeId(newTake.id); setSelectedSetupId(setupId); setMobileView("editor"); }, [invalidateDays] ); // ── After shoot day creation: auto-create Setup A + Take 1 ──────────────── const handleDayCreated = useCallback( async (day: { id: string }) => { try { // Create Setup A const setupRes = await fetch(`/api/shoot-log/days/${day.id}/setups`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "A" }), }); if (!setupRes.ok) throw new Error(); const { setup } = await setupRes.json(); // Create Take 1 const takeRes = await fetch(`/api/shoot-log/setups/${setup.id}/takes`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); if (!takeRes.ok) throw new Error(); const { take: newTake } = await takeRes.json(); invalidateDays(); setExpandDayId(day.id); setSelectedTakeId(newTake.id); setSelectedSetupId(setup.id); setMobileView("editor"); } catch { // Day was created but setup/take failed — still refresh invalidateDays(); setExpandDayId(day.id); } }, [invalidateDays] ); // ── After new setup creation ─────────────────────────────────────────────── const handleSetupCreated = useCallback( async (setup: { id: string }) => { invalidateDays(); setNewSetupDayId(null); await createTake(setup.id); }, [invalidateDays, createTake] ); // ── Duplicate take ───────────────────────────────────────────────────────── const handleDuplicate = useCallback(async () => { if (!take) return; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { id, takeNumber, attachments, setup, createdById, createdAt, updatedAt, ...rest } = take as FullTake & Record; const clipName = incrementClipName(take.clipName); const initial = { ...rest, clipName, supervisorNotes: null, continuityNotes: null, vfxRequirements: null }; await createTake(take.setupId, initial); }, [take, createTake]); // ── Delete take ──────────────────────────────────────────────────────────── const handleDelete = useCallback(async () => { if (!selectedTakeId) return; if (!confirm("Delete this take? This cannot be undone.")) return; await fetch(`/api/shoot-log/takes/${selectedTakeId}`, { method: "DELETE" }); setSelectedTakeId(null); setMobileView("list"); invalidateDays(); }, [selectedTakeId, invalidateDays]); // ── Render ───────────────────────────────────────────────────────────────── if (!projectId) { return (

No active projects found.

); } return ( <> {/* Project selector (top bar) */}
Shot Log
{/* Keyboard hint */}
← → navigate D duplicate N new take
{/* Split layout */}
{/* Left panel — full width on mobile, fixed sidebar on md+ */}
{ setSelectedTakeId(takeId); setSelectedSetupId(setupId); setMobileView("editor"); }} onNewDay={() => setNewDayOpen(true)} onNewSetup={(dayId) => setNewSetupDayId(dayId)} onNewTake={(setupId) => createTake(setupId)} />
{/* Right panel — hidden on mobile when list is showing */}
{takeLoading ? (
) : take ? ( setSelectedTakeId(prevTakeNav.id) : null} onNext={nextTakeNav ? () => setSelectedTakeId(nextTakeNav.id) : null} onDuplicate={handleDuplicate} onNewTake={() => createTake(take.setupId)} onDelete={handleDelete} onAttachmentsChange={invalidateDays} onBack={() => setMobileView("list")} /> ) : (

Select a take or create a new shoot day

)}
{/* Dialogs */} {newSetupDayId && ( { if (!o) setNewSetupDayId(null); }} onCreated={handleSetupCreated} /> )} ); } /** Increment trailing number in clip name: A001_C002 → A001_C003 */ function incrementClipName(clipName: string | null | undefined): string | null { if (!clipName) return null; const match = clipName.match(/^(.*?)(\d+)$/); if (!match) return clipName; const [, prefix, numStr] = match; const next = String(Number(numStr) + 1).padStart(numStr.length, "0"); return `${prefix}${next}`; }