81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
|
|
interface Toast {
|
|
id: number;
|
|
message: string;
|
|
type: ToastType;
|
|
}
|
|
|
|
interface ToastContextType {
|
|
showToast: (message: string, type?: ToastType) => void;
|
|
}
|
|
|
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
|
|
export function useToast() {
|
|
const context = useContext(ToastContext);
|
|
if (!context) throw new Error('useToast must be used within a ToastProvider');
|
|
return context;
|
|
}
|
|
|
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
|
|
|
const showToast = useCallback((message: string, type: ToastType = 'success') => {
|
|
const id = Date.now();
|
|
setToasts(prev => [...prev, { id, message, type }]);
|
|
setTimeout(() => {
|
|
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
}, 3000);
|
|
}, []);
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ showToast }}>
|
|
{children}
|
|
<ToastContainer toasts={toasts} />
|
|
</ToastContext.Provider>
|
|
);
|
|
}
|
|
|
|
function ToastContainer({ toasts }: { toasts: Toast[] }) {
|
|
if (typeof document === 'undefined') return null;
|
|
|
|
return createPortal(
|
|
<div className="fixed bottom-8 right-8 z-[100] flex flex-col gap-3 pointer-events-none">
|
|
{toasts.map(toast => (
|
|
<ToastItem key={toast.id} toast={toast} />
|
|
))}
|
|
</div>,
|
|
document.body
|
|
);
|
|
}
|
|
|
|
function ToastItem({ toast }: { toast: Toast }) {
|
|
const icons = {
|
|
success: <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M5 13l4 4L19 7" /></svg>,
|
|
error: <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M6 18L18 6M6 6l12 12" /></svg>,
|
|
warning: <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>,
|
|
info: <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>,
|
|
};
|
|
|
|
const colors = {
|
|
success: "bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20",
|
|
error: "bg-red-50 text-red-600 border-red-100 dark:bg-red-500/10 dark:text-red-400 dark:border-red-500/20",
|
|
warning: "bg-amber-50 text-amber-600 border-amber-100 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20",
|
|
info: "bg-blue-50 text-blue-600 border-blue-100 dark:bg-blue-500/10 dark:text-blue-400 dark:border-blue-500/20",
|
|
};
|
|
|
|
return (
|
|
<div className={`
|
|
flex items-center gap-3 px-6 py-4 rounded-2xl border shadow-xl shadow-black/5 pointer-events-auto anim-right
|
|
${colors[toast.type]}
|
|
`}>
|
|
<div className="shrink-0">{icons[toast.type]}</div>
|
|
<div className="text-sm font-bold tracking-tight">{toast.message}</div>
|
|
</div>
|
|
);
|
|
}
|