File size: 3,398 Bytes
88da18c | 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 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;
|