From b87fa64d4dae1f40de565f29f584b9d932a9b7ee Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 26 Sep 2025 11:32:22 +0200 Subject: [PATCH] feat: implement optimistic UI for checkbox toggling in DailyCheckboxItem - Added optimistic state handling in `DailyCheckboxItem` for immediate feedback on checkbox toggles, improving user experience. - Updated `useDaily` hook to handle checkbox state updates without blocking UI, ensuring smoother interactions. - Enhanced error handling to rollback state on toggle failures, maintaining data integrity. --- src/components/daily/DailyCheckboxItem.tsx | 36 +++++++++++-- src/hooks/useDaily.ts | 59 +++++++++++----------- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/components/daily/DailyCheckboxItem.tsx b/src/components/daily/DailyCheckboxItem.tsx index 1978465..c1cd6ec 100644 --- a/src/components/daily/DailyCheckboxItem.tsx +++ b/src/components/daily/DailyCheckboxItem.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; import { DailyCheckbox, DailyCheckboxType } from '@/lib/types'; import { Input } from '@/components/ui/Input'; @@ -24,6 +24,36 @@ export function DailyCheckboxItem({ const [inlineEditingId, setInlineEditingId] = useState(null); const [inlineEditingText, setInlineEditingText] = useState(''); const [editingCheckbox, setEditingCheckbox] = useState(null); + const [optimisticChecked, setOptimisticChecked] = useState(null); + + // État optimiste local pour une réponse immédiate + const isChecked = optimisticChecked !== null ? optimisticChecked : checkbox.isChecked; + + // Synchroniser l'état optimiste avec les changements externes + useEffect(() => { + if (optimisticChecked !== null && optimisticChecked === checkbox.isChecked) { + // L'état serveur a été mis à jour, on peut reset l'optimiste + setOptimisticChecked(null); + } + }, [checkbox.isChecked, optimisticChecked]); + + // Handler optimiste pour le toggle + const handleOptimisticToggle = async () => { + const newCheckedState = !isChecked; + + // Mise à jour optimiste immédiate + setOptimisticChecked(newCheckedState); + + try { + await onToggle(checkbox.id); + // Reset l'état optimiste après succès + setOptimisticChecked(null); + } catch (error) { + // Rollback en cas d'erreur + setOptimisticChecked(null); + console.error('Erreur lors du toggle:', error); + } + }; // Édition inline simple const handleStartInlineEdit = () => { @@ -82,8 +112,8 @@ export function DailyCheckboxItem({ {/* Checkbox */} onToggle(checkbox.id)} + checked={isChecked} + onChange={handleOptimisticToggle} disabled={saving} className="w-4 h-4 md:w-3.5 md:h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1" /> diff --git a/src/hooks/useDaily.ts b/src/hooks/useDaily.ts index 9ceb86a..d31f11c 100644 --- a/src/hooks/useDaily.ts +++ b/src/hooks/useDaily.ts @@ -209,49 +209,48 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD if (!dailyView) return Promise.resolve(); return new Promise((resolve) => { - startTransition(async () => { - // Trouver la checkbox dans yesterday ou today - let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId); - if (!checkbox) { - checkbox = dailyView.today.find(cb => cb.id === checkboxId); - } - - if (!checkbox) { - resolve(); - return; - } + // Trouver la checkbox dans yesterday ou today + let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId); + if (!checkbox) { + checkbox = dailyView.today.find(cb => cb.id === checkboxId); + } + + if (!checkbox) { + resolve(); + return; + } - // Mise à jour optimiste IMMÉDIATE - const newCheckedState = !checkbox.isChecked; - const previousDailyView = dailyView; - - setDailyView(prev => prev ? { - ...prev, - yesterday: prev.yesterday.map(cb => - cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb - ), - today: prev.today.map(cb => - cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb - ) - } : null); + // Mise à jour optimiste IMMÉDIATE + const newCheckedState = !checkbox.isChecked; + const previousDailyView = dailyView; + + setDailyView(prev => prev ? { + ...prev, + yesterday: prev.yesterday.map(cb => + cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb + ), + today: prev.today.map(cb => + cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb + ) + } : null); - try { - const result = await toggleCheckboxAction(checkboxId); - + // Appel serveur en arrière-plan (sans startTransition) + toggleCheckboxAction(checkboxId) + .then(result => { if (!result.success) { // Rollback en cas d'erreur setDailyView(previousDailyView); setError(result.error || 'Erreur lors de la mise à jour de la checkbox'); } resolve(); - } catch (err) { + }) + .catch(err => { // Rollback en cas d'erreur setDailyView(previousDailyView); setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox'); console.error('Erreur toggleCheckbox:', err); resolve(); - } - }); + }); }); }, [dailyView]);