84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
|
|
import React, { useState, useCallback, createContext, useContext } from 'react';
|
|
|
|
type ToastType = 'success' | 'error' | 'info';
|
|
|
|
interface Toast {
|
|
id: number;
|
|
message: string;
|
|
type: ToastType;
|
|
}
|
|
|
|
interface ToastContextType {
|
|
addToast: (message: string, type?: ToastType) => void;
|
|
}
|
|
|
|
const ToastContext = createContext<ToastContextType>({ addToast: () => { } });
|
|
|
|
export const useToast = () => useContext(ToastContext);
|
|
|
|
let toastId = 0;
|
|
|
|
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
|
|
const addToast = useCallback((message: string, type: ToastType = 'info') => {
|
|
const id = ++toastId;
|
|
setToasts(prev => [...prev, { id, message, type }]);
|
|
setTimeout(() => {
|
|
setToasts(prev => prev.filter(t => t.id !== id));
|
|
}, 4000);
|
|
}, []);
|
|
|
|
const removeToast = (id: number) => {
|
|
setToasts(prev => prev.filter(t => t.id !== id));
|
|
};
|
|
|
|
const icons: Record<ToastType, string> = {
|
|
success: 'M5 13l4 4L19 7',
|
|
error: 'M6 18L18 6M6 6l12 12',
|
|
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
};
|
|
|
|
const colors: Record<ToastType, string> = {
|
|
success: 'bg-emerald-500',
|
|
error: 'bg-red-500',
|
|
info: 'bg-blue-500',
|
|
};
|
|
|
|
const bgColors: Record<ToastType, string> = {
|
|
success: 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800',
|
|
error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800',
|
|
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
|
|
};
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ addToast }}>
|
|
{children}
|
|
<div className="fixed bottom-6 right-6 z-[200] flex flex-col gap-3 pointer-events-none">
|
|
{toasts.map(toast => (
|
|
<div
|
|
key={toast.id}
|
|
className={`pointer-events-auto flex items-center gap-3 px-5 py-3.5 rounded-2xl shadow-xl border backdrop-blur-sm animate-in slide-in-from-right-5 fade-in duration-300 min-w-[300px] max-w-[420px] ${bgColors[toast.type]}`}
|
|
>
|
|
<div className={`w-7 h-7 rounded-lg ${colors[toast.type]} flex items-center justify-center shrink-0`}>
|
|
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d={icons[toast.type]} />
|
|
</svg>
|
|
</div>
|
|
<p className="text-sm font-semibold text-slate-800 dark:text-slate-100 flex-1">{toast.message}</p>
|
|
<button
|
|
onClick={() => removeToast(toast.id)}
|
|
className="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-white shrink-0"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ToastContext.Provider>
|
|
);
|
|
};
|