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:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user