import React, { createContext, useContext, useReducer, useState, useEffect, useRef } from 'react'; import { Toast } from './Toast'; type ToastType = 'success' | 'error' | 'warning' | 'info'; export interface ToastMessage { id: string; message: string; type: ToastType; duration?: number; details?: string; } export interface ToastContextType { addToast: (message: string, type: ToastType, duration?: number, details?: string) => void; removeToast: (id: string) => void; } const ToastContext = createContext(undefined); type ToastAction = | { type: 'ADD_TOAST'; payload: ToastMessage } | { type: 'REMOVE_TOAST'; payload: string } | { type: 'SET_HOVERED_TOAST', payload: { id: string, hovered: boolean } }; const toastReducer = (state: ToastMessage[], action: ToastAction): ToastMessage[] => { switch (action.type) { case 'ADD_TOAST': return [...state, { ...action.payload, hovered: false }]; case 'REMOVE_TOAST': return state.filter(toast => toast.id !== action.payload); case 'SET_HOVERED_TOAST': return state.map(toast => toast.id === action.payload.id ? { ...toast, hovered: action.payload.hovered } : toast ); default: return state; } }; export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [toasts, dispatch] = useReducer(toastReducer, []); const hoverTimeouts = useRef>({}); const addToast = (message: string, type: ToastType, duration: number = 5000, details?: string) => { const id = Date.now().toString(); dispatch({ type: 'ADD_TOAST', payload: { id, message, type, duration, details } }); }; const removeToast = (id: string) => { dispatch({ type: 'REMOVE_TOAST', payload: id }); }; const setToastHovered = (id: string, hovered: boolean) => { dispatch({ type: 'SET_HOVERED_TOAST', payload: { id, hovered } }); }; // Auto remove toasts after duration, but respect hover state useEffect(() => { const timers = toasts.map(toast => { // Clear any existing timeout for this toast if (hoverTimeouts.current[toast.id]) { clearTimeout(hoverTimeouts.current[toast.id]); delete hoverTimeouts.current[toast.id]; } // If toast is hovered, don't set a timer if (toast.hovered) { return null; } // If duration is 0, it's persistent if (toast.duration === 0) { return null; } // Set timeout to remove toast const timeout = setTimeout(() => { removeToast(toast.id); }, toast.duration); return { id: toast.id, timeout }; }); // Cleanup function return () => { timers.forEach(timer => { if (timer) clearTimeout(timer.timeout); }); }; }, [toasts]); return ( {children}
{toasts.map(toast => (
setToastHovered(toast.id, hovered)} />
))}
); }; export const useToast = () => { const context = useContext(ToastContext); if (!context) { throw new Error('useToast must be used within a ToastProvider'); } return context; };