feat: implement drag & drop functionality using @dnd-kit

- Added drag & drop capabilities to the Kanban board with @dnd-kit for task status updates.
- Integrated DndContext in `KanbanBoard` and utilized `useDroppable` in `KanbanColumn` for drop zones.
- Enhanced `TaskCard` with draggable features and visual feedback during dragging.
- Updated `TODO.md` to reflect the completion of drag & drop tasks and optimistically update task statuses.
- Introduced optimistic updates in `useTasks` for smoother user experience during drag & drop operations.
This commit is contained in:
Julien Froidefond
2025-09-14 09:08:06 +02:00
parent cff99969d3
commit 9193305550
10 changed files with 324 additions and 86 deletions

View File

@@ -6,6 +6,16 @@ import { Button } from '@/components/ui/Button';
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
import { CreateTaskData } from '@/clients/tasks-client';
import { useMemo, useState } from 'react';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { TaskCard } from './TaskCard';
interface KanbanBoardProps {
tasks: Task[];
@@ -13,11 +23,22 @@ interface KanbanBoardProps {
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
loading?: boolean;
}
export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, loading = false }: KanbanBoardProps) {
export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onUpdateTitle, onUpdateStatus, loading = false }: KanbanBoardProps) {
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [activeTask, setActiveTask] = useState<Task | null>(null);
// Configuration des capteurs pour le drag & drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // Évite les clics accidentels
},
})
);
// Organiser les tâches par statut
const tasksByStatus = useMemo(() => {
const grouped = tasks.reduce((acc, task) => {
@@ -70,54 +91,99 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
}
};
// Gestion du début du drag
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const task = tasks.find(t => t.id === active.id);
setActiveTask(task || null);
};
// Gestion de la fin du drag
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
if (!over || !onUpdateStatus) return;
const taskId = active.id as string;
const newStatus = over.id as TaskStatus;
// Trouver la tâche actuelle
const task = tasks.find(t => t.id === taskId);
if (!task || task.status === newStatus) return;
// Mettre à jour le statut
await onUpdateStatus(taskId, newStatus);
};
return (
<div className="h-full flex flex-col bg-slate-950">
{/* Header avec bouton nouvelle tâche */}
<div className="flex justify-between items-center p-6 pb-0">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<h2 className="text-lg font-mono font-bold text-slate-100 uppercase tracking-wider">
Kanban Board
</h2>
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="h-full flex flex-col bg-slate-950">
{/* Header avec bouton nouvelle tâche */}
<div className="flex justify-between items-center p-6 pb-0">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<h2 className="text-lg font-mono font-bold text-slate-100 uppercase tracking-wider">
Kanban Board
</h2>
</div>
{onCreateTask && (
<Button
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
disabled={loading}
>
+ Nouvelle tâche
</Button>
)}
</div>
{/* Board tech dark */}
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
{columns.map((column) => (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
tasks={column.tasks}
onCreateTask={onCreateTask ? handleCreateTask : undefined}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
/>
))}
</div>
{/* Modal de création */}
{onCreateTask && (
<Button
variant="primary"
onClick={() => setIsCreateModalOpen(true)}
disabled={loading}
>
+ Nouvelle tâche
</Button>
<CreateTaskForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateTask}
loading={loading}
/>
)}
</div>
{/* Board tech dark */}
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
{columns.map((column) => (
<KanbanColumn
key={column.id}
id={column.id}
title={column.title}
color={column.color}
tasks={column.tasks}
onCreateTask={onCreateTask ? handleCreateTask : undefined}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
/>
))}
</div>
{/* Modal de création */}
{onCreateTask && (
<CreateTaskForm
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSubmit={handleCreateTask}
loading={loading}
/>
)}
</div>
{/* Overlay pour le drag & drop */}
<DragOverlay>
{activeTask ? (
<div className="rotate-3 opacity-90">
<TaskCard
task={activeTask}
onDelete={undefined}
onEdit={undefined}
onUpdateTitle={undefined}
/>
</div>
) : null}
</DragOverlay>
</DndContext>
);
}