refactor: userpreferences are now in the DB

This commit is contained in:
Julien Froidefond
2025-09-17 08:30:36 +02:00
parent 4f137455f4
commit 14d300c682
24 changed files with 763 additions and 404 deletions

View File

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,76 @@
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

@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from 'next/server';
import { userPreferencesService } from '@/services/user-preferences';
/**
* GET /api/user-preferences - Récupère toutes les préférences utilisateur
*/
export async function GET() {
try {
const preferences = await userPreferencesService.getAllPreferences();
return NextResponse.json({
success: true,
data: preferences
});
} catch (error) {
console.error('Erreur lors de la récupération des préférences:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la récupération des préférences'
},
{ status: 500 }
);
}
}
/**
* PUT /api/user-preferences - Met à jour toutes les préférences utilisateur
*/
export async function PUT(request: NextRequest) {
try {
const preferences = await request.json();
await userPreferencesService.saveAllPreferences(preferences);
return NextResponse.json({
success: true,
message: 'Préférences sauvegardées avec succès'
});
} catch (error) {
console.error('Erreur lors de la sauvegarde des préférences:', error);
return NextResponse.json(
{
success: false,
error: 'Erreur lors de la sauvegarde des préférences'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,28 @@
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,5 +1,6 @@
import { tasksService } from '@/services/tasks';
import { tagsService } from '@/services/tags';
import { userPreferencesService } from '@/services/user-preferences';
import { HomePageClient } from '@/components/HomePageClient';
// Force dynamic rendering (no static generation)
@@ -7,10 +8,11 @@ export const dynamic = 'force-dynamic';
export default async function HomePage() {
// SSR - Récupération des données côté serveur
const [initialTasks, initialStats, initialTags] = await Promise.all([
const [initialTasks, initialStats, initialTags, initialPreferences] = await Promise.all([
tasksService.getTasks(),
tasksService.getTaskStats(),
tagsService.getTags()
tagsService.getTags(),
userPreferencesService.getAllPreferences()
]);
return (
@@ -18,6 +20,7 @@ export default async function HomePage() {
initialTasks={initialTasks}
initialStats={initialStats}
initialTags={initialTags}
initialPreferences={initialPreferences}
/>
);
}

View File

@@ -1,9 +1,9 @@
'use client';
import { createContext, useContext, ReactNode, useState, useMemo, useEffect } from 'react';
import { createContext, useContext, ReactNode, useMemo } from 'react';
import { useTasks } from '@/hooks/useTasks';
import { useTags } from '@/hooks/useTags';
import { userPreferencesService } from '@/services/user-preferences';
import { useUserPreferences } from './UserPreferencesContext';
import { Task, Tag, TaskStats } from '@/lib/types';
import { CreateTaskData, UpdateTaskData, TaskFilters } from '@/clients/tasks-client';
import { KanbanFilters } from '@/components/kanban/KanbanFilters';
@@ -49,48 +49,42 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag
);
const { tags, loading: tagsLoading, error: tagsError } = useTags(initialTags);
// État des filtres Kanban avec persistance
const [kanbanFilters, setKanbanFilters] = useState<KanbanFilters>({});
const { preferences, updateKanbanFilters, updateViewPreferences } = useUserPreferences();
// Charger les préférences au montage
useEffect(() => {
const savedFilters = userPreferencesService.getKanbanFilters();
const savedViewPrefs = userPreferencesService.getViewPreferences();
setKanbanFilters({
search: savedFilters.search,
tags: savedFilters.tags,
priorities: savedFilters.priorities,
showCompleted: savedFilters.showCompleted,
sortBy: savedFilters.sortBy || createSortKey('priority', 'desc'), // Tri par défaut
compactView: savedViewPrefs.compactView,
swimlanesByTags: savedViewPrefs.swimlanesByTags,
swimlanesMode: savedViewPrefs.swimlanesMode || 'tags'
});
}, []);
// Construire l'objet KanbanFilters à partir des préférences
const kanbanFilters: KanbanFilters = useMemo(() => ({
search: preferences.kanbanFilters.search || '',
tags: preferences.kanbanFilters.tags || [],
priorities: preferences.kanbanFilters.priorities || [],
showCompleted: preferences.kanbanFilters.showCompleted ?? true,
sortBy: preferences.kanbanFilters.sortBy || createSortKey('priority', 'desc'),
compactView: preferences.viewPreferences.compactView || false,
swimlanesByTags: preferences.viewPreferences.swimlanesByTags || false,
swimlanesMode: preferences.viewPreferences.swimlanesMode || 'tags'
}), [preferences]);
// Fonction pour mettre à jour les filtres avec persistance
const updateKanbanFilters = (newFilters: KanbanFilters) => {
setKanbanFilters(newFilters);
// Sauvegarder les filtres
userPreferencesService.saveKanbanFilters({
const setKanbanFilters = async (newFilters: KanbanFilters) => {
// Séparer les vrais filtres des préférences de vue
const kanbanFilterUpdates = {
search: newFilters.search,
tags: newFilters.tags,
priorities: newFilters.priorities,
showCompleted: newFilters.showCompleted,
sortBy: newFilters.sortBy
});
};
// Sauvegarder les préférences de vue
userPreferencesService.saveViewPreferences({
compactView: newFilters.compactView || false,
swimlanesByTags: newFilters.swimlanesByTags || false,
swimlanesMode: newFilters.swimlanesMode || 'tags',
showObjectives: true, // Toujours visible pour l'instant
showFilters: true // Toujours visible pour l'instant
});
const viewPreferenceUpdates = {
compactView: newFilters.compactView,
swimlanesByTags: newFilters.swimlanesByTags,
swimlanesMode: newFilters.swimlanesMode
};
// Mettre à jour via UserPreferencesContext
await Promise.all([
updateKanbanFilters(kanbanFilterUpdates),
updateViewPreferences(viewPreferenceUpdates)
]);
};
// Séparer les tâches épinglées (objectifs) des autres et les trier
@@ -176,7 +170,7 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag
tagsLoading,
tagsError,
kanbanFilters,
setKanbanFilters: updateKanbanFilters,
setKanbanFilters,
filteredTasks,
pinnedTasks
};

View File

@@ -14,34 +14,22 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: ReactNode;
initialTheme?: Theme;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>('dark');
export function ThemeProvider({ children, initialTheme = 'dark' }: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(initialTheme);
const [mounted, setMounted] = useState(false);
// Hydration safe initialization
useEffect(() => {
setMounted(true);
// Check localStorage first
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setThemeState(savedTheme);
return;
}
// Fallback to system preference
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
setThemeState('light');
}
}, []);
// Apply theme class to document
useEffect(() => {
if (mounted) {
document.documentElement.className = theme;
localStorage.setItem('theme', theme);
}
}, [theme, mounted]);
@@ -55,7 +43,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
<div className={mounted ? theme : 'dark'}>
<div className={mounted ? theme : initialTheme}>
{children}
</div>
</ThemeContext.Provider>

View File

@@ -0,0 +1,150 @@
'use client';
import { createContext, useContext, ReactNode, useState, useCallback, useEffect } from 'react';
import { userPreferencesClient } from '@/clients/user-preferences-client';
import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
interface UserPreferencesContextType {
preferences: UserPreferences;
// Kanban Filters
updateKanbanFilters: (updates: Partial<KanbanFilters>) => Promise<void>;
// View Preferences
updateViewPreferences: (updates: Partial<ViewPreferences>) => Promise<void>;
toggleObjectivesVisibility: () => Promise<void>;
toggleObjectivesCollapse: () => Promise<void>;
toggleTheme: () => Promise<void>;
setTheme: (theme: 'light' | 'dark') => Promise<void>;
// Column Visibility
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => Promise<void>;
toggleColumnVisibility: (status: TaskStatus) => Promise<void>;
isColumnVisible: (status: TaskStatus) => boolean;
}
const UserPreferencesContext = createContext<UserPreferencesContextType | null>(null);
interface UserPreferencesProviderProps {
children: ReactNode;
initialPreferences: UserPreferences;
}
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences);
// Synchroniser le thème avec le ThemeProvider global (si disponible)
useEffect(() => {
if (typeof window !== 'undefined') {
// Appliquer le thème au document
document.documentElement.className = preferences.viewPreferences.theme;
}
}, [preferences.viewPreferences.theme]);
// === 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 }));
}
}, [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 }));
}
}, [preferences.viewPreferences]);
const toggleObjectivesVisibility = useCallback(async () => {
const newValue = !preferences.viewPreferences.showObjectives;
await updateViewPreferences({ showObjectives: newValue });
}, [preferences.viewPreferences.showObjectives, updateViewPreferences]);
const toggleObjectivesCollapse = useCallback(async () => {
const newValue = !preferences.viewPreferences.objectivesCollapsed;
await updateViewPreferences({ objectivesCollapsed: newValue });
}, [preferences.viewPreferences.objectivesCollapsed, updateViewPreferences]);
const toggleTheme = useCallback(async () => {
const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark';
await updateViewPreferences({ theme: newTheme });
}, [preferences.viewPreferences.theme, updateViewPreferences]);
const setTheme = useCallback(async (theme: 'light' | 'dark') => {
await updateViewPreferences({ theme });
}, [updateViewPreferences]);
// === 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 }));
}
}, [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 isColumnVisible = useCallback((status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status);
}, [preferences.columnVisibility.hiddenStatuses]);
const contextValue: UserPreferencesContextType = {
preferences,
updateKanbanFilters,
updateViewPreferences,
toggleObjectivesVisibility,
toggleObjectivesCollapse,
toggleTheme,
setTheme,
updateColumnVisibility,
toggleColumnVisibility,
isColumnVisible
};
return (
<UserPreferencesContext.Provider value={contextValue}>
{children}
</UserPreferencesContext.Provider>
);
}
export function useUserPreferences() {
const context = useContext(UserPreferencesContext);
if (!context) {
throw new Error('useUserPreferences must be used within a UserPreferencesProvider');
}
return context;
}