/* eslint-disable react-hooks/set-state-in-effect */ // Toast Notification System - Slide-in notifications from top-right "use client"; import { useEffect, useState, useCallback } from "react"; import { createPortal } from "react-dom"; type ToastType = "success" | "error" | "warning" | "info"; interface Toast { id: string; type: ToastType; title: string; message?: string; duration?: number; } interface ToastNotificationProps { toast: Toast; onDismiss: (id: string) => void; } function ToastItem({ toast, onDismiss }: ToastNotificationProps) { const [progress, setProgress] = useState(100); const [isExiting, setIsExiting] = useState(false); const duration = toast.duration ?? 5000; useEffect(() => { const startTime = Date.now(); const interval = setInterval(() => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, 100 - (elapsed / duration) * 100); setProgress(remaining); if (remaining === 0) { clearInterval(interval); setIsExiting(true); setTimeout(() => onDismiss(toast.id), 300); } }, 50); return () => clearInterval(interval); }, [toast.id, duration, onDismiss]); const handleDismiss = () => { setIsExiting(true); setTimeout(() => onDismiss(toast.id), 300); }; const icons = { success: ( ), error: ( ), warning: ( ), info: ( ), }; const colors = { success: "bg-emerald-50 border-emerald-200 text-emerald-800", error: "bg-red-50 border-red-200 text-red-800", warning: "bg-amber-50 border-amber-200 text-amber-800", info: "bg-blue-50 border-blue-200 text-blue-800", }; const iconColors = { success: "text-emerald-500", error: "text-red-500", warning: "text-amber-500", info: "text-blue-500", }; return (
{/* Progress bar */}
{icons[toast.type]}

{toast.title}

{toast.message &&

{toast.message}

}
); } // Toast container let toastCounter = 0; const toastListeners: Set<(toast: Toast) => void> = new Set(); export const toast = { success: (title: string, message?: string) => { const newToast: Toast = { id: `toast-${++toastCounter}`, type: "success", title, message }; toastListeners.forEach((listener) => listener(newToast)); }, error: (title: string, message?: string) => { const newToast: Toast = { id: `toast-${++toastCounter}`, type: "error", title, message }; toastListeners.forEach((listener) => listener(newToast)); }, warning: (title: string, message?: string) => { const newToast: Toast = { id: `toast-${++toastCounter}`, type: "warning", title, message }; toastListeners.forEach((listener) => listener(newToast)); }, info: (title: string, message?: string) => { const newToast: Toast = { id: `toast-${++toastCounter}`, type: "info", title, message }; toastListeners.forEach((listener) => listener(newToast)); }, }; export default function ToastNotificationContainer() { const [toasts, setToasts] = useState([]); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); const handleNewToast = (newToast: Toast) => { setToasts((prev) => [...prev, newToast]); }; toastListeners.add(handleNewToast); return () => { toastListeners.delete(handleNewToast); }; }, []); const handleDismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); if (!mounted) return null; return createPortal(
{toasts.map((t) => (
))}
, document.body ); }