Files

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>
);
}