import * as React from 'react' const TOAST_LIMIT = 5 const TOAST_AUTO_DISMISS = 5000 export type ToastVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info' export interface ToasterToast { id: string title?: string description?: string action?: React.ReactNode variant?: ToastVariant duration?: number } type Action = | { type: 'ADD_TOAST'; toast: ToasterToast } | { type: 'UPDATE_TOAST'; toast: Partial & { id: string } } | { type: 'DISMISS_TOAST'; toastId?: string } | { type: 'REMOVE_TOAST'; toastId?: string } interface State { toasts: ToasterToast[] } const toastTimeouts = new Map>() function addToRemoveQueue(toastId: string, delay: number) { if (toastTimeouts.has(toastId)) { return } const timeout = setTimeout(() => { toastTimeouts.delete(toastId) dispatch({ type: 'REMOVE_TOAST', toastId }) }, delay) toastTimeouts.set(toastId, timeout) } export function reducer(state: State, action: Action): State { switch (action.type) { case 'ADD_TOAST': return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), } case 'UPDATE_TOAST': return { ...state, toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), } case 'DISMISS_TOAST': { const { toastId } = action if (toastId) { addToRemoveQueue(toastId, 300) } else { state.toasts.forEach((t) => addToRemoveQueue(t.id, 300)) } return state } case 'REMOVE_TOAST': if (action.toastId === undefined) { return { ...state, toasts: [] } } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), } } } const listeners: Array<(state: State) => void> = [] let memoryState: State = { toasts: [] } function dispatch(action: Action) { memoryState = reducer(memoryState, action) listeners.forEach((listener) => listener(memoryState)) } let count = 0 function genId() { count = (count + 1) % Number.MAX_VALUE return count.toString() } type ToastInput = Omit function toast(props: ToastInput) { const id = genId() const duration = props.duration ?? TOAST_AUTO_DISMISS dispatch({ type: 'ADD_TOAST', toast: { ...props, id }, }) // Auto-dismiss if (duration > 0) { addToRemoveQueue(id, duration) } const update = (updateProps: Partial) => dispatch({ type: 'UPDATE_TOAST', toast: { ...updateProps, id } }) const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }) return { id, dismiss, update } } function clearAllToasts() { // Clear all pending timeouts toastTimeouts.forEach((timeout) => clearTimeout(timeout)) toastTimeouts.clear() dispatch({ type: 'REMOVE_TOAST', toastId: undefined }) } function useToast() { const [state, setState] = React.useState(memoryState) React.useEffect(() => { listeners.push(setState) return () => { const index = listeners.indexOf(setState) if (index > -1) { listeners.splice(index, 1) } } }, [state]) return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), clear: clearAllToasts, } } export { useToast, toast, clearAllToasts }