import React, { createContext, useContext, useState, useCallback } from 'react';
import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
import { Toast, ToastType } from '@/hooks/useToast';
import { motion, AnimatePresence } from 'framer-motion';
// Create ToastContext
const ToastContext = createContext<{
toasts: Toast[];
removeToast: (id: string) => void;
success: (message: string, duration?: number) => string;
error: (message: string, duration?: number) => string;
warning: (message: string, duration?: number) => string;
info: (message: string, duration?: number) => string;
} | null>(null);
const ToastItem: React.FC<{ toast: Toast; onRemove: (id: string) => void }> = ({ toast, onRemove }) => {
const getIcon = () => {
switch (toast.type) {
case 'success':
return <CheckCircle className="h-5 w-5 text-green-600" />;
case 'error':
return <AlertCircle className="h-5 w-5 text-red-600" />;
case 'warning':
return <AlertTriangle className="h-5 w-5 text-yellow-600" />;
case 'info':
return <Info className="h-5 w-5 text-blue-600" />;
}
};
const getBgColor = () => {
switch (toast.type) {
case 'success':
return 'bg-green-50 border-green-200';
case 'error':
return 'bg-red-50 border-red-200';
case 'warning':
return 'bg-yellow-50 border-yellow-200';
case 'info':
return 'bg-blue-50 border-blue-200';
}
};
return (
<motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.2 } }}
className={`${getBgColor()} border rounded-lg shadow-lg p-4 min-w-[calc(100vw-2rem)] sm:min-w-[300px] max-w-md`}
>
<div className="flex items-start gap-3">
{getIcon()}
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{toast.message}</p>
</div>
<button
onClick={() => onRemove(toast.id)}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="h-4 w-4" />
</button>
</div>
</motion.div>
);
};
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const showToast = useCallback((message: string, type: ToastType = 'info', duration: number = 5000) => {
const id = Math.random().toString(36).substr(2, 9);
const toast: Toast = { id, message, type, duration };
setToasts((prev) => [...prev, toast]);
if (duration > 0) {
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, duration);
}
return id;
}, []);
const success = useCallback((message: string, duration?: number) => {
return showToast(message, 'success', duration);
}, [showToast]);
const error = useCallback((message: string, duration?: number) => {
return showToast(message, 'error', duration);
}, [showToast]);
const warning = useCallback((message: string, duration?: number) => {
return showToast(message, 'warning', duration);
}, [showToast]);
const info = useCallback((message: string, duration?: number) => {
return showToast(message, 'info', duration);
}, [showToast]);
return (
<ToastContext.Provider value={{ toasts, removeToast, success, error, warning, info }}>
{children}
<ToastContainer />
</ToastContext.Provider>
);
};
const ToastContainer: React.FC = () => {
const context = useContext(ToastContext);
if (!context) return null;
const { toasts, removeToast } = context;
return (
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
<AnimatePresence>
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onRemove={removeToast} />
))}
</AnimatePresence>
</div>
);
};
// Export useToast hook
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
// Fallback if used outside provider
return {
toasts: [],
removeToast: () => {},
success: () => '',
error: () => '',
warning: () => '',
info: () => '',
};
}
return context;
};