From 42e614c59291dc994d4e04982d593a1c70171c09 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:26:52 +0200 Subject: [PATCH] feat: link takes to pipeline shots via dropdown selector --- app/api/shoot-log/shots/route.ts | 34 ++++++++ app/api/shoot-log/takes/[takeId]/route.ts | 1 + components/shoot-log/TakeEditorPanel.tsx | 101 +++++++++++++++++++++- 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 app/api/shoot-log/shots/route.ts diff --git a/app/api/shoot-log/shots/route.ts b/app/api/shoot-log/shots/route.ts new file mode 100644 index 0000000..df7ad3b --- /dev/null +++ b/app/api/shoot-log/shots/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { db } from "@/lib/db"; + +function requireRole(role: string) { + return ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(role); +} + +// GET /api/shoot-log/shots?projectId=xxx +// Returns a lightweight shot list for the pipeline-link selector +export async function GET(req: NextRequest) { + const session = await auth(); + if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + if (!requireRole(session.user.role)) + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + + const projectId = req.nextUrl.searchParams.get("projectId"); + if (!projectId) return NextResponse.json({ error: "projectId required" }, { status: 400 }); + + const shots = await db.shot.findMany({ + where: { projectId }, + select: { + id: true, + shotCode: true, + scene: true, + episode: true, + description: true, + status: true, + }, + orderBy: [{ episode: "asc" }, { shotCode: "asc" }], + }); + + return NextResponse.json({ shots }); +} diff --git a/app/api/shoot-log/takes/[takeId]/route.ts b/app/api/shoot-log/takes/[takeId]/route.ts index aed6643..a5cc10c 100644 --- a/app/api/shoot-log/takes/[takeId]/route.ts +++ b/app/api/shoot-log/takes/[takeId]/route.ts @@ -21,6 +21,7 @@ function sanitize(data: Record) { "hasSurvey","hasLidar","hasWitnessCamera","hasLensGrid","hasTexturePhotos","hasPhotogrammetry", "weather","sunDirection","artificialLights", "supervisorNotes","continuityNotes","vfxRequirements","quality", + "pipelineShotId", ]); return Object.fromEntries( Object.entries(data) diff --git a/components/shoot-log/TakeEditorPanel.tsx b/components/shoot-log/TakeEditorPanel.tsx index e8b85a8..1374b27 100644 --- a/components/shoot-log/TakeEditorPanel.tsx +++ b/components/shoot-log/TakeEditorPanel.tsx @@ -1,9 +1,10 @@ "use client"; import { useEffect, useRef, useState, useCallback } from "react"; +import { useQuery } from "@tanstack/react-query"; import { ChevronLeft, ChevronRight, Copy, Plus, Check, Loader2, - Trash2, AlertCircle, ChevronDown, ChevronUp, + Trash2, AlertCircle, ChevronDown, ChevronUp, Link2, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -29,6 +30,7 @@ export interface FullTake { id: string; setupId: string; takeNumber: number; + pipelineShotId: string | null; scene: string | null; shotLabel: string | null; unitLabel: string | null; @@ -199,6 +201,96 @@ function SectionHeader({ title }: { title: string }) { ); } +// ─── Shot selector ──────────────────────────────────────────────────────────── + +interface ShotOption { + id: string; + shotCode: string; + scene: string | null; + episode: string | null; +} + +function ShotSelector({ + projectId, + value, + onChange, +}: { + projectId: string; + value: string | null; + onChange: (id: string | null) => void; +}) { + const [search, setSearch] = useState(""); + + const { data, isLoading } = useQuery<{ shots: ShotOption[] }>({ + queryKey: ["project-shots", projectId], + queryFn: async () => { + const res = await fetch(`/api/shoot-log/shots?projectId=${projectId}`); + if (!res.ok) throw new Error("Failed"); + return res.json(); + }, + staleTime: 60_000, + enabled: !!projectId, + }); + + const shots = data?.shots ?? []; + const filtered = search + ? shots.filter( + (s) => + s.shotCode.toLowerCase().includes(search.toLowerCase()) || + (s.scene ?? "").toLowerCase().includes(search.toLowerCase()) || + (s.episode ?? "").toLowerCase().includes(search.toLowerCase()) + ) + : shots; + + const selected = shots.find((s) => s.id === value); + + return ( +
+ setSearch(e.target.value)} + className="h-8 text-xs bg-zinc-900 border-zinc-700" + /> +
+ + {value && ( + + )} +
+ {selected && ( +
+ + + Linked to {selected.shotCode} + {selected.episode ? ` · Ep ${selected.episode}` : ""} + +
+ )} +
+ ); +} + // ─── Main component ─────────────────────────────────────────────────────────── interface Props { @@ -342,6 +434,13 @@ export function TakeEditorPanel({ {}} placeholder="—" className="opacity-60 cursor-default" /> + + set("pipelineShotId", id)} + /> + {/* Camera */}