chore: prettier everywhere

This commit is contained in:
Julien Froidefond
2025-10-09 13:40:03 +02:00
parent f8100ae3e9
commit d9cf9a2655
303 changed files with 15420 additions and 9391 deletions

View File

@@ -1,8 +1,19 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from 'react';
import { useUserPreferences } from './UserPreferencesContext';
import { PRESET_BACKGROUNDS, BACKGROUND_NAMES, TOAST_ICONS, getNextBackground } from '@/lib/ui-config';
import {
PRESET_BACKGROUNDS,
BACKGROUND_NAMES,
TOAST_ICONS,
getNextBackground,
} from '@/lib/ui-config';
import { useSession } from 'next-auth/react';
import { useToast } from '@/components/ui/Toast';
@@ -12,7 +23,9 @@ interface BackgroundContextType {
cycleBackground: () => void;
}
const BackgroundContext = createContext<BackgroundContextType | undefined>(undefined);
const BackgroundContext = createContext<BackgroundContextType | undefined>(
undefined
);
interface BackgroundProviderProps {
children: ReactNode;
@@ -22,9 +35,9 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
const { preferences, updateViewPreferences } = useUserPreferences();
const { data: session } = useSession();
const { showToast } = useToast();
const [backgroundImage, setBackgroundImageState] = useState<string | undefined>(
preferences?.viewPreferences?.backgroundImage
);
const [backgroundImage, setBackgroundImageState] = useState<
string | undefined
>(preferences?.viewPreferences?.backgroundImage);
const [backgroundBlur, setBackgroundBlurState] = useState<number>(
preferences?.viewPreferences?.backgroundBlur || 0
);
@@ -41,28 +54,41 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
// Sync with preferences (only for authenticated users)
useEffect(() => {
// Only sync if user is authenticated
if (session?.user?.id && preferences?.viewPreferences?.backgroundImage !== backgroundImage) {
if (
session?.user?.id &&
preferences?.viewPreferences?.backgroundImage !== backgroundImage
) {
setBackgroundImageState(preferences?.viewPreferences?.backgroundImage);
}
if (preferences?.viewPreferences?.backgroundBlur !== backgroundBlur) {
setBackgroundBlurState(preferences?.viewPreferences?.backgroundBlur || 0);
}
if (preferences?.viewPreferences?.backgroundOpacity !== backgroundOpacity) {
setBackgroundOpacityState(preferences?.viewPreferences?.backgroundOpacity || 100);
setBackgroundOpacityState(
preferences?.viewPreferences?.backgroundOpacity || 100
);
}
}, [preferences?.viewPreferences?.backgroundImage, preferences?.viewPreferences?.backgroundBlur, preferences?.viewPreferences?.backgroundOpacity, backgroundImage, backgroundBlur, backgroundOpacity, session?.user?.id]);
}, [
preferences?.viewPreferences?.backgroundImage,
preferences?.viewPreferences?.backgroundBlur,
preferences?.viewPreferences?.backgroundOpacity,
backgroundImage,
backgroundBlur,
backgroundOpacity,
session?.user?.id,
]);
// Apply background image to document body
useEffect(() => {
if (mounted) {
const body = document.body;
// Supprimer l'ancien élément de fond s'il existe
const existingBackground = document.getElementById('custom-background');
if (existingBackground) {
existingBackground.remove();
}
if (backgroundImage) {
console.log('Creating background element for:', backgroundImage);
// Créer un élément div pour l'image de fond avec les effets
@@ -75,13 +101,18 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
backgroundElement.style.height = '100%';
backgroundElement.style.zIndex = '-1';
backgroundElement.style.pointerEvents = 'none';
// Vérifier si c'est une URL d'image ou un preset
const presetIds = PRESET_BACKGROUNDS.map(preset => preset.id);
if (backgroundImage && presetIds.includes(backgroundImage as typeof presetIds[number])) {
const presetIds = PRESET_BACKGROUNDS.map((preset) => preset.id);
if (
backgroundImage &&
presetIds.includes(backgroundImage as (typeof presetIds)[number])
) {
// Trouver le preset correspondant
const preset = PRESET_BACKGROUNDS.find(p => p.id === backgroundImage);
const preset = PRESET_BACKGROUNDS.find(
(p) => p.id === backgroundImage
);
if (preset) {
// Appliquer le gradient directement
backgroundElement.style.background = preset.preview;
@@ -93,14 +124,14 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
backgroundElement.style.backgroundImage = `url(${backgroundImage})`;
backgroundElement.className = 'custom-background';
}
// Appliquer les propriétés communes
backgroundElement.style.backgroundSize = 'cover';
backgroundElement.style.backgroundPosition = 'center';
backgroundElement.style.backgroundRepeat = 'no-repeat';
backgroundElement.style.filter = `blur(${backgroundBlur}px)`;
backgroundElement.style.opacity = `${backgroundOpacity / 100}`;
// Ajouter l'élément au body
body.appendChild(backgroundElement);
body.classList.add('has-background-image');
@@ -118,24 +149,31 @@ export function BackgroundProvider({ children }: BackgroundProviderProps) {
const cycleBackground = () => {
const currentBackground = backgroundImage; // Utiliser le state local au lieu des préférences
const customImages = preferences?.viewPreferences?.customImages || [];
const nextBackground = getNextBackground(currentBackground || 'none', customImages);
const newBackgroundImage = nextBackground === 'none' ? undefined : nextBackground;
const nextBackground = getNextBackground(
currentBackground || 'none',
customImages
);
const newBackgroundImage =
nextBackground === 'none' ? undefined : nextBackground;
setBackgroundImageState(newBackgroundImage);
// Sauvegarder seulement si l'utilisateur est authentifié
if (session?.user?.id) {
updateViewPreferences({ backgroundImage: newBackgroundImage });
}
// Afficher le toast avec le nom du background
const backgroundName = BACKGROUND_NAMES[nextBackground] || 'Image personnalisée';
const backgroundName =
BACKGROUND_NAMES[nextBackground] || 'Image personnalisée';
showToast(`Background: ${backgroundName}`, 2000, TOAST_ICONS.background);
};
return (
<BackgroundContext.Provider value={{ backgroundImage, setBackgroundImage, cycleBackground }}>
<BackgroundContext.Provider
value={{ backgroundImage, setBackgroundImage, cycleBackground }}
>
{children}
</BackgroundContext.Provider>
);

View File

@@ -8,20 +8,22 @@ interface JiraConfigContextType {
isConfigured: boolean;
}
const JiraConfigContext = createContext<JiraConfigContextType | undefined>(undefined);
const JiraConfigContext = createContext<JiraConfigContextType | undefined>(
undefined
);
interface JiraConfigProviderProps {
children: React.ReactNode;
config: JiraConfig;
}
export function JiraConfigProvider({ children, config }: JiraConfigProviderProps) {
export function JiraConfigProvider({
children,
config,
}: JiraConfigProviderProps) {
// Une config Jira est considérée comme valide si elle a les champs obligatoires
const isConfigured = Boolean(
config.baseUrl &&
config.email &&
config.apiToken &&
config.enabled
config.baseUrl && config.email && config.apiToken && config.enabled
);
return (

View File

@@ -7,7 +7,12 @@ import { useUserPreferences } from './UserPreferencesContext';
import { Task, Tag, TaskStats, TaskStatus } from '@/lib/types';
import { CreateTaskData, TaskFilters } from '@/clients/tasks-client';
import type { KanbanFilters } from '@/lib/types';
import { sortTasks, getSortOption, DEFAULT_SORT, createSortKey } from '@/lib/sort-config';
import {
sortTasks,
getSortOption,
DEFAULT_SORT,
createSortKey,
} from '@/lib/sort-config';
interface TasksContextType {
tasks: Task[]; // Toutes les tâches
@@ -17,7 +22,10 @@ interface TasksContextType {
syncing: boolean;
error: string | null;
createTask: (data: CreateTaskData) => Promise<Task | null>;
updateTaskOptimistic: (taskId: string, status: TaskStatus) => Promise<Task | null>;
updateTaskOptimistic: (
taskId: string,
status: TaskStatus
) => Promise<Task | null>;
refreshTasks: () => Promise<void>;
setFilters: (filters: TaskFilters) => void;
// Kanban filters
@@ -41,40 +49,51 @@ interface TasksProviderProps {
initialStats?: TaskStats;
}
export function TasksProvider({ children, initialTasks, initialTags, initialStats }: TasksProviderProps) {
export function TasksProvider({
children,
initialTasks,
initialTags,
initialStats,
}: TasksProviderProps) {
const tasksState = useTasks(
{ limit: 20 },
{ tasks: initialTasks, stats: initialStats }
);
const { tags, loading: tagsLoading, error: tagsError } = useTags(initialTags);
const { preferences, updateKanbanFilters, updateViewPreferences } = useUserPreferences();
const { preferences, updateKanbanFilters, updateViewPreferences } =
useUserPreferences();
// 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',
showWithDueDate: preferences.kanbanFilters.showWithDueDate || false,
showCompletedLast7Days: preferences.kanbanFilters.showCompletedLast7Days || false,
// Filtres Jira
showJiraOnly: preferences.kanbanFilters.showJiraOnly || false,
hideJiraTasks: preferences.kanbanFilters.hideJiraTasks || false,
jiraProjects: preferences.kanbanFilters.jiraProjects || [],
jiraTypes: preferences.kanbanFilters.jiraTypes || [],
// Filtres TFS
showTfsOnly: preferences.kanbanFilters.showTfsOnly || false,
hideTfsTasks: preferences.kanbanFilters.hideTfsTasks || false,
tfsProjects: preferences.kanbanFilters.tfsProjects || [],
// Filtres Manuel
showManualOnly: preferences.kanbanFilters.showManualOnly || false,
hideManualTasks: preferences.kanbanFilters.hideManualTasks || false
}), [preferences]);
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',
showWithDueDate: preferences.kanbanFilters.showWithDueDate || false,
showCompletedLast7Days:
preferences.kanbanFilters.showCompletedLast7Days || false,
// Filtres Jira
showJiraOnly: preferences.kanbanFilters.showJiraOnly || false,
hideJiraTasks: preferences.kanbanFilters.hideJiraTasks || false,
jiraProjects: preferences.kanbanFilters.jiraProjects || [],
jiraTypes: preferences.kanbanFilters.jiraTypes || [],
// Filtres TFS
showTfsOnly: preferences.kanbanFilters.showTfsOnly || false,
hideTfsTasks: preferences.kanbanFilters.hideTfsTasks || false,
tfsProjects: preferences.kanbanFilters.tfsProjects || [],
// Filtres Manuel
showManualOnly: preferences.kanbanFilters.showManualOnly || false,
hideManualTasks: preferences.kanbanFilters.hideManualTasks || false,
}),
[preferences]
);
// Fonction pour mettre à jour les filtres avec persistance
const setKanbanFilters = async (newFilters: KanbanFilters) => {
@@ -98,68 +117,75 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
tfsProjects: newFilters.tfsProjects,
// Filtres Manuel
showManualOnly: newFilters.showManualOnly,
hideManualTasks: newFilters.hideManualTasks
hideManualTasks: newFilters.hideManualTasks,
};
const viewPreferenceUpdates = {
compactView: newFilters.compactView as boolean,
swimlanesByTags: newFilters.swimlanesByTags as boolean,
swimlanesMode: newFilters.swimlanesMode as 'tags' | 'priority'
swimlanesMode: newFilters.swimlanesMode as 'tags' | 'priority',
};
// Mettre à jour via UserPreferencesContext
await Promise.all([
updateKanbanFilters(kanbanFilterUpdates),
updateViewPreferences(viewPreferenceUpdates)
updateViewPreferences(viewPreferenceUpdates),
]);
};
// Séparer les tâches épinglées (objectifs) des autres et les trier
const { pinnedTasks, regularTasks } = useMemo(() => {
const pinnedTagNames = tags.filter(tag => tag.isPinned).map(tag => tag.name);
const pinnedTagNames = tags
.filter((tag) => tag.isPinned)
.map((tag) => tag.name);
const pinned: Task[] = [];
const regular: Task[] = [];
tasksState.tasks.forEach(task => {
const hasPinnedTag = task.tags?.some(tagName => pinnedTagNames.includes(tagName));
tasksState.tasks.forEach((task) => {
const hasPinnedTag = task.tags?.some((tagName) =>
pinnedTagNames.includes(tagName)
);
if (hasPinnedTag) {
pinned.push(task);
} else {
regular.push(task);
}
});
// Trier les tâches épinglées avec le même tri que les autres
const sortedPinned = kanbanFilters.sortBy ?
(() => {
const sortOption = getSortOption(kanbanFilters.sortBy);
return sortOption ?
sortTasks(pinned, [{ field: sortOption.field, direction: sortOption.direction }]) :
sortTasks(pinned, DEFAULT_SORT);
})() :
sortTasks(pinned, DEFAULT_SORT);
const sortedPinned = kanbanFilters.sortBy
? (() => {
const sortOption = getSortOption(kanbanFilters.sortBy);
return sortOption
? sortTasks(pinned, [
{ field: sortOption.field, direction: sortOption.direction },
])
: sortTasks(pinned, DEFAULT_SORT);
})()
: sortTasks(pinned, DEFAULT_SORT);
return { pinnedTasks: sortedPinned, regularTasks: regular };
}, [tasksState.tasks, tags, kanbanFilters.sortBy]);
// Calcul du nombre de filtres actifs
const activeFiltersCount = useMemo(() => {
return (kanbanFilters.tags?.filter(Boolean).length || 0) +
(kanbanFilters.priorities?.filter(Boolean).length || 0) +
(kanbanFilters.search ? 1 : 0) +
(kanbanFilters.showWithDueDate ? 1 : 0) +
(kanbanFilters.showCompletedLast7Days ? 1 : 0) +
(kanbanFilters.jiraProjects?.filter(Boolean).length || 0) +
(kanbanFilters.jiraTypes?.filter(Boolean).length || 0) +
(kanbanFilters.showJiraOnly ? 1 : 0) +
(kanbanFilters.hideJiraTasks ? 1 : 0) +
(kanbanFilters.tfsProjects?.filter(Boolean).length || 0) +
(kanbanFilters.showTfsOnly ? 1 : 0) +
(kanbanFilters.hideTfsTasks ? 1 : 0) +
(kanbanFilters.showManualOnly ? 1 : 0) +
(kanbanFilters.hideManualTasks ? 1 : 0);
return (
(kanbanFilters.tags?.filter(Boolean).length || 0) +
(kanbanFilters.priorities?.filter(Boolean).length || 0) +
(kanbanFilters.search ? 1 : 0) +
(kanbanFilters.showWithDueDate ? 1 : 0) +
(kanbanFilters.showCompletedLast7Days ? 1 : 0) +
(kanbanFilters.jiraProjects?.filter(Boolean).length || 0) +
(kanbanFilters.jiraTypes?.filter(Boolean).length || 0) +
(kanbanFilters.showJiraOnly ? 1 : 0) +
(kanbanFilters.hideJiraTasks ? 1 : 0) +
(kanbanFilters.tfsProjects?.filter(Boolean).length || 0) +
(kanbanFilters.showTfsOnly ? 1 : 0) +
(kanbanFilters.hideTfsTasks ? 1 : 0) +
(kanbanFilters.showManualOnly ? 1 : 0) +
(kanbanFilters.hideManualTasks ? 1 : 0)
);
}, [kanbanFilters]);
// Filtrage et tri des tâches régulières (pas les épinglées)
@@ -169,85 +195,91 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
// Filtre par recherche
if (kanbanFilters.search) {
const searchLower = kanbanFilters.search.toLowerCase();
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(searchLower) ||
task.description?.toLowerCase().includes(searchLower) ||
task.tags?.some(tag => tag.toLowerCase().includes(searchLower))
filtered = filtered.filter(
(task) =>
task.title.toLowerCase().includes(searchLower) ||
task.description?.toLowerCase().includes(searchLower) ||
task.tags?.some((tag) => tag.toLowerCase().includes(searchLower))
);
}
// Filtre par tags
if (kanbanFilters.tags?.length) {
filtered = filtered.filter(task =>
kanbanFilters.tags!.some(filterTag =>
task.tags?.includes(filterTag)
)
filtered = filtered.filter((task) =>
kanbanFilters.tags!.some((filterTag) => task.tags?.includes(filterTag))
);
}
// Filtre par priorités
if (kanbanFilters.priorities?.length) {
filtered = filtered.filter(task =>
filtered = filtered.filter((task) =>
kanbanFilters.priorities!.includes(task.priority)
);
}
// Filtres spécifiques Jira
if (kanbanFilters.showJiraOnly) {
filtered = filtered.filter(task => task.source === 'jira');
filtered = filtered.filter((task) => task.source === 'jira');
} else if (kanbanFilters.hideJiraTasks) {
filtered = filtered.filter(task => task.source !== 'jira');
filtered = filtered.filter((task) => task.source !== 'jira');
}
// Filtre par projets Jira
if (kanbanFilters.jiraProjects?.length) {
filtered = filtered.filter(task =>
task.source !== 'jira' || kanbanFilters.jiraProjects!.includes(task.jiraProject || '')
filtered = filtered.filter(
(task) =>
task.source !== 'jira' ||
kanbanFilters.jiraProjects!.includes(task.jiraProject || '')
);
}
// Filtre par types Jira
if (kanbanFilters.jiraTypes?.length) {
filtered = filtered.filter(task =>
task.source !== 'jira' || kanbanFilters.jiraTypes!.includes(task.jiraType || '')
filtered = filtered.filter(
(task) =>
task.source !== 'jira' ||
kanbanFilters.jiraTypes!.includes(task.jiraType || '')
);
}
// Filtres spécifiques TFS
if (kanbanFilters.showTfsOnly) {
filtered = filtered.filter(task => task.source === 'tfs');
filtered = filtered.filter((task) => task.source === 'tfs');
} else if (kanbanFilters.hideTfsTasks) {
filtered = filtered.filter(task => task.source !== 'tfs');
filtered = filtered.filter((task) => task.source !== 'tfs');
}
// Filtres spécifiques Manuel
if (kanbanFilters.showManualOnly) {
filtered = filtered.filter(task => task.source === 'manual');
filtered = filtered.filter((task) => task.source === 'manual');
} else if (kanbanFilters.hideManualTasks) {
filtered = filtered.filter(task => task.source !== 'manual');
filtered = filtered.filter((task) => task.source !== 'manual');
}
// Filtre par projets TFS
if (kanbanFilters.tfsProjects?.length) {
filtered = filtered.filter(task =>
task.source !== 'tfs' || kanbanFilters.tfsProjects!.includes(task.tfsProject || '')
filtered = filtered.filter(
(task) =>
task.source !== 'tfs' ||
kanbanFilters.tfsProjects!.includes(task.tfsProject || '')
);
}
// Filtre par date de fin
if (kanbanFilters.showWithDueDate) {
filtered = filtered.filter(task => task.dueDate != null);
filtered = filtered.filter((task) => task.dueDate != null);
}
// Filtre par tâches complétées les 7 derniers jours
if (kanbanFilters.showCompletedLast7Days) {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
filtered = filtered.filter(task =>
task.status === 'done' &&
task.completedAt &&
task.completedAt >= sevenDaysAgo
filtered = filtered.filter(
(task) =>
task.status === 'done' &&
task.completedAt &&
task.completedAt >= sevenDaysAgo
);
}
@@ -255,10 +287,12 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
if (kanbanFilters.sortBy) {
const sortOption = getSortOption(kanbanFilters.sortBy);
if (sortOption) {
filtered = sortTasks(filtered, [{
field: sortOption.field,
direction: sortOption.direction
}]);
filtered = sortTasks(filtered, [
{
field: sortOption.field,
direction: sortOption.direction,
},
]);
}
} else {
// Tri par défaut (priorité desc + tags asc)
@@ -278,7 +312,7 @@ export function TasksProvider({ children, initialTasks, initialTags, initialStat
setKanbanFilters,
filteredTasks,
pinnedTasks,
activeFiltersCount
activeFiltersCount,
};
return (

View File

@@ -1,9 +1,21 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from 'react';
import { updateViewPreferences } from '@/actions/preferences';
import { useToast } from '@/components/ui/Toast';
import { Theme, THEME_CONFIG, getNextDarkTheme, THEME_NAMES, getThemeIcon } from '@/lib/ui-config';
import {
Theme,
THEME_CONFIG,
getNextDarkTheme,
THEME_NAMES,
getThemeIcon,
} from '@/lib/ui-config';
import { useSession } from 'next-auth/react';
interface ThemeContextType {
@@ -22,9 +34,15 @@ interface ThemeProviderProps {
userPreferredTheme?: Theme;
}
export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTheme: initialUserPreferredTheme = 'dark' }: ThemeProviderProps) {
export function ThemeProvider({
children,
initialTheme = 'dark',
userPreferredTheme: initialUserPreferredTheme = 'dark',
}: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(initialTheme);
const [userPreferredTheme, setUserPreferredTheme] = useState<Theme>(initialUserPreferredTheme);
const [userPreferredTheme, setUserPreferredTheme] = useState<Theme>(
initialUserPreferredTheme
);
const [mounted, setMounted] = useState(false);
const { showToast } = useToast();
const { data: session } = useSession();
@@ -48,12 +66,12 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
// Toggle between light and the user's chosen dark theme
const newTheme = theme === 'light' ? userPreferredTheme : 'light';
setThemeState(newTheme);
// Sauvegarder en base seulement si l'utilisateur est authentifié
if (session?.user?.id) {
try {
const result = await updateViewPreferences({
theme: newTheme
theme: newTheme,
});
if (!result.success) {
console.error('Erreur lors de la sauvegarde du thème:', result.error);
@@ -62,24 +80,24 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
console.error('Erreur lors de la sauvegarde du thème:', error);
}
}
// Afficher le toast avec le nom du thème
showToast(`Thème: ${THEME_NAMES[newTheme]}`, 2000, getThemeIcon(newTheme));
};
const setTheme = async (newTheme: Theme) => {
setThemeState(newTheme);
// Si ce n'est pas le thème light, c'est le thème préféré de l'utilisateur
if (newTheme !== 'light') {
setUserPreferredTheme(newTheme);
}
// Sauvegarder en base seulement si l'utilisateur est authentifié
if (session?.user?.id) {
try {
const result = await updateViewPreferences({
theme: newTheme
theme: newTheme,
});
if (!result.success) {
console.error('Erreur lors de la sauvegarde du thème:', result.error);
@@ -88,7 +106,7 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
console.error('Erreur lors de la sauvegarde du thème:', error);
}
}
// Afficher le toast avec le nom du thème
showToast(`Thème: ${THEME_NAMES[newTheme]}`, 2000, getThemeIcon(newTheme));
};
@@ -99,17 +117,23 @@ export function ThemeProvider({ children, initialTheme = 'dark', userPreferredTh
await setTheme(THEME_CONFIG.darkThemes[0]);
return;
}
// Sinon, on passe au thème dark suivant
const nextTheme = getNextDarkTheme(theme);
await setTheme(nextTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme, cycleDarkThemes, userPreferredTheme }}>
<div className={mounted ? theme : initialTheme}>
{children}
</div>
<ThemeContext.Provider
value={{
theme,
toggleTheme,
setTheme,
cycleDarkThemes,
userPreferredTheme,
}}
>
<div className={mounted ? theme : initialTheme}>{children}</div>
</ThemeContext.Provider>
);
}

View File

@@ -1,6 +1,14 @@
'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 {
@@ -10,7 +18,13 @@ declare global {
columnVisibilitySyncTimeout?: NodeJS.Timeout;
}
}
import { UserPreferences, KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types';
import {
UserPreferences,
KanbanFilters,
ViewPreferences,
ColumnVisibility,
TaskStatus,
} from '@/lib/types';
import {
updateViewPreferences as updateViewPreferencesAction,
updateKanbanFilters as updateKanbanFiltersAction,
@@ -18,7 +32,7 @@ import {
toggleObjectivesVisibility as toggleObjectivesVisibilityAction,
toggleObjectivesCollapse as toggleObjectivesCollapseAction,
toggleFontSize as toggleFontSizeAction,
toggleColumnVisibility as toggleColumnVisibilityAction
toggleColumnVisibility as toggleColumnVisibilityAction,
} from '@/actions/preferences';
import { useTheme } from './ThemeContext';
import { useSession } from 'next-auth/react';
@@ -26,25 +40,27 @@ import { useSession } from 'next-auth/react';
interface UserPreferencesContextType {
preferences: UserPreferences;
isPending: boolean;
// Kanban Filters
updateKanbanFilters: (updates: Partial<KanbanFilters>) => void;
// View Preferences
// View Preferences
updateViewPreferences: (updates: Partial<ViewPreferences>) => void;
toggleObjectivesVisibility: () => void;
toggleObjectivesCollapse: () => void;
toggleTheme: () => void;
setTheme: (theme: 'light' | 'dark') => void;
toggleFontSize: () => void;
// Column Visibility
updateColumnVisibility: (updates: Partial<ColumnVisibility>) => void;
toggleColumnVisibility: (status: TaskStatus) => void;
isColumnVisible: (status: TaskStatus) => boolean;
}
const UserPreferencesContext = createContext<UserPreferencesContextType | null>(null);
const UserPreferencesContext = createContext<UserPreferencesContextType | null>(
null
);
interface UserPreferencesProviderProps {
children: ReactNode;
@@ -57,7 +73,7 @@ const defaultPreferences: UserPreferences = {
tags: [],
priorities: [],
showCompleted: false,
sortBy: 'priority'
sortBy: 'priority',
},
viewPreferences: {
compactView: false,
@@ -69,25 +85,30 @@ const defaultPreferences: UserPreferences = {
fontSize: 'medium',
backgroundImage: undefined,
backgroundBlur: 0,
backgroundOpacity: 100
backgroundOpacity: 100,
},
columnVisibility: {
hiddenStatuses: []
hiddenStatuses: [],
},
jiraConfig: {
enabled: false
enabled: false,
},
jiraAutoSync: false,
jiraSyncInterval: 'daily',
tfsConfig: {
enabled: false
enabled: false,
},
tfsAutoSync: false,
tfsSyncInterval: 'daily'
tfsSyncInterval: 'daily',
};
export function UserPreferencesProvider({ children, initialPreferences }: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(initialPreferences || defaultPreferences);
export function UserPreferencesProvider({
children,
initialPreferences,
}: UserPreferencesProviderProps) {
const [preferences, setPreferences] = useState<UserPreferences>(
initialPreferences || defaultPreferences
);
const [isPending] = useTransition();
const { toggleTheme: themeToggleTheme, setTheme: themeSetTheme } = useTheme();
const { status } = useSession();
@@ -95,7 +116,7 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
// Fonction pour charger les préférences côté client
const loadUserPreferences = useCallback(async () => {
if (status === 'loading') return; // Attendre que la session soit chargée
try {
const response = await fetch('/api/user-preferences');
if (response.ok) {
@@ -123,54 +144,69 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
// Ne plus synchroniser automatiquement - ThemeContext est la source de vérité
// === KANBAN FILTERS ===
const updateKanbanFilters = useCallback((updates: Partial<KanbanFilters>) => {
// 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);
});
}, 1000); // debounce 1s
}, [preferences.kanbanFilters]);
const updateKanbanFilters = useCallback(
(updates: Partial<KanbanFilters>) => {
// 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
);
});
}, 1000); // debounce 1s
},
[preferences.kanbanFilters]
);
// === VIEW PREFERENCES ===
const updateViewPreferences = useCallback((updates: Partial<ViewPreferences>) => {
// 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);
});
}, 1000); // debounce 1s
}, [preferences.viewPreferences]);
const updateViewPreferences = useCallback(
(updates: Partial<ViewPreferences>) => {
// 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
);
});
}, 1000); // debounce 1s
},
[preferences.viewPreferences]
);
const toggleObjectivesVisibility = useCallback(() => {
// 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);
toggleObjectivesVisibilityAction().catch((error) => {
console.error(
'Erreur lors du toggle de la visibilité des objectifs:',
error
);
});
}, 0);
}, []);
@@ -178,8 +214,11 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
const toggleObjectivesCollapse = useCallback(() => {
// 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);
toggleObjectivesCollapseAction().catch((error) => {
console.error(
'Erreur lors du toggle du collapse des objectifs:',
error
);
});
}, 0);
}, []);
@@ -188,85 +227,118 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
themeToggleTheme();
}, [themeToggleTheme]);
const setTheme = useCallback((theme: 'light' | 'dark') => {
themeSetTheme(theme);
}, [themeSetTheme]);
const setTheme = useCallback(
(theme: 'light' | 'dark') => {
themeSetTheme(theme);
},
[themeSetTheme]
);
const toggleFontSize = useCallback(() => {
// Optimistic update - cycle through font sizes
const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large'];
const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize);
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 }
setPreferences((prev) => ({
...prev,
viewPreferences: { ...prev.viewPreferences, fontSize: newFontSize },
}));
// Non-bloquant : utiliser setTimeout pour éviter de bloquer l'UI
setTimeout(() => {
toggleFontSizeAction().catch(error => {
toggleFontSizeAction().catch((error) => {
console.error('Erreur lors du toggle de la taille de police:', error);
});
}, 0);
}, [preferences.viewPreferences.fontSize]);
// === COLUMN VISIBILITY ===
const updateColumnVisibility = useCallback((updates: Partial<ColumnVisibility>) => {
// 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);
});
}, 1000); // debounce 1s
}, [preferences.columnVisibility]);
const toggleColumnVisibility = useCallback((status: TaskStatus) => {
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 updateColumnVisibility = useCallback(
(updates: Partial<ColumnVisibility>) => {
// Optimistic update immédiat
setPreferences((prev) => ({
...prev,
columnVisibility: { ...prev.columnVisibility, ...updates },
}));
// 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);
});
}, 1000); // debounce 1s
}, [preferences.columnVisibility.hiddenStatuses]);
// Sauvegarde locale immédiate pour éviter les pertes
const newColumnVisibility = {
...preferences.columnVisibility,
...updates,
};
localStorage.setItem(
'columnVisibility',
JSON.stringify(newColumnVisibility)
);
const isColumnVisible = useCallback((status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status);
}, [preferences.columnVisibility.hiddenStatuses]);
// 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
);
});
}, 1000); // debounce 1s
},
[preferences.columnVisibility]
);
const toggleColumnVisibility = useCallback(
(status: TaskStatus) => {
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 },
}));
// 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
);
});
}, 1000); // debounce 1s
},
[preferences.columnVisibility.hiddenStatuses]
);
const isColumnVisible = useCallback(
(status: TaskStatus) => {
return !preferences.columnVisibility.hiddenStatuses.includes(status);
},
[preferences.columnVisibility.hiddenStatuses]
);
const contextValue: UserPreferencesContextType = {
preferences,
@@ -280,7 +352,7 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
toggleFontSize,
updateColumnVisibility,
toggleColumnVisibility,
isColumnVisible
isColumnVisible,
};
return (
@@ -293,7 +365,9 @@ export function UserPreferencesProvider({ children, initialPreferences }: UserPr
export function useUserPreferences() {
const context = useContext(UserPreferencesContext);
if (!context) {
throw new Error('useUserPreferences must be used within a UserPreferencesProvider');
throw new Error(
'useUserPreferences must be used within a UserPreferencesProvider'
);
}
return context;
}