/** * Toast hook for managing toast notifications */ import { useState, useCallback, useEffect } from 'react'; export type ToastType = 'success' | 'error' | 'warning' | 'info'; export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; export interface Toast { id: string; type: ToastType; title?: string; message: string; duration?: number; dismissible?: boolean; action?: { label: string; onClick: () => void; }; timestamp: number; } interface ToastState { toasts: Toast[]; position: ToastPosition; maxToasts: number; } interface ToastOptions { type?: ToastType; title?: string; duration?: number; dismissible?: boolean; action?: { label: string; onClick: () => void; }; } interface ToastActions { addToast: (message: string, options?: ToastOptions) => string; removeToast: (id: string) => void; clearToasts: () => void; success: (message: string, options?: Omit) => string; error: (message: string, options?: Omit) => string; warning: (message: string, options?: Omit) => string; info: (message: string, options?: Omit) => string; setPosition: (position: ToastPosition) => void; setMaxToasts: (max: number) => void; } const DEFAULT_DURATION = 5000; // 5 seconds const DEFAULT_POSITION: ToastPosition = 'top-right'; const DEFAULT_MAX_TOASTS = 6; // Generate unique ID const generateId = (): string => { return `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }; export const useToast = ( initialPosition: ToastPosition = DEFAULT_POSITION, initialMaxToasts: number = DEFAULT_MAX_TOASTS ): ToastState & ToastActions => { const [state, setState] = useState({ toasts: [], position: initialPosition, maxToasts: initialMaxToasts, }); // Remove toast by ID const removeToast = useCallback((id: string) => { setState(prev => ({ ...prev, toasts: prev.toasts.filter(toast => toast.id !== id), })); }, []); // Add toast const addToast = useCallback((message: string, options: ToastOptions = {}): string => { const id = generateId(); const toast: Toast = { id, type: options.type || 'info', title: options.title, message, duration: options.duration ?? DEFAULT_DURATION, dismissible: options.dismissible ?? true, action: options.action, timestamp: Date.now(), }; setState(prev => { const newToasts = [...prev.toasts, toast]; // Limit number of toasts if (newToasts.length > prev.maxToasts) { return { ...prev, toasts: newToasts.slice(-prev.maxToasts), }; } return { ...prev, toasts: newToasts, }; }); // Auto-dismiss toast if duration is set if (toast.duration && toast.duration > 0) { setTimeout(() => { removeToast(id); }, toast.duration); } return id; }, [removeToast]); // Clear all toasts const clearToasts = useCallback(() => { setState(prev => ({ ...prev, toasts: [], })); }, []); // Convenience methods for different toast types const success = useCallback((message: string, options: Omit = {}) => { return addToast(message, { ...options, type: 'success' }); }, [addToast]); const error = useCallback((message: string, options: Omit = {}) => { return addToast(message, { ...options, type: 'error', duration: options.duration ?? 8000 }); }, [addToast]); const warning = useCallback((message: string, options: Omit = {}) => { return addToast(message, { ...options, type: 'warning' }); }, [addToast]); const info = useCallback((message: string, options: Omit = {}) => { return addToast(message, { ...options, type: 'info' }); }, [addToast]); // Set toast position const setPosition = useCallback((position: ToastPosition) => { setState(prev => ({ ...prev, position, })); }, []); // Set maximum number of toasts const setMaxToasts = useCallback((maxToasts: number) => { setState(prev => { const newToasts = prev.toasts.length > maxToasts ? prev.toasts.slice(-maxToasts) : prev.toasts; return { ...prev, maxToasts, toasts: newToasts, }; }); }, []); return { ...state, addToast, removeToast, clearToasts, success, error, warning, info, setPosition, setMaxToasts, }; };