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

@@ -15,12 +15,14 @@ interface UseTasksState {
};
loading: boolean;
error: string | null;
syncing: boolean; // Pour indiquer les opérations optimistes en cours
}
interface UseTasksActions {
refreshTasks: () => Promise<void>;
createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTask: (data: UpdateTaskData) => Promise<Task | null>;
updateTaskOptimistic: (data: UpdateTaskData) => Promise<Task | null>;
deleteTask: (taskId: string) => Promise<void>;
setFilters: (filters: TaskFilters) => void;
}
@@ -42,7 +44,8 @@ export function useTasks(
completionRate: 0
},
loading: false,
error: null
error: null,
syncing: false
});
const [filters, setFilters] = useState<TaskFilters>(initialFilters || {});
@@ -117,6 +120,87 @@ export function useTasks(
}
}, [refreshTasks]);
/**
* Met à jour une tâche de manière optimiste (pour drag & drop)
*/
const updateTaskOptimistic = useCallback(async (data: UpdateTaskData): Promise<Task | null> => {
const { taskId, ...updates } = data;
// 1. Sauvegarder l'état actuel pour rollback
const currentTasks = state.tasks;
const taskToUpdate = currentTasks.find(t => t.id === taskId);
if (!taskToUpdate) {
console.error('Tâche non trouvée pour mise à jour optimiste:', taskId);
return null;
}
// 2. Mise à jour optimiste immédiate de l'état local
const updatedTask = { ...taskToUpdate, ...updates };
const updatedTasks = currentTasks.map(task =>
task.id === taskId ? updatedTask : task
);
// Recalculer les stats
const newStats = {
total: updatedTasks.length,
completed: updatedTasks.filter(t => t.status === 'done').length,
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
todo: updatedTasks.filter(t => t.status === 'todo').length,
completionRate: updatedTasks.length > 0
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
: 0
};
setState(prev => ({
...prev,
tasks: updatedTasks,
stats: newStats,
error: null,
syncing: true // Indiquer qu'une synchronisation est en cours
}));
// 3. Appel API en arrière-plan
try {
const response = await tasksClient.updateTask(data);
// Si l'API retourne des données différentes, on met à jour
if (response.data) {
setState(prev => ({
...prev,
tasks: prev.tasks.map(task =>
task.id === taskId ? response.data : task
),
syncing: false // Synchronisation terminée
}));
} else {
setState(prev => ({ ...prev, syncing: false }));
}
return response.data;
} catch (error) {
// 4. Rollback en cas d'erreur
setState(prev => ({
...prev,
tasks: currentTasks,
stats: {
total: currentTasks.length,
completed: currentTasks.filter(t => t.status === 'done').length,
inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
todo: currentTasks.filter(t => t.status === 'todo').length,
completionRate: currentTasks.length > 0
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
: 0
},
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour',
syncing: false // Arrêter l'indicateur de synchronisation
}));
console.error('Erreur lors de la mise à jour optimiste:', error);
return null;
}
}, [state.tasks]);
/**
* Supprime une tâche
*/
@@ -148,6 +232,7 @@ export function useTasks(
refreshTasks,
createTask,
updateTask,
updateTaskOptimistic,
deleteTask,
setFilters
};