feat: refactor user preferences management

- Marked all user preferences actions as complete in TODO.md.
- Updated `user-preferences-client.ts` to remove outdated mutation methods, now handled by server actions.
- Deleted unused API routes for column visibility, kanban filters, and view preferences.
- Refactored `UserPreferencesContext.tsx` to utilize server actions for updates, improving performance with `useTransition`.
This commit is contained in:
Julien Froidefond
2025-09-18 13:10:04 +02:00
parent cece09d150
commit aeb4e17939
7 changed files with 347 additions and 270 deletions

218
src/actions/preferences.ts Normal file
View File

@@ -0,0 +1,218 @@
'use server';
import { userPreferencesService } from '@/services/user-preferences';
import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import { revalidatePath } from 'next/cache';
/**
* Met à jour les préférences de vue
*/
export async function updateViewPreferences(updates: Partial<ViewPreferences>): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences(updates);
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur updateViewPreferences:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Met à jour les filtres Kanban
*/
export async function updateKanbanFilters(updates: Partial<KanbanFilters>): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateKanbanFilters(updates);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur updateKanbanFilters:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Met à jour la visibilité des colonnes
*/
export async function updateColumnVisibility(updates: Partial<ColumnVisibility>): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const newColumnVisibility: ColumnVisibility = {
...preferences.columnVisibility,
...updates
};
await userPreferencesService.saveColumnVisibility(newColumnVisibility);
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur updateColumnVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la visibilité des objectifs
*/
export async function toggleObjectivesVisibility(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const showObjectives = !preferences.viewPreferences.showObjectives;
await userPreferencesService.updateViewPreferences({ showObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle le mode collapse des objectifs
*/
export async function toggleObjectivesCollapse(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const collapseObjectives = !preferences.viewPreferences.collapseObjectives;
await userPreferencesService.updateViewPreferences({ collapseObjectives });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleObjectivesCollapse:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Change le thème (light/dark)
*/
export async function setTheme(theme: 'light' | 'dark'): Promise<{
success: boolean;
error?: string;
}> {
try {
await userPreferencesService.updateViewPreferences({ theme });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur setTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle le thème entre light et dark
*/
export async function toggleTheme(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await userPreferencesService.updateViewPreferences({ theme: newTheme });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleTheme:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la taille de police
*/
export async function toggleFontSize(): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
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];
await userPreferencesService.updateViewPreferences({ fontSize: newFontSize });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur toggleFontSize:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Toggle la visibilité d'une colonne Kanban
*/
export async function toggleColumnVisibility(status: TaskStatus): Promise<{
success: boolean;
error?: string;
}> {
try {
const preferences = await userPreferencesService.getAllPreferences();
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses);
if (hiddenStatuses.has(status)) {
hiddenStatuses.delete(status);
} else {
hiddenStatuses.add(status);
}
await userPreferencesService.saveColumnVisibility({
hiddenStatuses: Array.from(hiddenStatuses)
});
revalidatePath('/kanban');
return { success: true };
} catch (error) {
console.error('Erreur toggleColumnVisibility:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}

View File

@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* PATCH /api/user-preferences/column-visibility - Met à jour partiellement la visibilité des colonnes
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getColumnVisibility();
await userPreferencesService.saveColumnVisibility({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Visibilité des colonnes mise à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour de la visibilité des colonnes'
},
{ status: 500 }
);
}
}

View File

@@ -1,76 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* GET /api/user-preferences/kanban-filters - Récupère les filtres Kanban
*/
export async function GET() {
try {
const filters = await userPreferencesService.getKanbanFilters();
return NextResponse.json({
success: true,
data: filters
});
} catch (error) {
console.error('Erreur lors de la récupération des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la récupération des filtres Kanban'
},
{ status: 500 }
);
}
}
/**
* PUT /api/user-preferences/kanban-filters - Met à jour les filtres Kanban
*/
export async function PUT(request: NextRequest) {
try {
const filters = await request.json();
await userPreferencesService.saveKanbanFilters(filters);
return NextResponse.json({
success: true,
message: 'Filtres Kanban sauvegardés avec succès'
});
} catch (error) {
console.error('Erreur lors de la sauvegarde des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la sauvegarde des filtres Kanban'
},
{ status: 500 }
);
}
}
/**
* PATCH /api/user-preferences/kanban-filters - Met à jour partiellement les filtres Kanban
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getKanbanFilters();
await userPreferencesService.saveKanbanFilters({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Filtres Kanban mis à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour des filtres Kanban:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour des filtres Kanban'
},
{ status: 500 }
);
}
}

View File

@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* PATCH /api/user-preferences/view-preferences - Met à jour partiellement les préférences de vue
*/
export async function PATCH(request: NextRequest) {
try {
const updates = await request.json();
const current = await userPreferencesService.getViewPreferences();
await userPreferencesService.saveViewPreferences({ ...current, ...updates });
return NextResponse.json({
success: true,
message: 'Préférences de vue mises à jour avec succès'
});
} catch (error) {
console.error('Erreur lors de la mise à jour des préférences de vue:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la mise à jour des préférences de vue'
},
{ status: 500 }
);
}
}

View File

@@ -1,26 +1,37 @@
'use client';
import { createContext, useContext, ReactNode, useState, useCallback, useEffect } from 'react';
import { userPreferencesClient } from '@/clients/user-preferences-client';
import { createContext, useContext, ReactNode, useState, useCallback, useEffect, useTransition } from 'react';
import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import {
updateViewPreferences as updateViewPreferencesAction,
updateKanbanFilters as updateKanbanFiltersAction,
updateColumnVisibility as updateColumnVisibilityAction,
toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
toggleObjectivesCollapse as toggleObjectivesCollapseAction,
toggleTheme as toggleThemeAction,
setTheme as setThemeAction,
toggleFontSize as toggleFontSizeAction,
toggleColumnVisibility as toggleColumnVisibilityAction
} from '@/actions/preferences';
interface UserPreferencesContextType {
preferences: UserPreferences;
isPending: boolean;
// Kanban Filters
updateKanbanFilters: (updates: Partial<KanbanFilters>) => Promise<void>;
updateKanbanFilters: (updates: Partial<KanbanFilters>) => void;
// View Preferences
updateViewPreferences: (updates: Partial<ViewPreferences>) => Promise<void>;
toggleObjectivesVisibility: () => Promise<void>;
toggleObjectivesCollapse: () => Promise<void>;
toggleTheme: () => Promise<void>;
setTheme: (theme: 'light' | 'dark') => Promise<void>;
toggleFontSize: () => Promise<void>;
updateViewPreferences: (updates: Partial<ViewPreferences>) => void;
toggleObjectivesVisibility: () => void;
toggleObjectivesCollapse: () => void;
toggleTheme: () => void;
setTheme: (theme: 'light' | 'dark') => void;
toggleFontSize: () => void;
// Column Visibility
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => Promise<void>;
toggleColumnVisibility: (status: TaskStatus) => Promise<void>;
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => void;
toggleColumnVisibility: (status: TaskStatus) => void;
isColumnVisible: (status: TaskStatus) => boolean;
}
@@ -33,6 +44,7 @@ interface UserPreferencesProviderProps {
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences);
const [isPending, startTransition] = useTransition();
// Synchroniser le thème avec le ThemeProvider global (si disponible)
useEffect(() => {
@@ -44,87 +56,114 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
// === KANBAN FILTERS ===
const updateKanbanFilters = useCallback(async (updates: Partial<KanbanFilters>) => {
const newFilters = { ...preferences.kanbanFilters, ...updates };
setPreferences(prev => ({ ...prev, kanbanFilters: newFilters }));
try {
await userPreferencesClient.updateKanbanFilters(updates);
} catch (error) {
console.warn('Erreur lors de la sauvegarde des filtres Kanban:', error);
// Revert optimistic update on error
setPreferences(prev => ({ ...prev, kanbanFilters: preferences.kanbanFilters }));
}
const updateKanbanFilters = useCallback((updates: Partial<KanbanFilters>) => {
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) {
console.error('Erreur lors de la mise à jour des filtres Kanban:', error);
setPreferences(prev => ({ ...prev, kanbanFilters: originalFilters }));
}
});
}, [preferences.kanbanFilters]);
// === VIEW PREFERENCES ===
const updateViewPreferences = useCallback(async (updates: Partial<ViewPreferences>) => {
const newPreferences = { ...preferences.viewPreferences, ...updates };
setPreferences(prev => ({ ...prev, viewPreferences: newPreferences }));
try {
await userPreferencesClient.updateViewPreferences(updates);
} catch (error) {
console.warn('Erreur lors de la sauvegarde des préférences de vue:', error);
// Revert optimistic update on error
setPreferences(prev => ({ ...prev, viewPreferences: preferences.viewPreferences }));
}
const updateViewPreferences = useCallback((updates: Partial<ViewPreferences>) => {
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) {
console.error('Erreur lors de la mise à jour des préférences de vue:', error);
setPreferences(prev => ({ ...prev, viewPreferences: originalPreferences }));
}
});
}, [preferences.viewPreferences]);
const toggleObjectivesVisibility = useCallback(async () => {
const newValue = !preferences.viewPreferences.showObjectives;
await updateViewPreferences({ showObjectives: newValue });
}, [preferences.viewPreferences.showObjectives, updateViewPreferences]);
const toggleObjectivesVisibility = useCallback(() => {
startTransition(async () => {
await toggleObjectivesVisibilityAction();
});
}, []);
const toggleObjectivesCollapse = useCallback(async () => {
const newValue = !preferences.viewPreferences.objectivesCollapsed;
await updateViewPreferences({ objectivesCollapsed: newValue });
}, [preferences.viewPreferences.objectivesCollapsed, updateViewPreferences]);
const toggleObjectivesCollapse = useCallback(() => {
startTransition(async () => {
await toggleObjectivesCollapseAction();
});
}, []);
const toggleTheme = useCallback(async () => {
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await updateViewPreferences({ theme: newTheme });
}, [preferences.viewPreferences.theme, updateViewPreferences]);
const toggleTheme = useCallback(() => {
startTransition(async () => {
await toggleThemeAction();
});
}, []);
const setTheme = useCallback(async (theme: 'light' | 'dark') => {
await updateViewPreferences({ theme });
}, [updateViewPreferences]);
const setTheme = useCallback((theme: 'light' | 'dark') => {
startTransition(async () => {
await setThemeAction(theme);
});
}, []);
const toggleFontSize = useCallback(async () => {
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];
await updateViewPreferences({ fontSize: newFontSize });
}, [preferences.viewPreferences.fontSize, updateViewPreferences]);
const toggleFontSize = useCallback(() => {
startTransition(async () => {
await toggleFontSizeAction();
});
}, []);
// === COLUMN VISIBILITY ===
const updateColumnVisibility = useCallback(async (updates: Partial<ColumnVisibility>) => {
const newVisibility = { ...preferences.columnVisibility, ...updates };
setPreferences(prev => ({ ...prev, columnVisibility: newVisibility }));
try {
await userPreferencesClient.updateColumnVisibility(updates);
} catch (error) {
console.warn('Erreur lors de la sauvegarde de la visibilité des colonnes:', error);
// Revert optimistic update on error
setPreferences(prev => ({ ...prev, columnVisibility: preferences.columnVisibility }));
}
const updateColumnVisibility = useCallback((updates: Partial<ColumnVisibility>) => {
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) {
console.error('Erreur lors de la mise à jour de la visibilité des colonnes:', error);
setPreferences(prev => ({ ...prev, columnVisibility: originalVisibility }));
}
});
}, [preferences.columnVisibility]);
const toggleColumnVisibility = useCallback(async (status: TaskStatus) => {
const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses);
if (hiddenStatuses.has(status)) {
hiddenStatuses.delete(status);
} else {
hiddenStatuses.add(status);
}
await updateColumnVisibility({ hiddenStatuses: Array.from(hiddenStatuses) });
}, [preferences.columnVisibility.hiddenStatuses, updateColumnVisibility]);
const toggleColumnVisibility = useCallback((status: TaskStatus) => {
startTransition(async () => {
await toggleColumnVisibilityAction(status);
});
}, []);
const isColumnVisible = useCallback((status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status);
@@ -132,6 +171,7 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
const contextValue: UserPreferencesContextType = {
preferences,
isPending,
updateKanbanFilters,
updateViewPreferences,
toggleObjectivesVisibility,