Add kanban board view, project creation, and task assignment

This commit is contained in:
2026-01-28 17:57:57 +00:00
parent ff70948a54
commit 094e29d838
12 changed files with 459 additions and 26 deletions

135
src/pages/Board.tsx Normal file
View File

@@ -0,0 +1,135 @@
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>
);
}