Kanban board
Sortable columns with drag-and-drop cards. Uses dnd-kit.
Preview
Applied
2JD
Jane Doe
Counselor, age 21
AC
Alex Chen
Lifeguard, age 19
Interview
1PP
Priya Patel
Art specialist, age 22
Ready to hire
2SO
Sam O'Brien
Counselor, age 20
MS
Maya Silva
Waterfront, age 23
Placed
1TR
Tomás Ruiz
Counselor, age 21
Code
"use client"
// Thin demo wiring over the library's Kanban primitives.
// Copy this whole file, rewrite columns/seed/ParticipantCardBody for your
// data shape, and adapt the drag handlers to persist to your backend.
import { useState } from "react"
import type {
DragEndEvent,
DragOverEvent,
DragStartEvent,
} from "@dnd-kit/core"
import { arrayMove } from "@dnd-kit/sortable"
import {
Avatar,
AvatarBadge,
AvatarFallback,
CountryFlag,
KanbanBoard,
KanbanCard,
KanbanCardHandle,
KanbanColumn,
} from "@smaller-earth/ui"
type Column = { id: string; title: string }
type Participant = {
id: string
columnId: string
name: string
detail: string
countryCode: string
initials: string
}
const columns: Column[] = [
{ id: "applied", title: "Applied" },
{ id: "interview", title: "Interview" },
{ id: "ready", title: "Ready to hire" },
{ id: "placed", title: "Placed" },
]
const seed: Participant[] = [
{ id: "p1", columnId: "applied", name: "Jane Doe", detail: "Counselor, age 21", countryCode: "gb", initials: "JD" },
{ id: "p2", columnId: "applied", name: "Alex Chen", detail: "Lifeguard, age 19", countryCode: "us", initials: "AC" },
{ id: "p3", columnId: "interview", name: "Priya Patel", detail: "Art specialist, age 22", countryCode: "in", initials: "PP" },
{ id: "p4", columnId: "ready", name: "Sam O'Brien", detail: "Counselor, age 20", countryCode: "ie", initials: "SO" },
]
function ParticipantCardBody({ p }: { p: Participant }) {
return (
<div className="flex items-center gap-3 px-3 py-2">
<Avatar size="sm">
<AvatarFallback>{p.initials}</AvatarFallback>
<AvatarBadge className="overflow-hidden">
<CountryFlag code={p.countryCode} shape="circle" className="h-full w-full" />
</AvatarBadge>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-card-foreground">{p.name}</p>
<p className="truncate text-xs text-muted-foreground">{p.detail}</p>
</div>
<KanbanCardHandle />
</div>
)
}
export function KanbanBoardDemo() {
const [items, setItems] = useState<Participant[]>(seed)
const [activeId, setActiveId] = useState<string | null>(null)
const activeItem = items.find((i) => i.id === activeId) ?? null
function findColumnId(id: string) {
return items.find((i) => i.id === id)?.columnId
}
function handleDragStart(event: DragStartEvent) {
setActiveId(String(event.active.id))
}
function handleDragOver(event: DragOverEvent) {
const { active, over } = event
if (!over) return
const aId = String(active.id)
const oId = String(over.id)
if (aId === oId) return
const activeCol = findColumnId(aId)
const overCol = columns.some((c) => c.id === oId) ? oId : findColumnId(oId)
if (!activeCol || !overCol || activeCol === overCol) return
setItems((prev) => prev.map((i) => (i.id === aId ? { ...i, columnId: overCol } : i)))
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
setActiveId(null)
if (!over) return
const aId = String(active.id)
const oId = String(over.id)
if (aId === oId) return
setItems((prev) => {
const aIdx = prev.findIndex((i) => i.id === aId)
const oIdx = prev.findIndex((i) => i.id === oId)
if (aIdx === -1 || oIdx === -1) return prev
return arrayMove(prev, aIdx, oIdx)
})
}
return (
<KanbanBoard
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
overlay={
activeItem ? (
<KanbanCard id={activeItem.id} overlay>
<ParticipantCardBody p={activeItem} />
</KanbanCard>
) : null
}
>
{columns.map((col) => {
const colItems = items.filter((i) => i.columnId === col.id)
return (
<KanbanColumn
key={col.id}
id={col.id}
title={col.title}
itemIds={colItems.map((i) => i.id)}
>
{colItems.map((p) => (
<KanbanCard key={p.id} id={p.id}>
<ParticipantCardBody p={p} />
</KanbanCard>
))}
</KanbanColumn>
)
})}
</KanbanBoard>
)
}