From 54141726d60397605b5a1f29981df26d855dae80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 1 Sep 2026 20:39:38 +0100 Subject: [PATCH] gallery items reorder by dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine items meant eighteen arrows and four presses to move something from the bottom to the top. A grip on the left, the row as the drop target, and the list rearranging as the pointer crosses it — the same gesture the page rows use. One write at the end, carrying every id in order. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/GalleryItems.tsx | 83 +++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/web/sections/GalleryItems.tsx b/web/sections/GalleryItems.tsx index 984c63a..601bad2 100644 --- a/web/sections/GalleryItems.tsx +++ b/web/sections/GalleryItems.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react'; +import { GripVertical, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useClient } from 'hooks/useClient'; @@ -69,15 +69,44 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh } }; - /** Every id, in order — which is what makes two items claiming a position impossible. */ - const move = (index: number, by: -1 | 1) => { - if (!gallery) return; + /* + Reordering by dragging, the same way the page rows do it. + + The list rearranges as the pointer crosses a row rather than on drop, so what you see while + dragging is what you will get. The write happens once, at the end, and it sends EVERY id in order — + which is what makes two items claiming a position impossible. + + It replaced a pair of arrows per row. Nine items meant eighteen buttons and four presses to move + something from the bottom to the top. + */ + const [dragged, setDragged] = useState(null); + const orderedRef = useRef(null); + + const moveOver = (event: React.DragEvent, overId: number) => { + if (dragged === null || !gallery) return; + // Without this the drop is refused and the browser animates the row back to where it started. + event.preventDefault(); + if (dragged === overId) return; + const next = [...gallery.items]; - const target = index + by; - if (target < 0 || target >= next.length) return; - [next[index], next[target]] = [next[target]!, next[index]!]; + const from = next.findIndex((one) => one.id === dragged); + const to = next.findIndex((one) => one.id === overId); + if (from < 0 || to < 0) return; + + next.splice(to, 0, next.splice(from, 1)[0]!); + orderedRef.current = next; + setGallery({ ...gallery, items: next }); + }; + + const commitOrder = () => { + const ordered = orderedRef.current; + setDragged(null); + orderedRef.current = null; + // Dropped where it started, or outside the list. Nothing moved, so nothing is written. + if (!ordered) return; + void send(() => - clientRef.current.put(`/fofos-admin/galleries/${galleryId}/items/order`, { ids: next.map((one) => one.id) }), + clientRef.current.put(`/fofos-admin/galleries/${galleryId}/items/order`, { ids: ordered.map((one) => one.id) }), ); }; @@ -93,8 +122,30 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
    - {gallery.items.map((item, index) => ( -
  • + {gallery.items.map((item) => ( +
  • moveOver(event, item.id)} + onDrop={commitOrder} + className={`flex items-center gap-2 rounded border border-border/60 p-1.5 ${ + dragged === item.id ? 'opacity-40' : '' + }`} + > + { + setDragged(item.id); + // Firefox starts no drag at all without data on the transfer. The value is unused. + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', String(item.id)); + }} + onDragEnd={commitOrder} + className="shrink-0 cursor-grab text-muted-foreground/60 active:cursor-grabbing" + aria-label="Reorder" + > + + {/* From the DERIVED ladder, which is the whole point of the pipeline: most of these are HEIC and a browser cannot show the original at all. @@ -133,18 +184,6 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh - -