'use client'; import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { createPortal } from 'react-dom'; import { ToastContext } from '../context/ToastContext'; import { ToastContainer } from './ToastContainer'; import { ToastProps, ToastProviderProps, ToastPosition, ToastLayout, RichColorsMode, ToastSize } from '../types'; import { MAX_TOASTS } from '../utils/constants'; import { registerToastStore, unregisterToastStore } from '../utils/toastStore'; import '../styles/toast.css'; export const ToastProvider: React.FC = ({ children = null, defaultPosition, position: positionProp, defaultLayout, layout: layoutProp, maxToasts = MAX_TOASTS, containerClassName = '', showCloseButton = true, swipeDirection = 'right', showProgressBar = true, color = true, richColors = false, size = 'md', defaultDuration, duration, }) => { // Support both naming conventions: defaultPosition/position, defaultLayout/layout const initialPosition = positionProp ?? defaultPosition ?? 'top-right'; const initialLayout = layoutProp ?? defaultLayout ?? 'normal'; const finalDuration = duration ?? defaultDuration; const [toasts, setToasts] = useState([]); const [position, setPosition] = useState(initialPosition); const [layout, setLayout] = useState(initialLayout); const resolvedRichColorsMode: RichColorsMode | undefined = richColors === true ? 'minimal' : typeof richColors === 'string' ? richColors : undefined; const toastSize: ToastSize = size; const audioRefsRef = useRef>(new Map()); const addToast = useCallback( (toast: Omit) => { const id = Math.random().toString(36).substring(2, 11); // Apply default duration if not provided in toast options // If toast explicitly sets duration (including 0 for infinite), use it // Otherwise, use defaultDuration if provided, or leave undefined for Toast component default const toastWithDuration = { ...toast, duration: toast.duration !== undefined ? toast.duration : finalDuration, }; setToasts((prevToasts) => { const newToasts = [...prevToasts, { ...toastWithDuration, id }]; return newToasts.slice(-maxToasts); }); if (toast.soundEffect) { // Clean up old audio instances and create new one const audio = new Audio(toast.soundEffect); audioRefsRef.current.set(id, audio); audio.play() .catch((e) => console.error('Error playing sound:', e)) .finally(() => { // Clean up after playback or error setTimeout(() => { audioRefsRef.current.delete(id); audio.remove(); }, 1000); }); } return id; }, [maxToasts, finalDuration], ); // Cleanup audio on unmount useEffect(() => { return () => { audioRefsRef.current.forEach((audio) => { audio.pause(); audio.remove(); }); audioRefsRef.current.clear(); }; }, []); const removeToast = useCallback((id: string) => { setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); }, []); const updateToast = useCallback((id: string, updates: Partial>) => { setToasts((prevToasts) => prevToasts.map((toast) => (toast.id === id ? { ...toast, ...updates } : toast)), ); }, []); const clearAllToasts = useCallback(() => { setToasts([]); }, []); const contextValue = useMemo( () => ({ addToast, removeToast, updateToast, clearAllToasts, position, setPosition, layout, setLayout, showCloseButton, swipeDirection, showProgressBar, color, richColors: resolvedRichColorsMode, size: toastSize, }), [ addToast, removeToast, updateToast, clearAllToasts, position, layout, showCloseButton, swipeDirection, showProgressBar, color, resolvedRichColorsMode, toastSize, ], ); const [isMounted, setIsMounted] = useState(false); useEffect(() => { setIsMounted(true); }, []); // Register toast store for global access useEffect(() => { registerToastStore({ addToast, removeToast, updateToast, clearAllToasts, }); return () => { unregisterToastStore(); }; }, [addToast, removeToast, updateToast, clearAllToasts]); // Sync position prop changes useEffect(() => { if (positionProp !== undefined) { setPosition(positionProp); } }, [positionProp]); // Sync layout prop changes useEffect(() => { if (layoutProp !== undefined) { setLayout(layoutProp); } }, [layoutProp]); return ( {children} {isMounted && createPortal( , document.body, )} ); };