From 2e3e8bb2223d3d8dbcd41dc38d74c21e8a2ca70e Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 2 Oct 2025 12:31:29 +0200 Subject: [PATCH] feat: optimize UserPreferencesContext with debounce and local storage - Added debounce functionality for kanban filters, view preferences, and column visibility updates to reduce server load. - Implemented local storage synchronization for immediate updates, ensuring user preferences persist across sessions. - Removed unnecessary startTransition calls to streamline state updates and improve UI responsiveness. --- src/contexts/UserPreferencesContext.tsx | 231 ++++++++++++------------ 1 file changed, 113 insertions(+), 118 deletions(-) diff --git a/src/contexts/UserPreferencesContext.tsx b/src/contexts/UserPreferencesContext.tsx index 301284f..86f24c1 100644 --- a/src/contexts/UserPreferencesContext.tsx +++ b/src/contexts/UserPreferencesContext.tsx @@ -1,6 +1,15 @@ 'use client'; import { createContext, useContext, ReactNode, useState, useCallback, useEffect, useTransition } from 'react'; + +// Types pour les timeouts de debounce +declare global { + interface Window { + kanbanFiltersSyncTimeout?: NodeJS.Timeout; + viewPreferencesSyncTimeout?: NodeJS.Timeout; + columnVisibilitySyncTimeout?: NodeJS.Timeout; + } +} import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; import { updateViewPreferences as updateViewPreferencesAction, @@ -79,7 +88,7 @@ const defaultPreferences: UserPreferences = { export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { const [preferences, setPreferences] = useState(initialPreferences || defaultPreferences); - const [isPending, startTransition] = useTransition(); + const [isPending] = useTransition(); const { toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme(); const { status } = useSession(); @@ -116,63 +125,63 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr // === KANBAN FILTERS === const updateKanbanFilters = useCallback((updates: Partial) => { - startTransition(async () => { - const originalFilters = preferences.kanbanFilters; - - // Optimistic update - setPreferences(prev => ({ - ...prev, - kanbanFilters: { ...prev.kanbanFilters, ...updates } - })); - - try { - const result = await updateKanbanFiltersAction(updates); - if (!result.success) { - console.error('Erreur server action:', result.error); - setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters })); - } - } catch (error) { + // Optimistic update immédiat + setPreferences(prev => ({ + ...prev, + kanbanFilters: { ...prev.kanbanFilters, ...updates } + })); + + // Sauvegarde locale immédiate pour éviter les pertes + const newFilters = { ...preferences.kanbanFilters, ...updates }; + localStorage.setItem('kanbanFilters', JSON.stringify(newFilters)); + + // Sync en arrière-plan avec debounce + clearTimeout(window.kanbanFiltersSyncTimeout); + window.kanbanFiltersSyncTimeout = setTimeout(() => { + updateKanbanFiltersAction(updates).catch(error => { console.error('Erreur lors de la mise à jour des filtres Kanban:', error); - setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters })); - } - }); + }); + }, 1000); // debounce 1s }, [preferences.kanbanFilters]); // === VIEW PREFERENCES === const updateViewPreferences = useCallback((updates: Partial) => { - startTransition(async () => { - const originalPreferences = preferences.viewPreferences; - - // Optimistic update - setPreferences(prev => ({ - ...prev, - viewPreferences: { ...prev.viewPreferences, ...updates } - })); - - try { - const result = await updateViewPreferencesAction(updates); - if (!result.success) { - console.error('Erreur server action:', result.error); - setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); - } - } catch (error) { + // Optimistic update immédiat + setPreferences(prev => ({ + ...prev, + viewPreferences: { ...prev.viewPreferences, ...updates } + })); + + // Sauvegarde locale immédiate pour éviter les pertes + const newViewPrefs = { ...preferences.viewPreferences, ...updates }; + localStorage.setItem('viewPreferences', JSON.stringify(newViewPrefs)); + + // Sync en arrière-plan avec debounce + clearTimeout(window.viewPreferencesSyncTimeout); + window.viewPreferencesSyncTimeout = setTimeout(() => { + updateViewPreferencesAction(updates).catch(error => { console.error('Erreur lors de la mise à jour des préférences de vue:', error); - setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); - } - }); + }); + }, 1000); // debounce 1s }, [preferences.viewPreferences]); const toggleObjectivesVisibility = useCallback(() => { - startTransition(async () => { - await toggleObjectivesVisibilityAction(); - }); + // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI + setTimeout(() => { + toggleObjectivesVisibilityAction().catch(error => { + console.error('Erreur lors du toggle de la visibilité des objectifs:', error); + }); + }, 0); }, []); const toggleObjectivesCollapse = useCallback(() => { - startTransition(async () => { - await toggleObjectivesCollapseAction(); - }); + // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI + setTimeout(() => { + toggleObjectivesCollapseAction().catch(error => { + console.error('Erreur lors du toggle du collapse des objectifs:', error); + }); + }, 0); }, []); const toggleTheme = useCallback(() => { @@ -184,90 +193,76 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr }, [themeSetTheme]); const toggleFontSize = useCallback(() => { - startTransition(async () => { - const originalPreferences = preferences.viewPreferences; - - // Optimistic update - cycle through font sizes - const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; - const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); - const nextIndex = (currentIndex + 1) % fontSizes.length; - const newFontSize = fontSizes[nextIndex]; - - setPreferences(prev => ({ - ...prev, - viewPreferences: { ...prev.viewPreferences, fontSize: newFontSize } - })); - - try { - const result = await toggleFontSizeAction(); - if (!result.success) { - console.error('Erreur server action:', result.error); - setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); - } - } catch (error) { + // Optimistic update - cycle through font sizes + const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; + const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); + const nextIndex = (currentIndex + 1) % fontSizes.length; + const newFontSize = fontSizes[nextIndex]; + + setPreferences(prev => ({ + ...prev, + viewPreferences: { ...prev.viewPreferences, fontSize: newFontSize } + })); + + // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI + setTimeout(() => { + toggleFontSizeAction().catch(error => { console.error('Erreur lors du toggle de la taille de police:', error); - setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); - } - }); - }, [preferences.viewPreferences]); + }); + }, 0); + }, [preferences.viewPreferences.fontSize]); // === COLUMN VISIBILITY === const updateColumnVisibility = useCallback((updates: Partial) => { - startTransition(async () => { - const originalVisibility = preferences.columnVisibility; - - // Optimistic update - setPreferences(prev => ({ - ...prev, - columnVisibility: { ...prev.columnVisibility, ...updates } - })); - - try { - const result = await updateColumnVisibilityAction(updates); - if (!result.success) { - console.error('Erreur server action:', result.error); - setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); - } - } catch (error) { + // Optimistic update immédiat + setPreferences(prev => ({ + ...prev, + columnVisibility: { ...prev.columnVisibility, ...updates } + })); + + // Sauvegarde locale immédiate pour éviter les pertes + const newColumnVisibility = { ...preferences.columnVisibility, ...updates }; + localStorage.setItem('columnVisibility', JSON.stringify(newColumnVisibility)); + + // Sync en arrière-plan avec debounce + clearTimeout(window.columnVisibilitySyncTimeout); + window.columnVisibilitySyncTimeout = setTimeout(() => { + updateColumnVisibilityAction(updates).catch(error => { console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error); - setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); - } - }); + }); + }, 1000); // debounce 1s }, [preferences.columnVisibility]); const toggleColumnVisibility = useCallback((status: TaskStatus) => { - startTransition(async () => { - const originalVisibility = preferences.columnVisibility; - const hiddenStatuses = [...preferences.columnVisibility.hiddenStatuses]; - - // Optimistic update - if (hiddenStatuses.includes(status)) { - // Remove from hidden - const index = hiddenStatuses.indexOf(status); - hiddenStatuses.splice(index, 1); - } else { - // Add to hidden - hiddenStatuses.push(status); - } - - setPreferences(prev => ({ - ...prev, - columnVisibility: { ...prev.columnVisibility, hiddenStatuses } - })); + const hiddenStatuses = [...preferences.columnVisibility.hiddenStatuses]; + + // Optimistic update + if (hiddenStatuses.includes(status)) { + // Remove from hidden + const index = hiddenStatuses.indexOf(status); + hiddenStatuses.splice(index, 1); + } else { + // Add to hidden + hiddenStatuses.push(status); + } + + setPreferences(prev => ({ + ...prev, + columnVisibility: { ...prev.columnVisibility, hiddenStatuses } + })); - try { - const result = await toggleColumnVisibilityAction(status); - if (!result.success) { - console.error('Erreur server action:', result.error); - setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); - } - } catch (error) { + // Sauvegarde locale immédiate pour éviter les pertes + localStorage.setItem('columnVisibility', JSON.stringify({ hiddenStatuses })); + + // Sync en arrière-plan avec debounce + clearTimeout(window.columnVisibilitySyncTimeout); + window.columnVisibilitySyncTimeout = setTimeout(() => { + toggleColumnVisibilityAction(status).catch(error => { console.error('Erreur lors du toggle de la visibilité des colonnes:', error); - setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); - } - }); - }, [preferences.columnVisibility]); + }); + }, 1000); // debounce 1s + }, [preferences.columnVisibility.hiddenStatuses]); const isColumnVisible = useCallback((status: TaskStatus) => { return !preferences.columnVisibility.hiddenStatuses.includes(status);