import { createContext, useContext, useState, useCallback, type ReactNode, type HTMLAttributes, type ReactElement, } from "react"; export type ToastVariant = "success" | "error" | "warning" | "info"; export interface Toast { id: string; message: string; variant?: ToastVariant; duration?: number; } export interface ToastContextValue { showToast: (message: string, variant?: ToastVariant, duration?: number) => void; removeToast: (id: string) => void; } const ToastContext = createContext(null); export function useToast(): ToastContextValue { const context = useContext(ToastContext); if (!context) { throw new Error("useToast must be used within a ToastProvider"); } return context; } export interface ToastProviderProps { children: ReactNode; } export function ToastProvider({ children }: ToastProviderProps): ReactElement { const [toasts, setToasts] = useState([]); const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }, []); const showToast = useCallback( (message: string, variant: ToastVariant = "info", duration = 5000) => { const id = `toast-${String(Date.now())}-${Math.random().toString(36).substring(2, 11)}`; const newToast: Toast = { id, message, variant, duration }; setToasts((prev) => [...prev, newToast]); if (duration > 0) { setTimeout(() => { removeToast(id); }, duration); } }, [removeToast] ); return ( {children} ); } export interface ToastContainerProps extends HTMLAttributes { toasts: Toast[]; onRemove: (id: string) => void; } function ToastContainer({ toasts, onRemove, className = "", }: ToastContainerProps): ReactElement | null { if (toasts.length === 0) return null; return (
{toasts.map((toast) => ( ))}
); } interface ToastItemProps { toast: Toast; onRemove: (id: string) => void; } interface ToastVariantStyle { background: string; border: string; color: string; } const variantStyles: Record = { success: { background: "rgba(20,184,166,0.15)", border: "1px solid rgba(20,184,166,0.35)", color: "var(--success)", }, error: { background: "rgba(229,72,77,0.15)", border: "1px solid rgba(229,72,77,0.35)", color: "var(--danger)", }, warning: { background: "rgba(245,158,11,0.15)", border: "1px solid rgba(245,158,11,0.35)", color: "var(--warn)", }, info: { background: "rgba(47,128,255,0.15)", border: "1px solid rgba(47,128,255,0.35)", color: "var(--info)", }, }; function ToastItem({ toast, onRemove }: ToastItemProps): ReactElement { const vStyle = variantStyles[toast.variant ?? "info"]; const icon: Record = { success: ( ), error: ( ), warning: ( ), info: ( ), }; return (
{icon[toast.variant ?? "info"]} {toast.message}
); } // Helper function to show toasts outside of React components let toastContextValue: ToastContextValue | null = null; export function setToastContext(context: ToastContextValue | null): void { toastContextValue = context; } export interface ToastOptions { variant?: ToastVariant; duration?: number; } export function toast(message: string, options?: ToastOptions): void { if (!toastContextValue) { console.warn("Toast context not available. Make sure ToastProvider is mounted."); return; } toastContextValue.showToast(message, options?.variant ?? "info", options?.duration ?? 5000); }