reference support
Deploy / deploy (push) Successful in 3m38s

This commit is contained in:
twotalesanimation
2026-07-12 10:27:46 +02:00
parent 42e614c592
commit c09d06b6c7
6 changed files with 413 additions and 3 deletions
+85 -1
View File
@@ -4,8 +4,9 @@ import { useEffect, useRef, useState, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import {
ChevronLeft, ChevronRight, Copy, Plus, Check, Loader2,
Trash2, AlertCircle, ChevronDown, ChevronUp, Link2, X,
Trash2, AlertCircle, ChevronDown, ChevronUp, Link2, X, BookImage,
} from "lucide-react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -201,6 +202,86 @@ function SectionHeader({ title }: { title: string }) {
);
}
// ─── Linked shot panel ────────────────────────────────────────────────────────
interface LinkedShot {
id: string;
shotCode: string;
scene: string | null;
episode: string | null;
thumbnailUrl: string | null;
references: { id: string; fileUrl: string; fileName: string; label: string | null }[];
}
function LinkedShotPanel({ shotId }: { shotId: string }) {
const { data, isLoading } = useQuery<{ shot: LinkedShot }>({
queryKey: ["linked-shot", shotId],
queryFn: async () => {
const res = await fetch(`/api/shoot-log/shots/${shotId}`);
if (!res.ok) throw new Error("Failed");
return res.json();
},
staleTime: 60_000,
enabled: !!shotId,
});
if (isLoading) {
return (
<div className="flex items-center gap-2 text-xs text-zinc-500 mt-2">
<Loader2 className="h-3 w-3 animate-spin" /> Loading shot data
</div>
);
}
const shot = data?.shot;
if (!shot) return null;
const hasContent = shot.thumbnailUrl || shot.references.length > 0;
if (!hasContent) return null;
return (
<div className="mt-3 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3 space-y-3">
<div className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-zinc-500">
<BookImage className="h-3 w-3" />
Shot Reference {shot.shotCode}
</div>
{shot.thumbnailUrl && (
<div className="relative w-full max-w-xs aspect-[2.39] rounded-md overflow-hidden border border-zinc-700">
<Image
src={shot.thumbnailUrl}
alt={shot.shotCode}
fill
className="object-cover"
sizes="320px"
/>
</div>
)}
{shot.references.length > 0 && (
<div className="grid grid-cols-4 gap-2">
{shot.references.map((ref) => (
<div key={ref.id} className="relative aspect-square rounded-md overflow-hidden border border-zinc-700 bg-zinc-950 group">
<Image
src={ref.fileUrl}
alt={ref.label ?? ref.fileName}
fill
className="object-cover"
sizes="100px"
/>
{ref.label && (
<div className="absolute bottom-0 inset-x-0 bg-black/70 text-[9px] text-zinc-300 truncate px-1 py-0.5">
{ref.label}
</div>
)}
</div>
))}
</div>
)}
</div>
);
}
// ─── Shot selector ────────────────────────────────────────────────────────────
interface ShotOption {
@@ -440,6 +521,9 @@ export function TakeEditorPanel({
value={fields.pipelineShotId}
onChange={(id) => set("pipelineShotId", id)}
/>
{fields.pipelineShotId && (
<LinkedShotPanel shotId={fields.pipelineShotId} />
)}
</Field>
{/* Camera */}