feat: integrate ToastProvider and enhance theme management
- Added `ToastProvider` to `RootLayout` for improved user feedback on theme changes. - Updated `ThemeProvider` to display toast notifications with theme names and icons upon theme changes. - Refactored theme-related imports to streamline code and improve maintainability. - Simplified background cycling logic in `useBackgroundCycle` to utilize centralized background definitions. - Cleaned up unused background definitions in `BackgroundContext` for better clarity and performance.
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { Theme } from '@/lib/theme-config';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||
import { Theme, THEME_CONFIG, THEME_NAMES, getThemeIcon, getThemeDescription } from '@/lib/ui-config';
|
||||
|
||||
|
||||
// Génération des thèmes à partir de la configuration centralisée
|
||||
const themes: { id: Theme; name: string; description: string }[] = THEME_CONFIG.allThemes.map(themeId => {
|
||||
const metadata = getThemeMetadata(themeId);
|
||||
const themes: { id: Theme; name: string; description: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeId => {
|
||||
return {
|
||||
id: themeId,
|
||||
name: metadata.name,
|
||||
description: metadata.description
|
||||
name: THEME_NAMES[themeId],
|
||||
description: getThemeDescription(themeId),
|
||||
icon: getThemeIcon(themeId)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -95,7 +94,8 @@ export function ThemeSelector() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-[var(--foreground)] mb-1">
|
||||
<div className="font-medium text-[var(--foreground)] mb-1 flex items-center gap-2">
|
||||
<span className="text-base">{themeOption.icon}</span>
|
||||
{themeOption.name}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)] leading-relaxed">
|
||||
|
||||
@@ -4,83 +4,7 @@ import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
|
||||
|
||||
// Images de fond prédéfinies basées sur le thème actuel
|
||||
const PRESET_BACKGROUNDS = [
|
||||
{
|
||||
id: 'none',
|
||||
name: 'Aucune',
|
||||
description: 'Fond uni par défaut',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, var(--card) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-subtle',
|
||||
name: 'Dégradé subtil',
|
||||
description: 'Dégradé doux avec les couleurs du thème',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-primary',
|
||||
name: 'Dégradé primaire',
|
||||
description: 'Dégradé marqué avec la couleur primaire',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 25%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-accent',
|
||||
name: 'Dégradé accent',
|
||||
description: 'Dégradé marqué avec la couleur d\'accent',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--accent) 25%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-success',
|
||||
name: 'Dégradé succès',
|
||||
description: 'Dégradé marqué avec la couleur de succès',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--success) 25%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-purple',
|
||||
name: 'Dégradé violet',
|
||||
description: 'Dégradé marqué avec la couleur violette',
|
||||
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--purple) 25%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-diagonal',
|
||||
name: 'Dégradé diagonal',
|
||||
description: 'Dégradé diagonal avec plusieurs couleurs',
|
||||
preview: 'linear-gradient(45deg, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 50%, color-mix(in srgb, var(--accent) 20%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-radial',
|
||||
name: 'Dégradé radial',
|
||||
description: 'Dégradé radial centré avec les couleurs du thème',
|
||||
preview: 'radial-gradient(circle at center, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-sunset',
|
||||
name: 'Dégradé coucher de soleil',
|
||||
description: 'Dégradé orange-rouge intense',
|
||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--accent) 30%, var(--background)) 0%, color-mix(in srgb, var(--destructive) 20%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-ocean',
|
||||
name: 'Dégradé océan',
|
||||
description: 'Dégradé bleu profond',
|
||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--blue) 25%, var(--background)) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-forest',
|
||||
name: 'Dégradé forêt',
|
||||
description: 'Dégradé vert naturel',
|
||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--green) 25%, var(--background)) 0%, color-mix(in srgb, var(--success) 15%, var(--background)) 100%)'
|
||||
},
|
||||
{
|
||||
id: 'theme-galaxy',
|
||||
name: 'Dégradé galaxie',
|
||||
description: 'Dégradé violet-bleu mystique',
|
||||
preview: 'linear-gradient(135deg, color-mix(in srgb, var(--purple) 25%, var(--background)) 0%, color-mix(in srgb, var(--blue) 20%, var(--background)) 100%)'
|
||||
}
|
||||
];
|
||||
|
||||
import { PRESET_BACKGROUNDS } from '@/lib/ui-config';
|
||||
|
||||
export function BackgroundImageSelector() {
|
||||
const { preferences, updateViewPreferences } = useUserPreferences();
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useJiraConfig } from '@/contexts/JiraConfigContext';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { Theme } from '@/lib/theme-config';
|
||||
import { THEME_CONFIG, getThemeMetadata } from '@/lib/theme-config';
|
||||
import { Theme, THEME_CONFIG, getThemeIcon } from '@/lib/ui-config';
|
||||
import { useKeyboardShortcutsModal } from '@/contexts/KeyboardShortcutsContext';
|
||||
import { AuthButton } from '@/components/AuthButton';
|
||||
import { useSession, signOut } from 'next-auth/react';
|
||||
@@ -28,12 +27,10 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
||||
|
||||
// Liste des thèmes disponibles avec leurs labels et icônes
|
||||
const themes: { value: Theme; label: string; icon: string }[] = THEME_CONFIG.allThemes.map(themeValue => {
|
||||
const metadata = getThemeMetadata(themeValue);
|
||||
|
||||
return {
|
||||
value: themeValue,
|
||||
label: metadata.name,
|
||||
icon: metadata.icon
|
||||
label: themeValue.charAt(0).toUpperCase() + themeValue.slice(1).replace('_', ' '),
|
||||
icon: getThemeIcon(themeValue)
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
94
src/components/ui/Toast.tsx
Normal file
94
src/components/ui/Toast.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
duration?: number;
|
||||
onClose: () => void;
|
||||
index: number;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export function Toast({ message, duration = 2000, onClose, index, icon }: ToastProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setTimeout(onClose, 200); // Attendre la fin de l'animation
|
||||
}, duration);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [duration, onClose]);
|
||||
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`fixed top-4 right-4 z-50 transition-all duration-200 ${
|
||||
isVisible
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 -translate-y-2'
|
||||
}`}
|
||||
style={{ transform: `translateY(${index * 60}px)` }}
|
||||
>
|
||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-2 shadow-lg backdrop-blur-sm flex items-center gap-2">
|
||||
{icon && <span className="text-base">{icon}</span>}
|
||||
<div className="text-sm text-[var(--foreground)] font-medium">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
showToast: (message: string, duration?: number, icon?: string) => void;
|
||||
}
|
||||
|
||||
export const ToastContext = React.createContext<ToastContextType | null>(null);
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Array<{ id: string; message: string; duration?: number; icon?: string }>>([]);
|
||||
const MAX_TOASTS = 5;
|
||||
|
||||
const showToast = (message: string, duration?: number, icon?: string) => {
|
||||
const id = Math.random().toString(36).substr(2, 9);
|
||||
setToasts(prev => {
|
||||
const newToasts = [...prev, { id, message, duration, icon }];
|
||||
// Garder seulement les 5 plus récents
|
||||
return newToasts.slice(-MAX_TOASTS);
|
||||
});
|
||||
};
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
{children}
|
||||
{toasts.map((toast, index) => (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
message={toast.message}
|
||||
duration={toast.duration}
|
||||
onClose={() => removeToast(toast.id)}
|
||||
index={index}
|
||||
icon={toast.icon}
|
||||
/>
|
||||
))}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast() {
|
||||
const context = React.useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user