diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx new file mode 100644 index 000000000..0b3781d37 --- /dev/null +++ b/frontend/src/components/ui/Toast.tsx @@ -0,0 +1,113 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; + +type ToastType = 'success' | 'error' | 'info' | 'warning'; + +interface Toast { + id: string; + type: ToastType; + title: string; + message?: string; + duration?: number; +} + +interface ToastContextValue { + toasts: Toast[]; + addToast: (toast: Omit) => void; + removeToast: (id: string) => void; +} + +export const ToastContext = React.createContext({ + toasts: [], + addToast: () => {}, + removeToast: () => {}, +}); + +export function useToast() { + return React.useContext(ToastContext); +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((toast: Omit) => { + const id = Math.random().toString(36).substring(2, 9); + const newToast: Toast = { ...toast, id }; + setToasts((prev) => [...prev, newToast]); + + // Auto-dismiss after duration + const duration = toast.duration ?? 5000; + if (duration > 0) { + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, duration); + } + }, []); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, []); + + return ( + + {children} + + + ); +} + +const ICONS: Record = { + success: , + error: , + info: , + warning: , +}; + +const BORDER_COLORS: Record = { + success: 'border-emerald/30', + error: 'border-status-error/30', + info: 'border-status-info/30', + warning: 'border-status-warning/30', +}; + +function ToastContainer({ + toasts, + onRemove, +}: { + toasts: Toast[]; + onRemove: (id: string) => void; +}) { + return ( +
+ + {toasts.map((toast) => ( + +
+
{ICONS[toast.type]}
+
+

{toast.title}

+ {toast.message && ( +

{toast.message}

+ )} +
+ +
+
+ ))} +
+
+ ); +}