Mr-Haseeb786
Sanitized Production Build
56c7b6d
Raw History Blame Contribute Delete
3.4 kB
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle, AlertCircle, Info, X } from 'lucide-react';
import { cn } from '@/utils/helpers';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
interface ToastProps {
type: ToastType;
message: string;
title?: string;
onClose?: () => void;
autoClose?: boolean;
autoCloseDuration?: number;
action?: {
label: string;
onClick: () => void;
};
}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
(
{
type,
message,
title,
onClose,
autoClose = true,
autoCloseDuration = 5000,
action,
},
ref
) => {
const [isVisible, setIsVisible] = React.useState(true);
React.useEffect(() => {
if (!autoClose) return;
const timer = setTimeout(() => {
setIsVisible(false);
onClose?.();
}, autoCloseDuration);
return () => clearTimeout(timer);
}, [autoClose, autoCloseDuration, onClose]);
const icons = {
success: <CheckCircle className="w-5 h-5" />,
error: <AlertCircle className="w-5 h-5" />,
warning: <AlertCircle className="w-5 h-5" />,
info: <Info className="w-5 h-5" />,
};
const colors = {
success: 'bg-emerald-500/20 border-emerald-500/30 text-emerald-200',
error: 'bg-red-500/20 border-red-500/30 text-red-200',
warning: 'bg-yellow-500/20 border-yellow-500/30 text-yellow-200',
info: 'bg-blue-500/20 border-blue-500/30 text-blue-200',
};
const iconColors = {
success: 'text-emerald-400',
error: 'text-red-400',
warning: 'text-yellow-400',
info: 'text-blue-400',
};
return (
<AnimatePresence>
{isVisible && (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className={cn(
'flex items-start gap-3 w-full max-w-md px-4 py-3 rounded-lg border',
colors[type]
)}
>
<div className={cn('mt-0.5 flex-shrink-0', iconColors[type])}>
{icons[type]}
</div>
<div className="flex-1 min-w-0">
{title && (
<p className="font-semibold text-sm mb-0.5">{title}</p>
)}
<p className="text-sm opacity-90">{message}</p>
{action && (
<motion.button
whileHover={{ x: 2 }}
onClick={action.onClick}
className="text-xs font-medium mt-2 opacity-80 hover:opacity-100 transition-opacity"
>
{action.label}
</motion.button>
)}
</div>
{onClose && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={() => {
setIsVisible(false);
onClose?.();
}}
className="flex-shrink-0 opacity-50 hover:opacity-100 transition-opacity"
>
<X className="w-4 h-4" />
</motion.button>
)}
</motion.div>
)}
</AnimatePresence>
);
}
);
Toast.displayName = 'Toast';
export default Toast;