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.
This commit is contained in:
Julien Froidefond
2025-10-02 12:31:29 +02:00
parent 63ef861360
commit 2e3e8bb222

View File

@@ -1,6 +1,15 @@
'use client'; 'use client';
import { createContext, useContext, ReactNode, useState, useCallback, useEffect, useTransition } from 'react'; 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 { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import { import {
updateViewPreferences as updateViewPreferencesAction, updateViewPreferences as updateViewPreferencesAction,
@@ -79,7 +88,7 @@ const defaultPreferences: UserPreferences = {
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) { export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences); const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
const [isPending, startTransition] = useTransition(); const [isPending] = useTransition();
const { toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme(); const { toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme();
const { status } = useSession(); const { status } = useSession();
@@ -116,63 +125,63 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
// === KANBAN FILTERS === // === KANBAN FILTERS ===
const updateKanbanFilters = useCallback((updates: Partial<KanbanFilters>) => { const updateKanbanFilters = useCallback((updates: Partial<KanbanFilters>) => {
startTransition(async () => { // Optimistic update immédiat
const originalFilters = preferences.kanbanFilters; setPreferences(prev => ({
...prev,
kanbanFilters: { ...prev.kanbanFilters, ...updates }
}));
// Optimistic update // Sauvegarde locale immédiate pour éviter les pertes
setPreferences(prev => ({ const newFilters = { ...preferences.kanbanFilters, ...updates };
...prev, localStorage.setItem('kanbanFilters', JSON.stringify(newFilters));
kanbanFilters: { ...prev.kanbanFilters, ...updates }
}));
try { // Sync en arrière-plan avec debounce
const result = await updateKanbanFiltersAction(updates); clearTimeout(window.kanbanFiltersSyncTimeout);
if (!result.success) { window.kanbanFiltersSyncTimeout = setTimeout(() => {
console.error('Erreur server action:', result.error); updateKanbanFiltersAction(updates).catch(error => {
setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters }));
}
} catch (error) {
console.error('Erreur lors de la mise à jour des filtres Kanban:', error); console.error('Erreur lors de la mise à jour des filtres Kanban:', error);
setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters })); });
} }, 1000); // debounce 1s
});
}, [preferences.kanbanFilters]); }, [preferences.kanbanFilters]);
// === VIEW PREFERENCES === // === VIEW PREFERENCES ===
const updateViewPreferences = useCallback((updates: Partial<ViewPreferences>) => { const updateViewPreferences = useCallback((updates: Partial<ViewPreferences>) => {
startTransition(async () => { // Optimistic update immédiat
const originalPreferences = preferences.viewPreferences; setPreferences(prev => ({
...prev,
viewPreferences: { ...prev.viewPreferences, ...updates }
}));
// Optimistic update // Sauvegarde locale immédiate pour éviter les pertes
setPreferences(prev => ({ const newViewPrefs = { ...preferences.viewPreferences, ...updates };
...prev, localStorage.setItem('viewPreferences', JSON.stringify(newViewPrefs));
viewPreferences: { ...prev.viewPreferences, ...updates }
}));
try { // Sync en arrière-plan avec debounce
const result = await updateViewPreferencesAction(updates); clearTimeout(window.viewPreferencesSyncTimeout);
if (!result.success) { window.viewPreferencesSyncTimeout = setTimeout(() => {
console.error('Erreur server action:', result.error); updateViewPreferencesAction(updates).catch(error => {
setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences }));
}
} catch (error) {
console.error('Erreur lors de la mise à jour des préférences de vue:', 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]); }, [preferences.viewPreferences]);
const toggleObjectivesVisibility = useCallback(() => { const toggleObjectivesVisibility = useCallback(() => {
startTransition(async () => { // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI
await toggleObjectivesVisibilityAction(); setTimeout(() => {
}); toggleObjectivesVisibilityAction().catch(error => {
console.error('Erreur lors du toggle de la visibilité des objectifs:', error);
});
}, 0);
}, []); }, []);
const toggleObjectivesCollapse = useCallback(() => { const toggleObjectivesCollapse = useCallback(() => {
startTransition(async () => { // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI
await toggleObjectivesCollapseAction(); setTimeout(() => {
}); toggleObjectivesCollapseAction().catch(error => {
console.error('Erreur lors du toggle du collapse des objectifs:', error);
});
}, 0);
}, []); }, []);
const toggleTheme = useCallback(() => { const toggleTheme = useCallback(() => {
@@ -184,90 +193,76 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
}, [themeSetTheme]); }, [themeSetTheme]);
const toggleFontSize = useCallback(() => { const toggleFontSize = useCallback(() => {
startTransition(async () => { // Optimistic update - cycle through font sizes
const originalPreferences = preferences.viewPreferences; 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];
// Optimistic update - cycle through font sizes setPreferences(prev => ({
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; ...prev,
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); viewPreferences: { ...prev.viewPreferences, fontSize: newFontSize }
const nextIndex = (currentIndex + 1) % fontSizes.length; }));
const newFontSize = fontSizes[nextIndex];
setPreferences(prev => ({ // Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI
...prev, setTimeout(() => {
viewPreferences: { ...prev.viewPreferences, fontSize: newFontSize } toggleFontSizeAction().catch(error => {
}));
try {
const result = await toggleFontSizeAction();
if (!result.success) {
console.error('Erreur server action:', result.error);
setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences }));
}
} catch (error) {
console.error('Erreur lors du toggle de la taille de police:', error); console.error('Erreur lors du toggle de la taille de police:', error);
setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences })); });
} }, 0);
}); }, [preferences.viewPreferences.fontSize]);
}, [preferences.viewPreferences]);
// === COLUMN VISIBILITY === // === COLUMN VISIBILITY ===
const updateColumnVisibility = useCallback((updates: Partial<ColumnVisibility>) => { const updateColumnVisibility = useCallback((updates: Partial<ColumnVisibility>) => {
startTransition(async () => { // Optimistic update immédiat
const originalVisibility = preferences.columnVisibility; setPreferences(prev => ({
...prev,
columnVisibility: { ...prev.columnVisibility, ...updates }
}));
// Optimistic update // Sauvegarde locale immédiate pour éviter les pertes
setPreferences(prev => ({ const newColumnVisibility = { ...preferences.columnVisibility, ...updates };
...prev, localStorage.setItem('columnVisibility', JSON.stringify(newColumnVisibility));
columnVisibility: { ...prev.columnVisibility, ...updates }
}));
try { // Sync en arrière-plan avec debounce
const result = await updateColumnVisibilityAction(updates); clearTimeout(window.columnVisibilitySyncTimeout);
if (!result.success) { window.columnVisibilitySyncTimeout = setTimeout(() => {
console.error('Erreur server action:', result.error); updateColumnVisibilityAction(updates).catch(error => {
setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility }));
}
} catch (error) {
console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', 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]); }, [preferences.columnVisibility]);
const toggleColumnVisibility = useCallback((status: TaskStatus) => { const toggleColumnVisibility = useCallback((status: TaskStatus) => {
startTransition(async () => { const hiddenStatuses = [...preferences.columnVisibility.hiddenStatuses];
const originalVisibility = preferences.columnVisibility;
const hiddenStatuses = [...preferences.columnVisibility.hiddenStatuses];
// Optimistic update // Optimistic update
if (hiddenStatuses.includes(status)) { if (hiddenStatuses.includes(status)) {
// Remove from hidden // Remove from hidden
const index = hiddenStatuses.indexOf(status); const index = hiddenStatuses.indexOf(status);
hiddenStatuses.splice(index, 1); hiddenStatuses.splice(index, 1);
} else { } else {
// Add to hidden // Add to hidden
hiddenStatuses.push(status); hiddenStatuses.push(status);
} }
setPreferences(prev => ({ setPreferences(prev => ({
...prev, ...prev,
columnVisibility: { ...prev.columnVisibility, hiddenStatuses } columnVisibility: { ...prev.columnVisibility, hiddenStatuses }
})); }));
try { // Sauvegarde locale immédiate pour éviter les pertes
const result = await toggleColumnVisibilityAction(status); localStorage.setItem('columnVisibility', JSON.stringify({ hiddenStatuses }));
if (!result.success) {
console.error('Erreur server action:', result.error); // Sync en arrière-plan avec debounce
setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); clearTimeout(window.columnVisibilitySyncTimeout);
} window.columnVisibilitySyncTimeout = setTimeout(() => {
} catch (error) { toggleColumnVisibilityAction(status).catch(error => {
console.error('Erreur lors du toggle de la visibilité des colonnes:', error); console.error('Erreur lors du toggle de la visibilité des colonnes:', error);
setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility })); });
} }, 1000); // debounce 1s
}); }, [preferences.columnVisibility.hiddenStatuses]);
}, [preferences.columnVisibility]);
const isColumnVisible = useCallback((status: TaskStatus) => { const isColumnVisible = useCallback((status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status); return !preferences.columnVisibility.hiddenStatuses.includes(status);