Add src/components/KanbanBoard.tsx

This commit is contained in:
2026-05-02 18:03:55 -04:00
parent ed3d409e60
commit cd55bc5fda

View File

@@ -0,0 +1,171 @@
'use client';
import { useApp, type Task } from './AppProvider';
import { useState, useCallback } from 'react';
import { format, isToday, isTomorrow, isPast, parseISO } from 'date-fns';
interface KanbanTask extends Task {
projectColor?: string;
}
export default function KanbanBoard() {
const { tasks, projects, setSelectedTask, setShowEditTaskModal, setEditingTask, refreshTasks } = useApp();
const [draggedTask, setDraggedTask] = useState<string | null>(null);
// Add project info
const kanbanTasks: KanbanTask[] = tasks.map(task => {
const project = projects.find(p => p.id === task.projectId);
return { ...task, projectColor: project?.color };
});
const todoTasks = kanbanTasks.filter(t => t.status === 'todo');
const inProgressTasks = kanbanTasks.filter(t => t.status === 'in_progress');
const doneTasks = kanbanTasks.filter(t => t.status === 'done');
const columns: { key: string; title: string; tasks: KanbanTask[]; icon: string; color: string }[] = [
{ key: 'todo', title: 'To Do', tasks: todoTasks, icon: '📋', color: 'border-gray-300' },
{ key: 'in_progress', title: 'In Progress', tasks: inProgressTasks, icon: '🔄', color: 'border-blue-300' },
{ key: 'done', title: 'Done', tasks: doneTasks, icon: '✅', color: 'border-green-300' },
];
const handleDragStart = (taskId: string) => {
setDraggedTask(taskId);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = async (status: string, e: React.DragEvent) => {
e.preventDefault();
if (!draggedTask) return;
try {
await fetch(`/api/tasks/${draggedTask}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
});
refreshTasks();
// Refresh kanban data
const res = await fetch(`/api/kanban`);
const data = await res.json();
} catch (error) {
console.error('Failed to update task status:', error);
}
setDraggedTask(null);
};
const handleTaskClick = (task: KanbanTask) => {
setSelectedTask(task);
setEditingTask(task);
setShowEditTaskModal(true);
};
const getDueDateInfo = (dueDate: string | null) => {
if (!dueDate) return null;
const date = parseISO(dueDate);
if (isToday(date)) return { label: 'Today', className: 'text-[var(--priority-urgent)] font-medium' };
if (isTomorrow(date)) return { label: 'Tomorrow', className: 'text-[var(--priority-high)]' };
if (isPast(date)) return { label: format(date, 'MMM d'), className: 'text-[var(--priority-urgent)] font-medium' };
return { label: format(date, 'MMM d'), className: 'text-text-secondary' };
};
const priorityConfig = {
urgent: { bg: 'bg-[var(--priority-urgent)]/10', text: 'text-[var(--priority-urgent)]' },
high: { bg: 'bg-[var(--priority-high)]/10', text: 'text-[var(--priority-high)]' },
medium: { bg: 'bg-[var(--priority-medium)]/10', text: 'text-[var(--priority-medium)]' },
low: { bg: 'bg-[var(--priority-low)]/10', text: 'text-[var(--priority-low)]' },
};
return (
<div className="flex-1 overflow-x-auto overflow-y-hidden p-4">
<div className="flex gap-4 h-full min-w-[900px]">
{columns.map((column) => (
<div
key={column.key}
className="flex-1 flex flex-col bg-content rounded-xl border border-border"
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(column.key, e)}
>
{/* Column header */}
<div className={`p-3 border-b-2 ${column.color} rounded-t-xl`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span>{column.icon}</span>
<span className="font-semibold text-sm">{column.title}</span>
<span className="text-xs text-text-secondary bg-content px-2 py-0.5 rounded-full">
{column.tasks.length}
</span>
</div>
</div>
</div>
{/* Tasks */}
<div className="flex-1 overflow-y-auto p-2 space-y-2">
{column.tasks.map((task) => {
const dueInfo = getDueDateInfo(task.dueDate);
const prio = priorityConfig[task.priority];
return (
<div
key={task.id}
draggable
onDragStart={() => handleDragStart(task.id)}
onClick={() => handleTaskClick(task)}
className={`p-3 bg-card border border-border rounded-lg cursor-grab hover:shadow-md transition-all ${
draggedTask === task.id ? 'opacity-50 scale-95' : ''
} ${task.completed ? 'opacity-60' : ''}`}
>
{/* Project indicator */}
{task.projectColor && (
<div className="flex items-center gap-2 mb-2">
<span
className="w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: task.projectColor }}
/>
</div>
)}
{/* Title */}
<p className={`text-sm font-medium mb-1 ${task.completed ? 'line-through text-text-secondary' : ''}`}>
{task.title}
</p>
{/* Description preview */}
{task.description && (
<p className="text-xs text-text-secondary truncate mb-2">{task.description}</p>
)}
{/* Footer */}
<div className="flex items-center justify-between mt-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${prio.bg} ${prio.text}`}>
{task.priority}
</span>
{dueInfo && (
<span className={`text-xs ${dueInfo.className}`}>
{dueInfo.label}
</span>
)}
{task.recurrenceRule !== 'none' && (
<span title="Recurring">🔄</span>
)}
</div>
</div>
);
})}
{column.tasks.length === 0 && (
<div className="text-center py-8 text-text-secondary text-sm">
Drop tasks here
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}