import { useRef, useState } from 'react'; import { GripVertical, Plus, Trash2 } from 'lucide-react'; import { Link } from 'react-router'; import { Button } from '@/components/ui/button'; import { href } from '../routes'; import { CHOICES } from './AddComponentDialog'; import type { CanvasComponent, CanvasSection } from '../canvas'; /** * What is in one band, column by column. * * ── One list, two places ── * * The page's structure nests this under every band, and the band's own editor shows just its own. * They are the same list of the same things with the same actions, so they are the same component — * the carousels panel taught that lesson once already. * * ── Columns, because adding needs one ── * * A block goes IN a column, so a flat list cannot express adding one. A one-column band shows a single * button and never has to ask a question it has no answer to. * * A two-column band NAMES its columns. It used to leave them unlabelled and put the side on the * buttons — "Add left", "Add right" — and that reads as a flat list of blocks with two odd buttons * under it: a band whose only block was on the left looked like a band with nothing on the left and * two ways to fix it. The side belongs on the GROUP, which is the thing that has one; the buttons then * say what they do, and an empty column is visible as an empty column rather than inferred from the * absence of something. * * ── Reorder is within a column ── * * The write sends the column alongside the ids, and the query sets both — so dragging a block from one * column to the other is the same call, on the day the interface offers it. It does not yet: a list * per column has no gesture for crossing between them, and inventing one here would be guessing. */ export const BandBlocks = ({ slug, section, busy, onAdd, onDelete, onReorder, }: { slug: string; section: CanvasSection; busy: boolean; onAdd: (section: CanvasSection, column: number) => void; onDelete: (block: CanvasComponent) => void; onReorder: (section: CanvasSection, column: number, ids: number[]) => void; }) => { const [dragged, setDragged] = useState(null); const [order, setOrder] = useState(null); const pending = useRef(null); const columns = section.layout === 'two' ? 2 : 1; const blocksIn = (column: number) => (order ?? section.components).filter((block) => block.column === column); const moveOver = (event: React.DragEvent, over: CanvasComponent) => { if (dragged === null) return; const all = order ?? section.components; const from = all.findIndex((one) => one.id === dragged); // Within a column only — see the note above. A drop on the other column is simply not accepted. if (from < 0 || all[from]!.column !== over.column) return; event.preventDefault(); if (dragged === over.id) return; const next = [...all]; const to = next.findIndex((one) => one.id === over.id); next.splice(to, 0, next.splice(from, 1)[0]!); pending.current = next; setOrder(next); }; const commit = (column: number) => { const next = pending.current; setDragged(null); pending.current = null; // Dropped where it started, or outside the list. Nothing moved, so nothing is written. if (!next) return; setOrder(null); onReorder(section, column, next.filter((one) => one.column === column).map((one) => one.id)); }; return (
{Array.from({ length: columns }, (_, column) => (
{columns === 2 && ( {column === 0 ? 'Left' : 'Right'} )} {blocksIn(column).map((block) => ( moveOver(event, block)} onDrop={() => commit(column)} className={`flex items-center gap-1 ${dragged === block.id ? 'opacity-40' : ''}`} > { setDragged(block.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(block.id)); }} onDragEnd={() => commit(column)} className="shrink-0 cursor-grab pl-1 text-muted-foreground/50 active:cursor-grabbing" aria-label="Reorder" > {CHOICES.find((choice) => choice.kind === block.kind)?.label ?? block.kind} ))}
))}
); };