@@ -213,7 +213,88 @@ interface LinkedShot {
|
||||
references: { id: string; fileUrl: string; fileName: string; label: string | null }[];
|
||||
}
|
||||
|
||||
interface LightboxItem {
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function ShotLightbox({
|
||||
items,
|
||||
index,
|
||||
onClose,
|
||||
}: {
|
||||
items: LightboxItem[];
|
||||
index: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [current, setCurrent] = useState(index);
|
||||
const item = items[current];
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "ArrowLeft") setCurrent((c) => Math.max(0, c - 1));
|
||||
if (e.key === "ArrowRight") setCurrent((c) => Math.min(items.length - 1, c + 1));
|
||||
}
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [items.length, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/90 flex flex-col items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<button
|
||||
className="absolute top-4 right-4 text-white/70 hover:text-white p-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="max-w-[90vw] max-h-[80vh] overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={item.url}
|
||||
alt={item.label}
|
||||
className="max-w-full max-h-[80vh] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-sm text-zinc-300">{item.label}</p>
|
||||
|
||||
{items.length > 1 && (
|
||||
<div className="flex gap-4 mt-4" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
disabled={current === 0}
|
||||
onClick={() => setCurrent((c) => c - 1)}
|
||||
className="px-4 py-2 rounded bg-zinc-800 text-zinc-300 disabled:opacity-30 hover:bg-zinc-700 text-sm"
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
<span className="text-zinc-500 text-sm self-center">
|
||||
{current + 1} / {items.length}
|
||||
</span>
|
||||
<button
|
||||
disabled={current === items.length - 1}
|
||||
onClick={() => setCurrent((c) => c + 1)}
|
||||
className="px-4 py-2 rounded bg-zinc-800 text-zinc-300 disabled:opacity-30 hover:bg-zinc-700 text-sm"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkedShotPanel({ shotId }: { shotId: string }) {
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
|
||||
const { data, isLoading } = useQuery<{ shot: LinkedShot }>({
|
||||
queryKey: ["linked-shot", shotId],
|
||||
queryFn: async () => {
|
||||
@@ -239,7 +320,23 @@ function LinkedShotPanel({ shotId }: { shotId: string }) {
|
||||
const hasContent = shot.thumbnailUrl || shot.references.length > 0;
|
||||
if (!hasContent) return null;
|
||||
|
||||
// Build a flat list of lightbox items: thumbnail first, then references
|
||||
const lightboxItems: LightboxItem[] = [
|
||||
...(shot.thumbnailUrl
|
||||
? [{ url: shot.thumbnailUrl, label: `${shot.shotCode} — Thumbnail` }]
|
||||
: []),
|
||||
...shot.references.map((ref) => ({
|
||||
url: ref.fileUrl,
|
||||
label: ref.label ?? ref.fileName,
|
||||
})),
|
||||
];
|
||||
|
||||
// Index offsets: thumbnail is 0 (if present), refs start at offset
|
||||
const thumbLightboxIndex = shot.thumbnailUrl ? 0 : null;
|
||||
const refLightboxOffset = shot.thumbnailUrl ? 1 : 0;
|
||||
|
||||
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" />
|
||||
@@ -247,28 +344,37 @@ function LinkedShotPanel({ shotId }: { shotId: string }) {
|
||||
</div>
|
||||
|
||||
{shot.thumbnailUrl && (
|
||||
<div className="relative w-full max-w-xs aspect-[2.39] rounded-md overflow-hidden border border-zinc-700">
|
||||
<div
|
||||
className="relative w-full max-w-xs aspect-[2.39] rounded-md overflow-hidden border border-zinc-700 cursor-zoom-in group"
|
||||
onClick={() => setLightboxIndex(thumbLightboxIndex!)}
|
||||
>
|
||||
<Image
|
||||
src={shot.thumbnailUrl}
|
||||
alt={shot.shotCode}
|
||||
fill
|
||||
className="object-cover"
|
||||
className="object-cover transition-transform group-hover:scale-105"
|
||||
sizes="320px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||
</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">
|
||||
{shot.references.map((ref, i) => (
|
||||
<div
|
||||
key={ref.id}
|
||||
className="relative aspect-square rounded-md overflow-hidden border border-zinc-700 bg-zinc-950 cursor-zoom-in group"
|
||||
onClick={() => setLightboxIndex(refLightboxOffset + i)}
|
||||
>
|
||||
<Image
|
||||
src={ref.fileUrl}
|
||||
alt={ref.label ?? ref.fileName}
|
||||
fill
|
||||
className="object-cover"
|
||||
className="object-cover transition-transform group-hover:scale-105"
|
||||
sizes="100px"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||
{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}
|
||||
@@ -279,6 +385,15 @@ function LinkedShotPanel({ shotId }: { shotId: string }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{lightboxIndex !== null && (
|
||||
<ShotLightbox
|
||||
items={lightboxItems}
|
||||
index={lightboxIndex}
|
||||
onClose={() => setLightboxIndex(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user