import React, { useEffect, useState } from 'react'; import { clsx } from 'clsx'; import { X, CheckCircle2, AlertCircle, Info, AlertTriangle } from 'lucide-react'; import Button from './Button'; export type ToastType = 'success' | 'error' | 'warning' | 'info'; export interface ToastProps { id: string; message: string; description?: string; type?: ToastType; duration?: number; onClose: (id: string) => void; action?: { label: string; onClick: () => void; }; } const toastIcons = { success: CheckCircle2, error: AlertCircle, warning: AlertTriangle, info: Info, }; const toastStyles = { success: 'bg-success/10 border-success/20 text-success-foreground', error: 'bg-destructive/10 border-destructive/20 text-destructive-foreground', warning: 'bg-warning/10 border-warning/20 text-warning-foreground', info: 'bg-primary/10 border-primary/20 text-primary-foreground', }; /** * Individual toast notification */ export const Toast = ({ id, message, description, type = 'info', duration = 5000, onClose, action, }: ToastProps) => { const [isVisible, setIsVisible] = useState(true); useEffect(() => { if (duration > 0) { const timer = setTimeout(() => { setIsVisible(false); setTimeout(() => onClose(id), 300); // Wait for animation }, duration); return () => clearTimeout(timer); } }, [duration, id, onClose]); const Icon = toastIcons[type]; if (!isVisible) return null; return (

{message}

{description &&

{description}

} {action && (
)}
); }; /** * Toast container/provider */ export interface ToastContainerProps { toasts: ToastProps[]; onClose: (id: string) => void; position?: | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'; } export const ToastContainer = ({ toasts, onClose, position = 'top-right', }: ToastContainerProps) => { const positionClasses = { 'top-right': 'top-4 right-4', 'top-left': 'top-4 left-4', 'bottom-right': 'bottom-4 right-4', 'bottom-left': 'bottom-4 left-4', 'top-center': 'top-4 left-1/2 -translate-x-1/2', 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2', }; if (toasts.length === 0) return null; return (
{toasts.map((toast) => ( ))}
); }; /** * Hook for managing toasts */ export const useToast = () => { const [toasts, setToasts] = useState([]); const showToast = ( message: string, options?: { description?: string; type?: ToastType; duration?: number; action?: { label: string; onClick: () => void }; } ) => { const id = `toast-${Date.now()}-${Math.random()}`; const newToast: ToastProps = { id, message, description: options?.description, type: options?.type || 'info', duration: options?.duration ?? 5000, onClose: (id) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, action: options?.action, }; setToasts((prev) => [...prev, newToast]); return id; }; const removeToast = (id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }; return { toasts, showToast, removeToast, success: (message: string, options?: Omit[1], 'type'>) => showToast(message, { ...options, type: 'success' }), error: (message: string, options?: Omit[1], 'type'>) => showToast(message, { ...options, type: 'error' }), warning: (message: string, options?: Omit[1], 'type'>) => showToast(message, { ...options, type: 'warning' }), info: (message: string, options?: Omit[1], 'type'>) => showToast(message, { ...options, type: 'info' }), }; };