import { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react' import { IconCheck, IconX, IconInfoCircle, IconAlertTriangle } from '@tabler/icons-react' type ToastType = 'success' | 'error' | 'info' | 'warning' interface Toast { id: string message: string type: ToastType duration: number } interface ToastAPI { success: (msg: string, duration?: number) => void error: (msg: string, duration?: number) => void info: (msg: string, duration?: number) => void warning: (msg: string, duration?: number) => void } const ToastContext = createContext({ success: () => {}, error: () => {}, info: () => {}, warning: () => {}, }) export function useToast() { return useContext(ToastContext) } // global emitter for non-React code (api.ts etc) type ToastFn = (msg: string, type?: string) => void let globalToast: ToastFn | null = null export function setGlobalToast(fn: ToastFn) { globalToast = fn } export function emitToast(msg: string, type: ToastType = 'info') { if (globalToast) globalToast(msg, type) } let idCounter = 0 const TYPE_COLORS: Record = { success: 'var(--success)', error: 'var(--error)', warning: 'var(--warning)', info: 'var(--info)', } const TYPE_ICONS: Record = { success: IconCheck, error: IconX, warning: IconAlertTriangle, info: IconInfoCircle, } const MAX_VISIBLE = 5 function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: (id: string) => void }) { const [exiting, setExiting] = useState(false) const timerRef = useRef>() const Icon = TYPE_ICONS[toast.type] useEffect(() => { timerRef.current = setTimeout(() => { setExiting(true) setTimeout(() => onDismiss(toast.id), 200) }, toast.duration) return () => clearTimeout(timerRef.current) }, [toast.id, toast.duration, onDismiss]) const handleClose = () => { clearTimeout(timerRef.current) setExiting(true) setTimeout(() => onDismiss(toast.id), 200) } return (
) } export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] = useState([]) const dismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)) }, []) const add = useCallback((message: string, type: ToastType, duration = 4000) => { const id = `toast-${++idCounter}` setToasts((prev) => { const next = [...prev, { id, message, type, duration }] return next.slice(-MAX_VISIBLE) }) }, []) const api: ToastAPI = { success: useCallback((msg: string, dur?: number) => add(msg, 'success', dur), [add]), error: useCallback((msg: string, dur?: number) => add(msg, 'error', dur), [add]), info: useCallback((msg: string, dur?: number) => add(msg, 'info', dur), [add]), warning: useCallback((msg: string, dur?: number) => add(msg, 'warning', dur), [add]), } useEffect(() => { setGlobalToast((msg, type) => add(msg, (type as ToastType) || 'info')) return () => { globalToast = null } }, [add]) return ( {children}
{toasts.map((t) => ( ))}
) }