import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react'; const ToastContext = createContext(null); export function useToast() { const ctx = useContext(ToastContext); if (!ctx) throw new Error('useToast must be used within a ToastProvider'); return ctx; } const VARIANTS = { success: { bg: '#053321', border: '#0f5132', color: '#9ef7c1', icon: '✓' }, error: { bg: '#3c1114', border: '#f5c6cb', color: '#ffb3b8', icon: '✗' }, info: { bg: '#0c1f3f', border: '#2563eb', color: '#93c5fd', icon: 'ℹ' }, warning: { bg: '#3b2e00', border: '#d4af37', color: '#ffdf8a', icon: '⚠' }, }; let nextId = 0; function Toast({ toast, onDismiss }) { const v = VARIANTS[toast.variant] || VARIANTS.info; const [exiting, setExiting] = useState(false); const timerRef = useRef(null); useEffect(() => { timerRef.current = setTimeout(() => { setExiting(true); setTimeout(() => onDismiss(toast.id), 280); }, toast.duration || 4000); return () => clearTimeout(timerRef.current); }, [toast.id, toast.duration, onDismiss]); const handleDismiss = () => { clearTimeout(timerRef.current); setExiting(true); setTimeout(() => onDismiss(toast.id), 280); }; return (
{v.icon} {toast.message}
); } export default function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const dismiss = useCallback((id) => { setToasts(prev => prev.filter(t => t.id !== id)); }, []); const addToast = useCallback((message, variant = 'info', duration = 4000) => { const id = ++nextId; setToasts(prev => { const next = [...prev, { id, message, variant, duration }]; return next.length > 5 ? next.slice(-5) : next; }); return id; }, []); const toast = useCallback({ success: (msg, dur) => addToast(msg, 'success', dur), error: (msg, dur) => addToast(msg, 'error', dur || 6000), info: (msg, dur) => addToast(msg, 'info', dur), warning: (msg, dur) => addToast(msg, 'warning', dur || 5000), }, [addToast]); // Inject keyframes once useEffect(() => { if (document.getElementById('toast-keyframes')) return; const style = document.createElement('style'); style.id = 'toast-keyframes'; style.textContent = ` @keyframes toastIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } @keyframes toastOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } } @keyframes toastProgress { from { width: 100%; } to { width: 0%; } } `; document.head.appendChild(style); }, []); return ( {children}
{toasts.map(t => (
))}
); }