import { AlertCircle, AlertTriangle, CheckCircle, Info, X } from 'lucide-react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { setNotify } from '../../application/notification'; import { cn } from '../../lib/utils'; export type ToastType = 'success' | 'error' | 'warning' | 'info'; export interface Toast { id: string; type: ToastType; title?: string; message: string; duration?: number; onClick?: () => void; actionLabel?: string; } interface ToastActionsValue { showToast: (toast: Omit) => void; dismissToast: (id: string) => void; } interface ToastStateValue { toasts: Toast[]; } const ToastActionsContext = createContext(null); const ToastStateContext = createContext(null); /** Actions-only subscription — does not re-render when the toast list changes. */ export const useToastActions = () => { const context = useContext(ToastActionsContext); if (!context) { throw new Error('useToastActions must be used within a ToastProvider'); } return context; }; /** Full toast API. Prefer useToastActions when you do not need the list. */ export const useToast = () => { const actions = useContext(ToastActionsContext); const state = useContext(ToastStateContext); if (!actions || !state) { throw new Error('useToast must be used within a ToastProvider'); } return { ...state, ...actions }; }; // Simple hook for components that may not be inside ToastProvider let globalShowToast: ((toast: Omit) => void) | null = null; export interface ToastOptions { title?: string; duration?: number; onClick?: () => void; actionLabel?: string; } export const toast = { success: (message: string, titleOrOptions?: string | ToastOptions) => { const options = typeof titleOrOptions === 'string' ? { title: titleOrOptions } : titleOrOptions; globalShowToast?.({ type: 'success', message, duration: 3000, ...options }); }, error: (message: string, titleOrOptions?: string | ToastOptions) => { const options = typeof titleOrOptions === 'string' ? { title: titleOrOptions } : titleOrOptions; globalShowToast?.({ type: 'error', message, duration: 5000, ...options }); }, warning: (message: string, titleOrOptions?: string | ToastOptions) => { const options = typeof titleOrOptions === 'string' ? { title: titleOrOptions } : titleOrOptions; globalShowToast?.({ type: 'warning', message, duration: 4000, ...options }); }, info: (message: string, titleOrOptions?: string | ToastOptions) => { const options = typeof titleOrOptions === 'string' ? { title: titleOrOptions } : titleOrOptions; globalShowToast?.({ type: 'info', message, duration: 3000, ...options }); }, }; const TOAST_ICONS: Record = { success: , error: , warning: , info: , }; const TOAST_STYLES: Record = { success: 'border-emerald-600 bg-emerald-50 dark:bg-emerald-950', error: 'border-red-600 bg-red-50 dark:bg-red-950', warning: 'border-yellow-600 bg-yellow-50 dark:bg-yellow-950', info: 'border-blue-600 bg-blue-50 dark:bg-blue-950', }; export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [toasts, setToasts] = useState([]); const showToast = useCallback((nextToast: Omit) => { const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2)}`; const created: Toast = { ...nextToast, id }; setToasts(prev => [...prev, created]); // Auto dismiss if (nextToast.duration !== 0) { setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, nextToast.duration || 4000); } }, []); const dismissToast = useCallback((id: string) => { setToasts(prev => prev.filter(t => t.id !== id)); }, []); const actionsValue = useMemo( () => ({ showToast, dismissToast }), [showToast, dismissToast], ); const stateValue = useMemo( () => ({ toasts }), [toasts], ); // Register global toast function useEffect(() => { globalShowToast = showToast; setNotify(toast); return () => { globalShowToast = null; }; }, [showToast]); return ( {children} ); }; const ToastContainer: React.FC = () => { const { toasts } = useContext(ToastStateContext) ?? { toasts: [] as Toast[] }; const actions = useContext(ToastActionsContext); const onDismiss = actions?.dismissToast; if (toasts.length === 0 || !onDismiss) return null; const handleToastClick = (t: Toast) => { if (t.onClick) { t.onClick(); onDismiss(t.id); } }; return (
{toasts.map(t => (
handleToastClick(t)} role={t.onClick ? "button" : undefined} tabIndex={t.onClick ? 0 : undefined} >
{TOAST_ICONS[t.type]}
{t.title && (
{t.title}
)}
{t.message}
{t.actionLabel && t.onClick && (
{t.actionLabel} →
)}
))}
); }; export default ToastProvider;