Files

75 lines
3.0 KiB
TypeScript

"use client"
import React, { useMemo } from 'react'
import type { Section } from '@/lib/state/store'
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
function SortableItem({ id, children, testId }: { id: string; children: React.ReactNode; testId?: string }) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<li ref={setNodeRef} style={style} {...attributes} {...listeners} data-testid={testId} className="flex items-center justify-between border rounded px-3 py-2">
{children}
</li>
)
}
export default function SectionCanvas({
sections,
onMove,
onRemove,
onSelect,
}: {
sections: Section[]
onMove: (from: number, to: number) => void
onRemove: (index: number) => void
onSelect?: (index: number) => void
}) {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
const ids = useMemo(() => sections.map((_, i) => `sec-${i}`), [sections])
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over) return
if (active.id === over.id) return
const from = ids.indexOf(String(active.id))
const to = ids.indexOf(String(over.id))
if (from !== -1 && to !== -1) onMove(from, to)
}
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
<ul className="space-y-1" data-testid="section-canvas">
{sections.map((s, i) => (
<SortableItem id={`sec-${i}`} key={`sec-${i}`} testId={`sec-item-${i}`}>
<span className="text-sm">{s.type}</span>
<div className="flex items-center gap-2">
{onSelect && (
<button aria-label={`Select ${i}`} title={`Select ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => onSelect(i)}>
Select
</button>
)}
<button aria-label={`Up ${i}`} title={`Up ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => i > 0 && onMove(i, i - 1)}>
Up
</button>
<button aria-label={`Down ${i}`} title={`Down ${i}`} className="text-sm border rounded px-2 py-1" onClick={() => i < sections.length - 1 && onMove(i, i + 1)}>
Down
</button>
<button aria-label={`Remove ${i}`} title={`Remove ${i}`} className="text-sm text-red-600 border rounded px-2 py-1" onClick={() => onRemove(i)}>
Remove
</button>
</div>
</SortableItem>
))}
</ul>
</SortableContext>
</DndContext>
)
}