/* eslint-disable react-hooks/set-state-in-effect */ // Toast Notification System - Slide-in notifications from top-right "use client"; import { useEffect, useState, useCallback, useSyncExternalStore } from "react"; import { createPortal } from "react-dom"; import { subscribeToToasts, type Toast, } from "./toast-store"; 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: ( ), }[toast.type]; const bgColors = { success: "bg-green-50 border-green-200", error: "bg-red-50 border-red-200", warning: "bg-amber-50 border-amber-200", info: "bg-blue-50 border-blue-200", }[toast.type]; return (
{icons}

{toast.title}

{toast.message && (

{toast.message}

)} {/* Progress bar */}
); } export default function ToastNotificationContainer() { const [toasts, setToasts] = useState([]); // Track mount via useSyncExternalStore so we don't pay for an // extra render with `mounted=false` (and no toast UI) before the // useEffect fires. const mounted = useSyncExternalStore( () => () => {}, () => true, () => false, ); useEffect(() => { const handleNewToast = (newToast: Toast) => { setToasts((prev) => [...prev, newToast]); }; const unsubscribe = subscribeToToasts(handleNewToast); return unsubscribe; }, []); const handleDismiss = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); if (!mounted) return null; return createPortal(
{toasts.map((t) => (
))}
, document.body ); }