Add src/components/KanbanBoard.tsx
This commit is contained in:
171
src/components/KanbanBoard.tsx
Normal file
171
src/components/KanbanBoard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user