Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | import { useState, useCallback, createContext, useContext } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react'; import { cn } from '../../lib/utils'; type ToastType = 'success' | 'error' | 'info' | 'warning'; interface Toast { id: string; message: string; type: ToastType; } interface ToastContextType { toast: (message: string, type?: ToastType) => void; } const ToastContext = createContext<ToastContextType | null>(null); export function useToast() { const context = useContext(ToastContext); if (!context) { throw new Error('useToast must be used within a ToastProvider'); } return context; } const iconMap: Record<ToastType, React.ReactNode> = { success: <CheckCircle className="w-5 h-5 text-green-500" />, error: <AlertCircle className="w-5 h-5 text-red-500" />, info: <Info className="w-5 h-5 text-blue-500" />, warning: <AlertTriangle className="w-5 h-5 text-yellow-500" />, }; const styleMap: Record<ToastType, string> = { success: 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800', error: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800', info: 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800', warning: 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800', }; export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] = useState<Toast[]>([]); const toast = useCallback((message: string, type: ToastType = 'info') => { const id = Date.now().toString(); setToasts((prev) => [...prev, { id, message, type }]); setTimeout(() => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, 3000); }, []); const removeToast = useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)); }, []); return ( <ToastContext.Provider value={{ toast }}> {children} <div className="fixed bottom-4 right-4 z-50 space-y-2"> <AnimatePresence> {toasts.map((t) => ( <motion.div key={t.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} className={cn( 'flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg', 'bg-white dark:bg-gray-800', styleMap[t.type] )} > {iconMap[t.type]} <span className="text-sm text-gray-700 dark:text-gray-200">{t.message}</span> <button onClick={() => removeToast(t.id)} className="ml-2 text-gray-400 hover:text-gray-600" aria-label="Close" > <X className="w-4 h-4" /> </button> </motion.div> ))} </AnimatePresence> </div> </ToastContext.Provider> ); } |