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

@@ -10,22 +10,27 @@ interface HeaderProps {
todo: number;
completionRate: number;
};
syncing?: boolean;
}
export function Header({ title, subtitle, stats }: HeaderProps) {
export function Header({ title, subtitle, stats, syncing = false }: HeaderProps) {
return (
<header className="bg-slate-900/80 backdrop-blur-sm border-b border-slate-700/50 shadow-lg shadow-slate-900/20">
<div className="container mx-auto px-6 py-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
{/* Titre tech avec glow */}
<div className="flex items-center gap-4">
<div className="w-3 h-3 bg-cyan-400 rounded-full animate-pulse shadow-cyan-400/50 shadow-lg"></div>
<div className={`w-3 h-3 rounded-full shadow-lg ${
syncing
? 'bg-yellow-400 animate-spin shadow-yellow-400/50'
: 'bg-cyan-400 animate-pulse shadow-cyan-400/50'
}`}></div>
<div>
<h1 className="text-2xl font-mono font-bold text-slate-100 tracking-wider">
{title}
</h1>
<p className="text-slate-400 mt-1 font-mono text-sm">
{subtitle}
{subtitle} {syncing && '• Synchronisation...'}
</p>
</div>
</div>

View File

@@ -1,8 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { Header } from './Header';
import { tasksClient } from '@/clients/tasks-client';
import { useTasks } from '@/hooks/useTasks';
interface HeaderContainerProps {
title: string;
@@ -17,41 +16,17 @@ interface HeaderContainerProps {
}
export function HeaderContainer({ title, subtitle, initialStats }: HeaderContainerProps) {
const [stats, setStats] = useState(initialStats);
const [isHydrated, setIsHydrated] = useState(false);
// Hydratation côté client
useEffect(() => {
setIsHydrated(true);
}, []);
// Rafraîchir les stats périodiquement côté client
useEffect(() => {
if (!isHydrated) return;
const refreshStats = async () => {
try {
const response = await tasksClient.getTasks({ limit: 1 }); // Juste pour les stats
setStats(response.stats);
} catch (error) {
console.error('Erreur lors du rafraîchissement des stats:', error);
}
};
// Rafraîchir les stats toutes les 30 secondes
const interval = setInterval(refreshStats, 30000);
// Rafraîchir immédiatement après hydratation
refreshStats();
return () => clearInterval(interval);
}, [isHydrated]);
const { stats, syncing } = useTasks(
{ limit: 1 }, // Juste pour les stats
{ tasks: [], stats: initialStats }
);
return (
<Header
title={title}
subtitle={subtitle}
stats={stats}
syncing={syncing}
/>
);
}