'use client';
import * as React from 'react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge-2';
import { Button } from '@/components/ui/button-1';
import {
Kanban,
KanbanBoard,
KanbanColumn,
KanbanColumnContent,
KanbanColumnHandle,
KanbanItem,
KanbanItemHandle,
KanbanOverlay,
} from '@/components/ui/kanban';
import { GripVertical } from 'lucide-react';
interface Task {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
description?: string;
assignee?: string;
assigneeAvatar?: string;
dueDate?: string;
}
const COLUMN_TITLES: Record<string, string> = {
backlog: 'Backlog',
inProgress: 'In Progress',
review: 'Review',
done: 'Done',
};
interface TaskCardProps extends Omit<React.ComponentProps<typeof KanbanItem>, 'value' | 'children'> {
task: Task;
asHandle?: boolean;
}
function TaskCard({ task, asHandle, ...props }: TaskCardProps) {
const cardContent = (
<div className="rounded-md border bg-card p-3 shadow-xs">
<div className="flex flex-col gap-2.5">
<div className="flex items-center justify-between gap-2">
<span className="line-clamp-1 font-medium text-sm">{task.title}</span>
<Badge
variant={task.priority === 'high' ? 'destructive' : task.priority === 'medium' ? 'primary' : 'warning'}
appearance="outline"
className="pointer-events-none h-5 rounded-sm px-1.5 text-[11px] capitalize shrink-0"
>
{task.priority}
</Badge>
</div>
<div className="flex items-center justify-between text-muted-foreground text-xs">
{task.assignee && (
<div className="flex items-center gap-1">
<Avatar className="size-4">
<AvatarImage src={task.assigneeAvatar} />
<AvatarFallback>{task.assignee.charAt(0)}</AvatarFallback>
</Avatar>
<span className="line-clamp-1">{task.assignee}</span>
</div>
)}
{task.dueDate && <time className="text-[10px] tabular-nums whitespace-nowrap">{task.dueDate}</time>}
</div>
</div>
</div>
);
return (
<KanbanItem value={task.id} {...props}>
{asHandle ? <KanbanItemHandle>{cardContent}</KanbanItemHandle> : cardContent}
</KanbanItem>
);
}
interface TaskColumnProps extends Omit<React.ComponentProps<typeof KanbanColumn>, 'children'> {
tasks: Task[];
isOverlay?: boolean;
}
function TaskColumn({ value, tasks, isOverlay, ...props }: TaskColumnProps) {
return (
<KanbanColumn value={value} {...props} className="rounded-md border bg-card p-2.5 shadow-xs">
<div className="flex items-center justify-between mb-2.5">
<div className="flex items-center gap-2.5">
<span className="font-semibold text-sm">{COLUMN_TITLES[value]}</span>
<Badge variant="secondary">{tasks.length}</Badge>
</div>
<KanbanColumnHandle asChild>
<Button variant="dim" size="sm" mode="icon">
<GripVertical />
</Button>
</KanbanColumnHandle>
</div>
<KanbanColumnContent value={value} className="flex flex-col gap-2.5 p-0.5">
{tasks.map((task) => (
<TaskCard key={task.id} task={task} asHandle={!isOverlay} />
))}
</KanbanColumnContent>
</KanbanColumn>
);
}
export default function Component() {
const [columns, setColumns] = React.useState<Record<string, Task[]>>({
backlog: [
{
id: '1',
title: 'Add authentication',
priority: 'high',
assignee: 'John Doe',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/b1/b1f6209ae26207ebe11c243a659f0e5e15a0a48232261ecf3c05211a40af2225.jpg',
dueDate: 'Jan 10, 2025',
},
{
id: '2',
title: 'Create API endpoints',
priority: 'medium',
assignee: 'Jane Smith',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/e7/e7a0b30cb92ca533b2f8dbf57649e4b60129a9e84f3fc36d45b09e2dfcaec61d.jpg',
dueDate: 'Jan 15, 2025',
},
{
id: '3',
title: 'Write documentation',
priority: 'low',
assignee: 'Bob Johnson',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/4c/4cff4f892ece6dca0865313df96f11ac30e11b6dcbf3b9a86bad86a3049aa6e1.jpg',
dueDate: 'Jan 20, 2025',
},
],
inProgress: [
{
id: '4',
title: 'Design system updates',
priority: 'high',
assignee: 'Alice Brown',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/55/55d0cf713811843ffbd3412ee403668a82597bb83aabbc684a87f66c1fc962e4.jpg',
dueDate: 'Aug 25, 2025',
},
{
id: '5',
title: 'Implement dark mode',
priority: 'medium',
assignee: 'Charlie Wilson',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/32/32afb68c9233445d08f7c4af3e781f648c6eeeb7dadeb5bdd341a003684d1c93.jpg',
dueDate: 'Aug 25, 2025',
},
],
done: [
{
id: '7',
title: 'Setup project',
priority: 'high',
assignee: 'Eve Davis',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/7f/7f2f1b6a4c09f5092437fe960232360d1e2dcf7a198c8580f3c5478c7b2d9386.jpg',
dueDate: 'Sep 25, 2025',
},
{
id: '8',
title: 'Initial commit',
priority: 'low',
assignee: 'Frank White',
assigneeAvatar: 'https://cdn.21st.dev/assets/mirror/f2/f25b1b7a6a351c0f748d81bf4fcaf8c5a2f8ed036563c2693d4c1ca3718d9d5d.jpg',
dueDate: 'Sep 20, 2025',
},
],
});
return (
<div className="p-5">
<Kanban value={columns} onValueChange={setColumns} getItemValue={(item) => item.id}>
<KanbanBoard className="grid grid-cols-3 gap-5">
{Object.entries(columns).map(([columnValue, tasks]) => (
<TaskColumn key={columnValue} value={columnValue} tasks={tasks} />
))}
</KanbanBoard>
<KanbanOverlay>
<div className="rounded-md bg-muted/60 size-full" />
</KanbanOverlay>
</Kanban>
</div>
);
}