/** * Toaster — lightweight bottom-right notification stack. * * Subscribes to two app-level CustomEvents: * - `hasarui:toast` { kind: 'info'|'success'|'error', message } * - `hasarui:file-size-rejected` { rejected: {name,size}[], maxMb } * * Pages can fire `window.dispatchEvent(new CustomEvent('hasarui:toast', { detail: ... }))` * without importing this module — keeps the toast surface decoupled. */ import { useEffect, useState, useCallback } from 'react'; import { AlertTriangle, CheckCircle2, Info, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; type ToastKind = 'info' | 'success' | 'error'; interface Toast { id: number; kind: ToastKind; message: string; } let nextId = 1; export default function Toaster() { const { t } = useTranslation(); const [toasts, setToasts] = useState([]); const push = useCallback((kind: ToastKind, message: string) => { const id = nextId++; setToasts((cur) => [...cur, { id, kind, message }]); window.setTimeout(() => { setToasts((cur) => cur.filter((x) => x.id !== id)); }, 5000); }, []); useEffect(() => { function onToast(e: Event) { const ev = e as CustomEvent<{ kind?: ToastKind; message: string }>; if (!ev.detail?.message) return; push(ev.detail.kind ?? 'info', ev.detail.message); } function onSizeRejected(e: Event) { const ev = e as CustomEvent<{ rejected: { name: string; size: number }[]; maxMb: number; }>; const r = ev.detail?.rejected ?? []; if (!r.length) return; const msg = r.length === 1 ? t('errors.fileTooLargeOne', { name: r[0]?.name ?? '', maxMb: ev.detail.maxMb }) : t('errors.fileTooLarge', { count: r.length, maxMb: ev.detail.maxMb }); push('error', msg); } window.addEventListener('hasarui:toast', onToast as EventListener); window.addEventListener('hasarui:file-size-rejected', onSizeRejected as EventListener); return () => { window.removeEventListener('hasarui:toast', onToast as EventListener); window.removeEventListener( 'hasarui:file-size-rejected', onSizeRejected as EventListener, ); }; }, [push, t]); if (toasts.length === 0) return null; return (
{toasts.map((toast) => (
{toast.kind === 'error' ? ( ) : toast.kind === 'success' ? ( ) : ( )}
{toast.message}
))}
); }