116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
|
|
import React, { useEffect, useState } from 'react';
|
||
|
|
import { X, CheckCircle2, AlertCircle, Info, Sparkles } from 'lucide-react';
|
||
|
|
|
||
|
|
export type ToastType = 'success' | 'error' | 'info' | 'milestone';
|
||
|
|
|
||
|
|
export interface Toast {
|
||
|
|
id: string;
|
||
|
|
type: ToastType;
|
||
|
|
title: string;
|
||
|
|
message?: string;
|
||
|
|
duration?: number;
|
||
|
|
icon?: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ToastNotificationProps {
|
||
|
|
toast: Toast;
|
||
|
|
onClose: (id: string) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ToastNotification: React.FC<ToastNotificationProps> = ({ toast, onClose }) => {
|
||
|
|
const [isExiting, setIsExiting] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const duration = toast.duration || 5000;
|
||
|
|
const timer = setTimeout(() => {
|
||
|
|
setIsExiting(true);
|
||
|
|
setTimeout(() => onClose(toast.id), 300); // Wait for animation
|
||
|
|
}, duration);
|
||
|
|
|
||
|
|
return () => clearTimeout(timer);
|
||
|
|
}, [toast.id, toast.duration, onClose]);
|
||
|
|
|
||
|
|
const getIcon = () => {
|
||
|
|
if (toast.icon) return toast.icon;
|
||
|
|
|
||
|
|
switch (toast.type) {
|
||
|
|
case 'success':
|
||
|
|
return <CheckCircle2 className="w-5 h-5" />;
|
||
|
|
case 'error':
|
||
|
|
return <AlertCircle className="w-5 h-5" />;
|
||
|
|
case 'milestone':
|
||
|
|
return <Sparkles className="w-5 h-5" />;
|
||
|
|
case 'info':
|
||
|
|
default:
|
||
|
|
return <Info className="w-5 h-5" />;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStyles = () => {
|
||
|
|
switch (toast.type) {
|
||
|
|
case 'success':
|
||
|
|
return 'bg-green-50 border-green-200 text-green-800';
|
||
|
|
case 'error':
|
||
|
|
return 'bg-red-50 border-red-200 text-red-800';
|
||
|
|
case 'milestone':
|
||
|
|
return 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 text-purple-800';
|
||
|
|
case 'info':
|
||
|
|
default:
|
||
|
|
return 'bg-blue-50 border-blue-200 text-blue-800';
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`
|
||
|
|
w-full max-w-sm p-4 rounded-lg border shadow-lg backdrop-blur-sm
|
||
|
|
transition-all duration-300 transform
|
||
|
|
${getStyles()}
|
||
|
|
${isExiting ? 'opacity-0 translate-x-full' : 'opacity-100 translate-x-0'}
|
||
|
|
`}
|
||
|
|
>
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
{/* Icon */}
|
||
|
|
<div className="flex-shrink-0 mt-0.5">
|
||
|
|
{getIcon()}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<h3 className="text-sm font-semibold mb-0.5">
|
||
|
|
{toast.title}
|
||
|
|
</h3>
|
||
|
|
{toast.message && (
|
||
|
|
<p className="text-sm opacity-90">
|
||
|
|
{toast.message}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Close Button */}
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setIsExiting(true);
|
||
|
|
setTimeout(() => onClose(toast.id), 300);
|
||
|
|
}}
|
||
|
|
className="flex-shrink-0 p-1 hover:bg-black/10 rounded transition-colors"
|
||
|
|
>
|
||
|
|
<X className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Progress Bar */}
|
||
|
|
{toast.type === 'milestone' && (
|
||
|
|
<div className="mt-3 h-1 bg-white/30 rounded-full overflow-hidden">
|
||
|
|
<div
|
||
|
|
className="h-full bg-gradient-to-r from-purple-400 to-pink-400 animate-pulse"
|
||
|
|
style={{
|
||
|
|
animation: `progress ${toast.duration || 5000}ms linear`
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|