Files
mtg-online-drafter/src/client/src/components/Toast.tsx

89 lines
3.1 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback } from 'react';
import { X, Check, AlertCircle, Info } from 'lucide-react';
type ToastType = 'success' | 'error' | 'info' | 'warning';
interface Toast {
id: string;
message: string;
type: ToastType;
}
interface ToastContextType {
showToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = 'info') => {
const id = Math.random().toString(36).substring(2, 9);
setToasts((prev) => [...prev, { id, message, type }]);
// Auto remove after 3 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
const removeToast = (id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="fixed top-6 left-1/2 -translate-x-1/2 z-[9999] flex flex-col gap-3 pointer-events-none w-full max-w-sm px-4">
{toasts.map((toast) => (
<div
key={toast.id}
className={`
pointer-events-auto
flex items-center gap-4 px-4 py-3 rounded-xl border shadow-2xl
animate-in slide-in-from-top-full fade-in zoom-in-95 duration-300
bg-slate-800 text-white
${toast.type === 'success' ? 'border-emerald-500/50 shadow-emerald-900/20' :
toast.type === 'error' ? 'border-red-500/50 shadow-red-900/20' :
toast.type === 'warning' ? 'border-amber-500/50 shadow-amber-900/20' :
'border-blue-500/50 shadow-blue-900/20'}
`}
>
<div className={`p-2 rounded-full shrink-0 ${toast.type === 'success' ? 'bg-emerald-500/10 text-emerald-400' :
toast.type === 'error' ? 'bg-red-500/10 text-red-400' :
toast.type === 'warning' ? 'bg-amber-500/10 text-amber-400' :
'bg-blue-500/10 text-blue-400'
}`}>
{toast.type === 'success' && <Check className="w-5 h-5" />}
{toast.type === 'error' && <AlertCircle className="w-5 h-5" />}
{toast.type === 'warning' && <AlertCircle className="w-5 h-5" />}
{toast.type === 'info' && <Info className="w-5 h-5" />}
</div>
<div className="flex-1 text-sm font-medium">
{toast.message}
</div>
<button
onClick={() => removeToast(toast.id)}
className="p-1 hover:bg-slate-700 rounded transition-colors text-slate-400 hover:text-white"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
);
};