Case board
The open cases in stage columns, reported to verifying; dragging a card moves the case to that stage of its kind's pipeline. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add badge card @reui/kanbanInstall the glue
Install the block with
npx better-supabase add cases. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the examplecase-board.tsx /** * The organization's open cases in a column for their stage. Dragging a card to * another column moves the case there; a failed move puts it back. The parent * keys it by the cards' stages, so a refresh starts it from the saved board. */ export function CaseBoard({ data, canMove, }: { data: CaseBoardData; canMove: boolean; }) { const errorMessage = useErrorMessage(); const label = useStageLabel(); const [columns, setColumns] = useState(() => columnsOf(data.cards)); const move = useAction(moveCase); const byId = new Map(data.cards.map((card) => [card.id, card])); return ( <div data-recipe="case-board"> <Kanban value={columns} onValueChange={setColumns} getItemValue={(card) => card.id} onValueCommit={(_value, meta) => { if (meta.kind !== "item") { return; } const from = v.parse(StageSchema, meta.activeContainer); const to = v.parse(StageSchema, meta.overContainer); const card = byId.get(String(meta.event.active.id)); const stageId = card === undefined ? undefined : data.stages[card.pipelineId]?.[to]; if (from === to || card === undefined || stageId === undefined) { setColumns(meta.previousValue); return; } void move.run({ caseId: card.id, stageId }).then((result) => { if (!result.ok) { setColumns(meta.previousValue); toast.error(errorMessage(result.error)); } return result; }); }} > <KanbanBoard className="flex overflow-x-auto"> {OPEN_STAGES.map((stage) => { const column = columns[stage] ?? []; return ( <KanbanColumn key={stage} value={stage} className="w-72 shrink-0 gap-3 rounded-lg bg-muted/50 p-3" > <div className="flex items-center justify-between gap-2"> <span className="text-sm font-medium">{label(stage)}</span> <Badge variant="outline">{column.length}</Badge> </div> <KanbanColumnContent value={stage} className="min-h-16"> {column.map((card) => ( <KanbanItem key={card.id} value={card.id} disabled={!canMove || move.pending} > <KanbanItemHandle cursor={canMove}> <CaseItem card={card} /> </KanbanItemHandle> </KanbanItem> ))} </KanbanColumnContent> </KanbanColumn> ); })} </KanbanBoard> <KanbanOverlay> {({ value, variant }) => { const card = variant === "item" ? byId.get(String(value)) : undefined; return card ? <CaseItem card={card} /> : null; }} </KanbanOverlay> </Kanban> </div> ); }