"use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { GripVertical } from "lucide-react"; import { ImageCard, type CardImage } from "@/components/grid/ImageCard"; import { reorderCollectionImage } from "@/app/actions/collections"; import { cn } from "@/lib/utils"; /** * Drag-and-drop reorderable grid. Wraps each ImageCard in a draggable * container; on drop, computes which image should be placed before * which (or at the end) and calls the server action. The grid uses the * same column count as the regular MasonryGrid so visuals match. */ export function ReorderableCollectionGrid({ images, collectionId, view = "landscape", }: { images: CardImage[]; collectionId: number; view?: "portrait" | "landscape"; }) { // Local state mirrors the prop so we can apply optimistic reordering // before the server round-trips. Drift is reconciled when the page // re-fetches via router.refresh(). const [items, setItems] = useState(images); const [draggingId, setDraggingId] = useState(null); const [dropBeforeId, setDropBeforeId] = useState(null); const [, start] = useTransition(); const router = useRouter(); if (images.length === 0) return null; // Reconcile when the prop changes (e.g. after a refresh). if (items.length !== images.length || items.some((it, i) => it.id !== images[i]?.id)) { setItems(images); } const cols = view === "portrait" ? "6" : "var(--grid-cols, 3)"; function onDragStart(id: number, e: React.DragEvent) { setDraggingId(id); e.dataTransfer.effectAllowed = "move"; // Firefox needs setData to actually begin drag. e.dataTransfer.setData("text/plain", String(id)); } function onDragOverCard(id: number, e: React.DragEvent) { if (draggingId == null) return; e.preventDefault(); setDropBeforeId(id); } function onDragOverEnd(e: React.DragEvent) { if (draggingId == null) return; e.preventDefault(); setDropBeforeId("end"); } function commitDrop() { if (draggingId == null || dropBeforeId == null) { setDraggingId(null); setDropBeforeId(null); return; } const movedId = draggingId; const beforeId = dropBeforeId === "end" ? null : dropBeforeId; if (movedId === beforeId) { setDraggingId(null); setDropBeforeId(null); return; } // Optimistic local reorder. setItems((cur) => { const fromIdx = cur.findIndex((x) => x.id === movedId); if (fromIdx === -1) return cur; const moved = cur[fromIdx]; const without = [...cur.slice(0, fromIdx), ...cur.slice(fromIdx + 1)]; let toIdx = beforeId == null ? without.length : without.findIndex((x) => x.id === beforeId); if (toIdx === -1) toIdx = without.length; return [...without.slice(0, toIdx), moved, ...without.slice(toIdx)]; }); setDraggingId(null); setDropBeforeId(null); start(async () => { await reorderCollectionImage(collectionId, movedId, beforeId); router.refresh(); }); } function onDragEnd() { setDraggingId(null); setDropBeforeId(null); } return (
{ // Required for the drop event to fire on this container at all // — without preventDefault on dragover, browsers treat the area // as a non-drop-target. if (draggingId != null) e.preventDefault(); }} onDrop={commitDrop} onDragEnd={onDragEnd} > {items.map((img) => (
onDragStart(img.id, e)} onDragOver={(e) => onDragOverCard(img.id, e)} onDragEnd={onDragEnd} className={cn( "relative group/drag", draggingId === img.id && "opacity-40", dropBeforeId === img.id && "ring-2 ring-[var(--color-cyan)] ring-offset-2 ring-offset-[var(--color-bg-0)] rounded-2xl", )} >
))} {/* Sentinel drop zone for "end of list" */}
{draggingId != null ? "Drop here for end of list" : ""}
); }