Kanban Board

PreviousNext

component for the Kanban Board

Docs
spectrumuicomponent

Preview

Loading preview…
app/registry/kanban/kanbanboard.tsx
'use client';

import { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Calendar, GripVertical, MessageCircle, Paperclip, Plus } from 'lucide-react';




interface Task {
  id: string
  title: string
  description?: string
  priority?: "low" | "medium" | "high"
  assignee?: {
    name: string
    avatar: string
  }
  tags?: string[]
  dueDate?: string
  attachments?: number
  comments?: number
}

 interface Column {
  id: string
  title: string
  tasks: Task[]
  color?: string
}

const sampleData: Column[] = [
  {
    id: "todo",
    title: "To Do",
    color: "#8B7355",
    tasks: [
      {
        id: "1",
        title: "Design System Audit",
        description: "Review and update component library",
        priority: "high",
        assignee: { name: "Sarah Chen", avatar: "/headshot/Lummi Doodle 02.png" },
        tags: ["Design", "System"],
        dueDate: "2024-01-15",
        attachments: 3,
        comments: 7,
      },
      {
        id: "2",
        title: "User Research Analysis",
        description: "Analyze feedback from recent user interviews",
        priority: "medium",
        assignee: { name: "Alex Rivera", avatar: "/headshot/Lummi Doodle 04.png" },
        tags: ["Research", "UX"],
        dueDate: "2024-01-18",
        comments: 4,
      },
    ],
  },
  {
    id: "progress",
    title: "In Progress",
    color: "#6B8E23",
    tasks: [
      {
        id: "3",
        title: "Mobile App Redesign",
        description: "Implementing new navigation patterns",
        priority: "high",
        assignee: { name: "Jordan Kim", avatar: "/headshot/Lummi Doodle 06.png" },
        tags: ["Mobile", "UI"],
        attachments: 8,
        comments: 12,
      },
    ],
  },
  {
    id: "review",
    title: "Review",
    color: "#CD853F",
    tasks: [
      {
        id: "4",
        title: "API Documentation",
        description: "Complete developer documentation",
        priority: "medium",
        assignee: { name: "Maya Patel", avatar: "/headshot/Lummi Doodle 09.png" },
        tags: ["Documentation", "API"],
        dueDate: "2024-01-20",
        comments: 2,
      },
    ],
  },
  {
    id: "done",
    title: "Done",
    color: "#556B2F",
    tasks: [
      {
        id: "5",
        title: "Landing Page Optimization",
        description: "Improved conversion rate by 23%",
        priority: "low",
        assignee: { name: "Chris Wong", avatar: "/headshot/Lummi Doodle 10.png" },
        tags: ["Marketing", "Web"],
        attachments: 2,
        comments: 8,
      },
    ],
  },
]



export default function KanbanBoard() {
  const [columns, setColumns] = useState<Column[]>(sampleData);

  const handleDragStart = (e: React.DragEvent, task: Task, columnId: string) => {
    e.dataTransfer.setData('text/plain', JSON.stringify({ task, sourceColumnId: columnId }));
  };

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
  };

  const handleDrop = (e: React.DragEvent, targetColumnId: string) => {
    e.preventDefault();
    const data = JSON.parse(e.dataTransfer.getData('text/plain'));
    const { task, sourceColumnId } = data;

    if (sourceColumnId === targetColumnId) return;

    setColumns((prev) =>
      prev.map((col) => {
        if (col.id === sourceColumnId) {
          return { ...col, tasks: col.tasks.filter((t) => t.id !== task.id) };
        }
        if (col.id === targetColumnId) {
          return { ...col, tasks: [...col.tasks, task] };
        }
        return col;
      }),
    );
  };

  return (
    <div className="">
      <div className="mb-8 text-center">
        <h1 className="text-4xl font-light text-neutral-900 dark:text-neutral-100 mb-2">
          Kanban Board
        </h1>
        <p className="text-neutral-700 dark:text-neutral-300">Drag and drop task management</p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2  gap-6">
        {columns.map((column) => (
          <div
            key={column.id}
            className="bg-white/20 dark:bg-neutral-900/20 backdrop-blur-xl rounded-3xl p-5 border border-border dark:border-neutral-700/50"
            onDragOver={handleDragOver}
            onDrop={(e) => handleDrop(e, column.id)}
          >
            <div className="flex items-center justify-between mb-6">
              <div className="flex items-center gap-3">
                <div className="w-4 h-4 rounded-full " style={{ backgroundColor: column.color }} />
                <h3 className="font-semibold text-neutral-900 dark:text-neutral-100">
                  {column.title}
                </h3>
                <Badge className="bg-neutral-100/80 dark:bg-neutral-800/80 text-neutral-800 dark:text-neutral-200 border-neutral-200/50 dark:border-neutral-600/50">
                  {column.tasks.length}
                </Badge>
              </div>
              <button className="p-1 rounded-full bg-white/30 dark:bg-neutral-800/30 hover:bg-white/50 dark:hover:bg-neutral-700/50 transition-colors">
                <Plus className="w-4 h-4 text-neutral-700 dark:text-neutral-300" />
              </button>
            </div>

            <div className="space-y-4">
              {column.tasks.map((task) => (
                <Card
                  key={task.id}
                  className="cursor-move transition-all duration-300 border bg-white/60 dark:bg-neutral-800/60 backdrop-blur-sm hover:bg-white/70 dark:hover:bg-neutral-700/70"
                  draggable
                  onDragStart={(e) => handleDragStart(e, task, column.id)}
                >
                  <CardContent className="p-5">
                    <div className="space-y-4">
                      <div className="flex items-start justify-between">
                        <h4 className="font-semibold text-neutral-900 dark:text-neutral-100 leading-tight">
                          {task.title}
                        </h4>
                        <GripVertical className="w-5 h-5 text-neutral-500 dark:text-neutral-400 cursor-move" />
                      </div>

                      {task.description && (
                        <p className="text-sm text-neutral-700 dark:text-neutral-300 leading-relaxed">
                          {task.description}
                        </p>
                      )}

                      {task.tags && (
                        <div className="flex flex-wrap gap-2">
                          {task.tags.map((tag) => (
                            <Badge
                              key={tag}
                              className="text-xs bg-neutral-100/60 dark:bg-neutral-700/60 text-neutral-800 dark:text-neutral-200 border-neutral-200/50 dark:border-neutral-600/50 backdrop-blur-sm"
                            >
                              {tag}
                            </Badge>
                          ))}
                        </div>
                      )}

                      <div className="flex items-center justify-between pt-2 border-t border-neutral-200/30 dark:border-neutral-700/30">
                        <div className="flex items-center gap-4 text-neutral-600 dark:text-neutral-400">
                          {task.dueDate && (
                            <div className="flex items-center gap-1">
                              <Calendar className="w-4 h-4" />
                              <span className="text-xs font-medium">Jan 15</span>
                            </div>
                          )}
                          {task.comments && (
                            <div className="flex items-center gap-1">
                              <MessageCircle className="w-4 h-4" />
                              <span className="text-xs font-medium">{task.comments}</span>
                            </div>
                          )}
                          {task.attachments && (
                            <div className="flex items-center gap-1">
                              <Paperclip className="w-4 h-4" />
                              <span className="text-xs font-medium">{task.attachments}</span>
                            </div>
                          )}
                        </div>

                        {task.assignee && (
                          <Avatar className="w-8 h-8 ring-2 ring-white/50 dark:ring-neutral-700/50">
                            <AvatarImage src={task.assignee.avatar} />
                            <AvatarFallback className="bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200 font-medium">
                              {task.assignee.name
                                .split(' ')
                                .map((n) => n[0])
                                .join('')}
                            </AvatarFallback>
                          </Avatar>
                        )}
                      </div>
                    </div>
                  </CardContent>
                </Card>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Installation

npx shadcn@latest add @spectrumui/kanbanboard

Usage

import { Kanbanboard } from "@/components/kanbanboard"
<Kanbanboard />