import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode, } from 'react'; import './Toast.css'; // -- Types ------------------------------------------------------------------- export type ToastType = 'success' | 'error' | 'info'; interface ToastItem { id: number; message: string; type: ToastType; } interface ToastContextValue { addToast: (message: string, type?: ToastType) => void; } // -- Context ----------------------------------------------------------------- const ToastContext = createContext(null); export function useToast(): ToastContextValue { const ctx = useContext(ToastContext); if (!ctx) throw new Error('useToast must be used within a ToastProvider'); return ctx; } // -- Provider ---------------------------------------------------------------- const DISMISS_MS = 3000; export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const nextId = useRef(0); const addToast = useCallback((message: string, type: ToastType = 'info') => { const id = nextId.current++; setToasts((prev) => [...prev, { id, message, type }]); }, []); const removeToast = useCallback((id: number) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); return ( {children}
{toasts.map((t) => ( ))}
); } // -- Single toast ------------------------------------------------------------ function ToastItem({ toast, onDismiss, }: { toast: ToastItem; onDismiss: (id: number) => void; }) { const [exiting, setExiting] = useState(false); useEffect(() => { const timer = setTimeout(() => setExiting(true), DISMISS_MS - 300); const remove = setTimeout(() => onDismiss(toast.id), DISMISS_MS); return () => { clearTimeout(timer); clearTimeout(remove); }; }, [toast.id, onDismiss]); const icon = toast.type === 'success' ? '\u2713' : toast.type === 'error' ? '\u2717' : '\u2139'; return (
{icon} {toast.message}
); }