import { useRef, useState } from 'react'; import { GripVertical, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Link } from 'react-router'; import { href } from '../routes'; import type { CanvasComponent, CanvasSection } from '../canvas'; import { BandBlocks } from './BandBlocks'; /** * The page, as a list of its bands. * * ── Why the editor and not the nav ── * * The nav lists the same bands and is for GOING somewhere: press one and the preview scrolls to it. * This is for changing what the page is made of — adding a band, removing one, putting them in a * different order. Two different verbs on the same nouns, and mixing them would mean a nav row that * sometimes navigates and sometimes deletes. * * It replaced a bar painted onto the page itself, which carried the same three actions and made the * preview stop being a preview. See the note in canvas.ts. * * ── The gesture is the one used everywhere else ── * * Grip on the left, the row is the drop target, the list rearranges as the pointer crosses it, one * write at the end carrying every id. Pages, gallery items and partners all do this; a fourth way to * reorder something would be a fourth thing to learn. */ export const PageStructure = ({ slug, sections, busy, onAdd, onDelete, onReorder, onAddComponent, onDeleteComponent, onReorderComponents, }: { /** For the links: pressing a band still takes you to it. */ slug: string; sections: CanvasSection[]; busy: boolean; onAdd: () => void; onDelete: (section: CanvasSection) => void; onReorder: (ids: number[]) => void; /** A block needs a COLUMN to go in, which is the whole reason blocks are nested here. */ onAddComponent: (section: CanvasSection, column: number) => void; onDeleteComponent: (component: CanvasComponent) => void; onReorderComponents: (section: CanvasSection, column: number, ids: number[]) => void; }) => { const [dragged, setDragged] = useState(null); const [order, setOrder] = useState(null); const pending = useRef(null); // The live order while dragging, or the real one. Reset whenever the page hands over a new list. const rows = order ?? sections; const moveOver = (event: React.DragEvent, overId: number) => { if (dragged === null) return; event.preventDefault(); if (dragged === overId) return; const next = [...rows]; 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]!); pending.current = next; setOrder(next); }; const commit = () => { 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(next.map((one) => one.id)); }; return (
Sections
    {rows.map((section) => (
  • moveOver(event, section.id)} onDrop={commit} className={`flex flex-col rounded border border-border/60 ${ dragged === section.id ? 'opacity-40' : '' }`} >
    { setDragged(section.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(section.id)); }} onDragEnd={commit} className="shrink-0 cursor-grab py-1.5 pl-1.5 text-muted-foreground/60 active:cursor-grabbing" aria-label={`Reorder ${section.name}`} > {/* Still a link: pressing a band is how you get to it, here as in the nav. */} {section.name} {section.layout === 'two' ? 'Two columns' : 'One column'} ·{' '} {section.components.length} block{section.components.length === 1 ? '' : 's'}
  • ))}
); };