136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
import { Calendar, Flag, Plus } from 'lucide-react';
|
|
import type { Task, Project, Section } from '@/types';
|
|
import { cn, formatDate, isOverdue, getPriorityColor } from '@/lib/utils';
|
|
import { useTasksStore } from '@/stores/tasks';
|
|
import { AddTask } from '@/components/AddTask';
|
|
|
|
interface BoardViewProps {
|
|
project: Project;
|
|
sections: Section[];
|
|
tasks: Task[];
|
|
isLoading: boolean;
|
|
}
|
|
|
|
function TaskCard({ task }: { task: Task }) {
|
|
const { toggleComplete, setSelectedTask } = useTasksStore();
|
|
const overdue = !task.isCompleted && isOverdue(task.dueDate);
|
|
|
|
return (
|
|
<div
|
|
className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
|
onClick={() => setSelectedTask(task)}
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
{/* Priority indicator */}
|
|
<span
|
|
className="mt-1 w-2 h-2 rounded-full flex-shrink-0"
|
|
style={{ backgroundColor: getPriorityColor(task.priority) }}
|
|
/>
|
|
<div className="flex-1 min-w-0">
|
|
<p className={cn(
|
|
'text-sm font-medium text-gray-900',
|
|
task.isCompleted && 'line-through text-gray-500'
|
|
)}>
|
|
{task.title}
|
|
</p>
|
|
|
|
{task.description && (
|
|
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
|
|
{task.description}
|
|
</p>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
|
{task.dueDate && (
|
|
<span className={cn(
|
|
'inline-flex items-center gap-1 text-xs',
|
|
overdue ? 'text-red-500' : 'text-gray-500'
|
|
)}>
|
|
<Calendar className="w-3 h-3" />
|
|
{formatDate(task.dueDate)}
|
|
</span>
|
|
)}
|
|
|
|
{task.assignee && (
|
|
<span
|
|
className="w-5 h-5 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-[10px] font-medium flex-shrink-0"
|
|
title={task.assignee.name}
|
|
>
|
|
{task.assignee.name.charAt(0).toUpperCase()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BoardColumn({
|
|
title,
|
|
tasks,
|
|
projectId,
|
|
sectionId,
|
|
}: {
|
|
title: string;
|
|
tasks: Task[];
|
|
projectId: string;
|
|
sectionId?: string;
|
|
}) {
|
|
return (
|
|
<div className="flex-shrink-0 w-72 flex flex-col bg-gray-100 rounded-xl max-h-full">
|
|
{/* Column header */}
|
|
<div className="px-3 py-3 flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-gray-700">
|
|
{title}
|
|
<span className="ml-2 text-xs font-normal text-gray-400">{tasks.length}</span>
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Tasks */}
|
|
<div className="flex-1 overflow-y-auto px-2 pb-2 space-y-2">
|
|
{tasks.map((task) => (
|
|
<TaskCard key={task.id} task={task} />
|
|
))}
|
|
</div>
|
|
|
|
{/* Add task */}
|
|
<div className="px-2 pb-2">
|
|
<AddTask projectId={projectId} sectionId={sectionId} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function BoardView({ project, sections, tasks, isLoading }: BoardViewProps) {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="text-center py-12 text-gray-500">Loading tasks...</div>
|
|
);
|
|
}
|
|
|
|
const unsectionedTasks = tasks.filter((t) => !t.sectionId);
|
|
const columns = [
|
|
{ title: 'No Section', tasks: unsectionedTasks, sectionId: undefined },
|
|
...sections.map((section) => ({
|
|
title: section.name,
|
|
tasks: tasks.filter((t) => t.sectionId === section.id),
|
|
sectionId: section.id,
|
|
})),
|
|
];
|
|
|
|
return (
|
|
<div className="flex gap-4 overflow-x-auto pb-4" style={{ minHeight: '60vh' }}>
|
|
{columns.map((col) => (
|
|
<BoardColumn
|
|
key={col.sectionId || 'unsectioned'}
|
|
title={col.title}
|
|
tasks={col.tasks}
|
|
projectId={project.id}
|
|
sectionId={col.sectionId}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|