import { type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { AlertCircle, CheckCircle2, Info, X } from "lucide-react"; import { ToastContext, type Toast, type ToastContextValue, type ToastTone } from "@/client/toast/toast-context"; const TOAST_TIMEOUT_MS = 4000; const TONE_STYLES: Record< ToastTone, { panel: string; icon: typeof CheckCircle2; } > = { success: { panel: "border-emerald-500/20 bg-emerald-500/10 text-emerald-100", icon: CheckCircle2, }, error: { panel: "border-red-500/20 bg-red-500/10 text-red-100", icon: AlertCircle, }, info: { panel: "border-accent-500/20 bg-accent-500/10 text-accent-100", icon: Info, }, }; const createToastId = (): string => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; export const ToastProvider = ({ children }: { children: ReactNode }) => { const [toasts, setToasts] = useState([]); const timersRef = useRef>(new Map()); const dismissToast = useCallback((id: string) => { timersRef.current.delete(id); setToasts((current) => current.filter((item) => item.id !== id)); }, []); useEffect(() => { const timers = timersRef.current; return () => { for (const handle of timers.values()) { window.clearTimeout(handle); } }; }, []); const pushToast = useCallback( (input: Omit) => { const id = createToastId(); setToasts((current) => [...current, { ...input, id }]); if (input.tone !== "error") { const handle = window.setTimeout(() => dismissToast(id), TOAST_TIMEOUT_MS); timersRef.current.set(id, handle); } }, [dismissToast], ); const value: ToastContextValue = { pushToast }; return ( {children}
{toasts.map((toast) => { const meta = TONE_STYLES[toast.tone]; const Icon = meta.icon; return (
); })}
); };