Spaces:
Running
Running
| import { useState, useCallback, createContext, useContext } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { CheckCircle, AlertTriangle, Info, X, Zap } from 'lucide-react'; | |
| import { cn } from '@/lib/utils'; | |
| type ToastType = 'success' | 'error' | 'info' | 'warning'; | |
| interface Toast { | |
| id: number; | |
| message: string; | |
| type: ToastType; | |
| } | |
| interface ToastContextValue { | |
| toast: (message: string, type?: ToastType) => void; | |
| } | |
| const ToastContext = createContext<ToastContextValue>({ toast: () => {} }); | |
| export function useToast() { | |
| return useContext(ToastContext); | |
| } | |
| let nextId = 0; | |
| export function ToastProvider({ children }: { children: React.ReactNode }) { | |
| const [toasts, setToasts] = useState<Toast[]>([]); | |
| const addToast = useCallback((message: string, type: ToastType = 'info') => { | |
| const id = ++nextId; | |
| setToasts((prev) => [...prev, { id, message, type }]); | |
| setTimeout(() => { | |
| setToasts((prev) => prev.filter((t) => t.id !== id)); | |
| }, 4000); | |
| }, []); | |
| const removeToast = useCallback((id: number) => { | |
| setToasts((prev) => prev.filter((t) => t.id !== id)); | |
| }, []); | |
| return ( | |
| <ToastContext.Provider value={{ toast: addToast }}> | |
| {children} | |
| <div className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none"> | |
| <AnimatePresence> | |
| {toasts.map((t) => ( | |
| <ToastItem key={t.id} toast={t} onDismiss={() => removeToast(t.id)} /> | |
| ))} | |
| </AnimatePresence> | |
| </div> | |
| </ToastContext.Provider> | |
| ); | |
| } | |
| const icons: Record<ToastType, React.ReactNode> = { | |
| success: <CheckCircle className="h-4 w-4 text-lab-manager" />, | |
| error: <AlertTriangle className="h-4 w-4 text-destructive" />, | |
| info: <Info className="h-4 w-4 text-primary" />, | |
| warning: <Zap className="h-4 w-4 text-judge" />, | |
| }; | |
| const borderColors: Record<ToastType, string> = { | |
| success: 'border-lab-manager/40', | |
| error: 'border-destructive/40', | |
| info: 'border-primary/40', | |
| warning: 'border-judge/40', | |
| }; | |
| function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20, scale: 0.95 }} | |
| animate={{ opacity: 1, y: 0, scale: 1 }} | |
| exit={{ opacity: 0, x: 80, scale: 0.95 }} | |
| transition={{ type: 'spring', stiffness: 400, damping: 25 }} | |
| className={cn( | |
| 'pointer-events-auto flex items-center gap-2.5 rounded-lg border bg-card px-4 py-3 text-sm shadow-lg backdrop-blur-sm', | |
| borderColors[toast.type], | |
| )} | |
| > | |
| {icons[toast.type]} | |
| <span className="max-w-[260px] text-card-foreground">{toast.message}</span> | |
| <button onClick={onDismiss} className="ml-1 text-muted-foreground hover:text-foreground"> | |
| <X className="h-3.5 w-3.5" /> | |
| </button> | |
| </motion.div> | |
| ); | |
| } | |