feat: implement drag-and-drop reordering for daily checkboxes
- Added DnD functionality to `DailySection` for reordering checkboxes using `@dnd-kit/core` and `@dnd-kit/sortable`. - Introduced `onReorderCheckboxes` prop to handle server updates after reordering. - Updated `useDaily` hook to streamline error handling during reordering. - Cleaned up `Header` component by removing unnecessary syncing text. - Adjusted `DailyPageClient` to pass reorder function to `DailySection`.
This commit is contained in:
78
components/daily/DailyCheckboxSortable.tsx
Normal file
78
components/daily/DailyCheckboxSortable.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
||||
|
||||
interface DailyCheckboxSortableProps {
|
||||
checkbox: DailyCheckbox;
|
||||
onToggle: (checkboxId: string) => Promise<void>;
|
||||
onUpdate: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
|
||||
onDelete: (checkboxId: string) => Promise<void>;
|
||||
saving?: boolean;
|
||||
}
|
||||
|
||||
export function DailyCheckboxSortable({
|
||||
checkbox,
|
||||
onToggle,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
saving = false
|
||||
}: DailyCheckboxSortableProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: checkbox.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`
|
||||
${isDragging ? 'z-50' : 'z-0'}
|
||||
${isDragging ? 'shadow-lg' : ''}
|
||||
transition-shadow duration-200
|
||||
`}
|
||||
>
|
||||
<div className="relative group">
|
||||
{/* Handle de drag */}
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="absolute left-0 top-0 bottom-0 w-3 cursor-grab active:cursor-grabbing flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
title="Glisser pour réorganiser"
|
||||
>
|
||||
<div className="w-1.5 h-6 flex flex-col justify-center gap-0.5">
|
||||
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkbox item avec padding left pour le handle */}
|
||||
<div className="pl-4">
|
||||
<DailyCheckboxItem
|
||||
checkbox={checkbox}
|
||||
onToggle={onToggle}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
saving={saving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,13 @@
|
||||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { DailyCheckboxSortable } from './DailyCheckboxSortable';
|
||||
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
||||
import { DailyAddForm } from './DailyAddForm';
|
||||
import { DndContext, closestCenter, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface DailySectionProps {
|
||||
title: string;
|
||||
@@ -14,6 +19,7 @@ interface DailySectionProps {
|
||||
onToggleCheckbox: (checkboxId: string) => Promise<void>;
|
||||
onUpdateCheckbox: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
|
||||
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
||||
onReorderCheckboxes: (date: Date, checkboxIds: string[]) => Promise<void>;
|
||||
onToggleAll?: () => Promise<void>;
|
||||
saving: boolean;
|
||||
refreshing?: boolean;
|
||||
@@ -27,10 +33,13 @@ export function DailySection({
|
||||
onToggleCheckbox,
|
||||
onUpdateCheckbox,
|
||||
onDeleteCheckbox,
|
||||
onReorderCheckboxes,
|
||||
onToggleAll,
|
||||
saving,
|
||||
refreshing = false
|
||||
}: DailySectionProps) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [items, setItems] = useState(checkboxes);
|
||||
const formatShortDate = (date: Date) => {
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
@@ -39,7 +48,52 @@ export function DailySection({
|
||||
});
|
||||
};
|
||||
|
||||
// Mettre à jour les items quand les checkboxes changent
|
||||
React.useEffect(() => {
|
||||
setItems(checkboxes);
|
||||
}, [checkboxes]);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string);
|
||||
};
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
setActiveId(null);
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
// Mise à jour optimiste
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newItems);
|
||||
|
||||
// Envoyer l'ordre au serveur
|
||||
const checkboxIds = newItems.map(item => item.id);
|
||||
try {
|
||||
await onReorderCheckboxes(date, checkboxIds);
|
||||
} catch (error) {
|
||||
// Rollback en cas d'erreur
|
||||
setItems(checkboxes);
|
||||
console.error('Erreur lors du réordonnancement:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const activeCheckbox = activeId ? items.find(item => item.id === activeId) : null;
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||
>
|
||||
<Card className="p-0 flex flex-col h-[600px]">
|
||||
{/* Header */}
|
||||
<div className="p-4 pb-0">
|
||||
@@ -71,11 +125,12 @@ export function DailySection({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liste des checkboxes - zone scrollable */}
|
||||
{/* Liste des checkboxes - zone scrollable avec drag & drop */}
|
||||
<div className="flex-1 px-4 overflow-y-auto min-h-0">
|
||||
<SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-1.5 pb-4">
|
||||
{checkboxes.map((checkbox) => (
|
||||
<DailyCheckboxItem
|
||||
{items.map((checkbox) => (
|
||||
<DailyCheckboxSortable
|
||||
key={checkbox.id}
|
||||
checkbox={checkbox}
|
||||
onToggle={onToggleCheckbox}
|
||||
@@ -85,12 +140,13 @@ export function DailySection({
|
||||
/>
|
||||
))}
|
||||
|
||||
{checkboxes.length === 0 && (
|
||||
{items.length === 0 && (
|
||||
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm font-mono">
|
||||
Aucune tâche pour cette période
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</div>
|
||||
|
||||
{/* Footer - Formulaire d'ajout toujours en bas */}
|
||||
@@ -101,5 +157,27 @@ export function DailySection({
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<DragOverlay
|
||||
dropAnimation={null}
|
||||
style={{
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
{activeCheckbox ? (
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-md shadow-xl opacity-95 transform rotate-3 scale-105">
|
||||
<div className="pl-4">
|
||||
<DailyCheckboxItem
|
||||
checkbox={activeCheckbox}
|
||||
onToggle={() => Promise.resolve()}
|
||||
onUpdate={() => Promise.resolve()}
|
||||
onDelete={() => Promise.resolve()}
|
||||
saving={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
|
||||
{subtitle} {syncing && '• Synchronisation...'}
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -329,10 +329,7 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
|
||||
const dailyId = data.date.toISOString().split('T')[0];
|
||||
const result = await reorderCheckboxesAction(dailyId, data.checkboxIds);
|
||||
|
||||
if (result.success) {
|
||||
// Rafraîchir pour obtenir l'ordre correct
|
||||
await refreshDaily();
|
||||
} else {
|
||||
if (!result.success) {
|
||||
setError(result.error || 'Erreur lors du réordonnancement');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -341,7 +338,7 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [refreshDaily]);
|
||||
}, []);
|
||||
|
||||
const goToPreviousDay = useCallback(async (): Promise<void> => {
|
||||
const previousDay = new Date(currentDate);
|
||||
|
||||
@@ -36,6 +36,7 @@ export function DailyPageClient({
|
||||
toggleAllYesterday,
|
||||
updateCheckbox,
|
||||
deleteCheckbox,
|
||||
reorderCheckboxes,
|
||||
goToPreviousDay,
|
||||
goToNextDay,
|
||||
goToToday,
|
||||
@@ -93,6 +94,10 @@ export function DailyPageClient({
|
||||
});
|
||||
};
|
||||
|
||||
const handleReorderCheckboxes = async (date: Date, checkboxIds: string[]) => {
|
||||
await reorderCheckboxes({ date, checkboxIds });
|
||||
};
|
||||
|
||||
const getYesterdayDate = () => {
|
||||
const yesterday = new Date(currentDate);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
@@ -220,6 +225,7 @@ export function DailyPageClient({
|
||||
onToggleCheckbox={handleToggleCheckbox}
|
||||
onUpdateCheckbox={handleUpdateCheckbox}
|
||||
onDeleteCheckbox={handleDeleteCheckbox}
|
||||
onReorderCheckboxes={handleReorderCheckboxes}
|
||||
onToggleAll={toggleAllYesterday}
|
||||
saving={saving}
|
||||
refreshing={refreshing}
|
||||
@@ -234,6 +240,7 @@ export function DailyPageClient({
|
||||
onToggleCheckbox={handleToggleCheckbox}
|
||||
onUpdateCheckbox={handleUpdateCheckbox}
|
||||
onDeleteCheckbox={handleDeleteCheckbox}
|
||||
onReorderCheckboxes={handleReorderCheckboxes}
|
||||
onToggleAll={toggleAllToday}
|
||||
saving={saving}
|
||||
refreshing={refreshing}
|
||||
|
||||
Reference in New Issue
Block a user