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.
This commit is contained in:
Julien Froidefond
2025-09-26 11:32:22 +02:00
parent a01c0d83d0
commit b87fa64d4d
2 changed files with 62 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types'; import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@@ -24,6 +24,36 @@ export function DailyCheckboxItem({
const [inlineEditingId, setInlineEditingId] = useState<string | null>(null); const [inlineEditingId, setInlineEditingId] = useState<string | null>(null);
const [inlineEditingText, setInlineEditingText] = useState(''); const [inlineEditingText, setInlineEditingText] = useState('');
const [editingCheckbox, setEditingCheckbox] = useState<DailyCheckbox | null>(null); const [editingCheckbox, setEditingCheckbox] = useState<DailyCheckbox | null>(null);
const [optimisticChecked, setOptimisticChecked] = useState<boolean | null>(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 // Édition inline simple
const handleStartInlineEdit = () => { const handleStartInlineEdit = () => {
@@ -82,8 +112,8 @@ export function DailyCheckboxItem({
{/* Checkbox */} {/* Checkbox */}
<input <input
type="checkbox" type="checkbox"
checked={checkbox.isChecked} checked={isChecked}
onChange={() => onToggle(checkbox.id)} onChange={handleOptimisticToggle}
disabled={saving} 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" 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"
/> />

View File

@@ -209,49 +209,48 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
if (!dailyView) return Promise.resolve(); if (!dailyView) return Promise.resolve();
return new Promise((resolve) => { return new Promise((resolve) => {
startTransition(async () => { // Trouver la checkbox dans yesterday ou today
// Trouver la checkbox dans yesterday ou today let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId);
let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId); if (!checkbox) {
if (!checkbox) { checkbox = dailyView.today.find(cb => cb.id === checkboxId);
checkbox = dailyView.today.find(cb => cb.id === checkboxId); }
}
if (!checkbox) {
if (!checkbox) { resolve();
resolve(); return;
return; }
}
// Mise à jour optimiste IMMÉDIATE // Mise à jour optimiste IMMÉDIATE
const newCheckedState = !checkbox.isChecked; const newCheckedState = !checkbox.isChecked;
const previousDailyView = dailyView; const previousDailyView = dailyView;
setDailyView(prev => prev ? { setDailyView(prev => prev ? {
...prev, ...prev,
yesterday: prev.yesterday.map(cb => yesterday: prev.yesterday.map(cb =>
cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb
), ),
today: prev.today.map(cb => today: prev.today.map(cb =>
cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb
) )
} : null); } : null);
try { // Appel serveur en arrière-plan (sans startTransition)
const result = await toggleCheckboxAction(checkboxId); toggleCheckboxAction(checkboxId)
.then(result => {
if (!result.success) { if (!result.success) {
// Rollback en cas d'erreur // Rollback en cas d'erreur
setDailyView(previousDailyView); setDailyView(previousDailyView);
setError(result.error || 'Erreur lors de la mise à jour de la checkbox'); setError(result.error || 'Erreur lors de la mise à jour de la checkbox');
} }
resolve(); resolve();
} catch (err) { })
.catch(err => {
// Rollback en cas d'erreur // Rollback en cas d'erreur
setDailyView(previousDailyView); setDailyView(previousDailyView);
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox'); setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox');
console.error('Erreur toggleCheckbox:', err); console.error('Erreur toggleCheckbox:', err);
resolve(); resolve();
} });
});
}); });
}, [dailyView]); }, [dailyView]);