← back to Norma
components/ToastProvider.tsx
98 lines
'use client';
import { createContext, useContext, useState, useCallback, useRef, type ReactNode } from 'react';
type ToastType = 'success' | 'error' | 'info' | 'warning';
interface Toast {
id: number;
message: string;
type: ToastType;
}
interface ToastContextType {
addToast: (message: string, type?: ToastType) => void;
}
const ToastContext = createContext<ToastContextType>({ addToast: () => {} });
export function useToast() {
return useContext(ToastContext);
}
const TYPE_STYLES: Record<ToastType, { bg: string; border: string; text: string }> = {
success: { bg: 'rgba(34,197,94,0.15)', border: 'rgba(34,197,94,0.4)', text: '#4ade80' },
error: { bg: 'rgba(239,68,68,0.15)', border: 'rgba(239,68,68,0.4)', text: '#f87171' },
info: { bg: 'rgba(99,102,241,0.15)', border: 'rgba(99,102,241,0.4)', text: '#818cf8' },
warning: { bg: 'rgba(245,158,11,0.15)', border: 'rgba(245,158,11,0.4)', text: '#fbbf24' },
};
const ICONS: Record<ToastType, string> = {
success: '\u2713',
error: '\u2715',
warning: '\u26A0',
info: '\u2139',
};
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const nextId = useRef(0);
const addToast = useCallback((message: string, type: ToastType = 'success') => {
const id = ++nextId.current;
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3500);
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
<div
role="status"
aria-live="polite"
style={{
position: 'fixed',
bottom: 16,
right: 16,
zIndex: 9999,
display: 'flex',
flexDirection: 'column',
gap: 8,
pointerEvents: 'none',
maxWidth: 380,
}}>
{toasts.map((toast) => {
const style = TYPE_STYLES[toast.type];
return (
<div
key={toast.id}
style={{
padding: '10px 16px',
borderRadius: 10,
backgroundColor: style.bg,
border: `1px solid ${style.border}`,
color: style.text,
fontSize: '0.8125rem',
fontWeight: 500,
backdropFilter: 'blur(12px)',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
animation: 'toast-in 0.25s ease-out',
pointerEvents: 'auto',
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<span style={{ fontSize: '1rem', lineHeight: 1 }}>
{ICONS[toast.type]}
</span>
{toast.message}
</div>
);
})}
</div>
</ToastContext.Provider>
);
}