64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
|
|
import React, { useEffect } from 'react';
|
|
import { CheckCircle, AlertCircle, X } from 'lucide-react';
|
|
|
|
export interface ToastMessage {
|
|
id: string;
|
|
type: 'success' | 'error' | 'info';
|
|
message: string;
|
|
}
|
|
|
|
interface ToastProps {
|
|
toasts: ToastMessage[];
|
|
onRemove: (id: string) => void;
|
|
}
|
|
|
|
const ToastContainer: React.FC<ToastProps> = ({ toasts, onRemove }) => {
|
|
return (
|
|
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-[100] flex flex-col gap-2 w-full max-w-md px-4">
|
|
{toasts.map((toast) => (
|
|
<ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ToastItem: React.FC<{ toast: ToastMessage; onRemove: (id: string) => void }> = ({ toast, onRemove }) => {
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
onRemove(toast.id);
|
|
}, 3000);
|
|
return () => clearTimeout(timer);
|
|
}, [toast.id, onRemove]);
|
|
|
|
const getStyles = () => {
|
|
switch (toast.type) {
|
|
case 'success':
|
|
return 'bg-emerald-50 border-emerald-100 text-emerald-700';
|
|
case 'error':
|
|
return 'bg-red-50 border-red-100 text-red-700';
|
|
default:
|
|
return 'bg-indigo-50 border-indigo-100 text-indigo-700';
|
|
}
|
|
};
|
|
|
|
const getIcon = () => {
|
|
switch (toast.type) {
|
|
case 'success': return <CheckCircle size={20} className="text-emerald-500" />;
|
|
case 'error': return <AlertCircle size={20} className="text-red-500" />;
|
|
default: return <AlertCircle size={20} className="text-indigo-500" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`flex items-center gap-3 p-4 rounded-2xl border shadow-lg animate-fade-in-up ${getStyles()}`}>
|
|
{getIcon()}
|
|
<p className="text-sm font-bold flex-1">{toast.message}</p>
|
|
<button onClick={() => onRemove(toast.id)} className="opacity-50 hover:opacity-100">
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ToastContainer; |