import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { createPortal } from "react-dom"; export type ToastState = { type: "ok" | "error"; text: string } | null; type ToastApi = { toast: ToastState; showOk: (text: string) => void; showError: (text: string) => void; clear: () => void; }; const ToastContext = createContext(null); function useToastState(autoHideMs: number): ToastApi { const [toast, setToast] = useState(null); const showOk = useCallback((text: string) => setToast({ type: "ok", text }), []); const showError = useCallback((text: string) => setToast({ type: "error", text }), []); const clear = useCallback(() => setToast(null), []); useEffect(() => { if (!toast) return; const timer = window.setTimeout(() => setToast(null), autoHideMs); return () => window.clearTimeout(timer); }, [toast, autoHideMs]); return useMemo(() => ({ toast, showOk, showError, clear }), [toast, showOk, showError, clear]); } function ToastViewport({ toast }: { toast: NonNullable }) { if (typeof document === "undefined") return null; return createPortal(
{toast.text}
, document.body, ); } export function ToastProvider({ children, autoHideMs = 2600 }: { children: ReactNode; autoHideMs?: number }) { const api = useToastState(autoHideMs); return ( {children} {api.toast ? : null} ); } export function useToast(): ToastApi { const ctx = useContext(ToastContext); if (!ctx) throw new Error("useToast must be used within ToastProvider"); return ctx; }