import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode, } from "react"; import { createPortal } from "react-dom"; import { AlertCircle, CheckCircle2, Info, X } from "lucide-react"; export type ToastVariant = "success" | "error" | "info"; type ToastItem = { id: string; message: string; variant: ToastVariant; }; type ToastContextValue = { showToast: (message: string, variant?: ToastVariant) => void; }; const ToastContext = createContext(null); export function useToast(): ToastContextValue { const ctx = useContext(ToastContext); if (!ctx) throw new Error("useToast must be used within "); return ctx; } const TOAST_DURATION = 2800; export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const counter = useRef(0); const showToast = useCallback((message: string, variant: ToastVariant = "success") => { const id = String(++counter.current); setToasts((prev) => [...prev, { id, message, variant }]); }, []); const dismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); return ( {children} {typeof document !== "undefined" ? createPortal( , document.body, ) : null} ); } function ToastList({ toasts, onDismiss, }: { toasts: ToastItem[]; onDismiss: (id: string) => void; }) { if (toasts.length === 0) return null; return (
{toasts.map((toast) => ( ))}
); } const VARIANT_ICONS: Record = { success: CheckCircle2, error: AlertCircle, info: Info, }; const VARIANT_ICON_CLASS: Record = { success: "text-green-400", error: "text-red-400", info: "text-blue-400", }; function ToastEntry({ toast, onDismiss, }: { toast: ToastItem; onDismiss: (id: string) => void; }) { const Icon = VARIANT_ICONS[toast.variant]; useEffect(() => { const timer = setTimeout(() => onDismiss(toast.id), TOAST_DURATION); return () => clearTimeout(timer); }, [toast.id, onDismiss]); return (
); }