66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||
|
||
type ToastTone = 'success' | 'error' | 'info';
|
||
|
||
interface ToastItem {
|
||
id: number;
|
||
message: string;
|
||
tone: ToastTone;
|
||
}
|
||
|
||
interface ToastContextValue {
|
||
showToast: (message: string, tone?: ToastTone) => void;
|
||
success: (message: string) => void;
|
||
error: (message: string) => void;
|
||
info: (message: string) => void;
|
||
}
|
||
|
||
const ToastContext = createContext<ToastContextValue | null>(null);
|
||
|
||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
||
|
||
const dismissToast = useCallback((id: number) => {
|
||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||
}, []);
|
||
|
||
const showToast = useCallback((message: string, tone: ToastTone = 'info') => {
|
||
const id = Date.now() + Math.floor(Math.random() * 1000);
|
||
setToasts((prev) => [...prev, { id, message, tone }]);
|
||
window.setTimeout(() => {
|
||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||
}, 5000);
|
||
}, []);
|
||
|
||
const value = useMemo<ToastContextValue>(() => ({
|
||
showToast,
|
||
success: (message) => showToast(message, 'success'),
|
||
error: (message) => showToast(message, 'error'),
|
||
info: (message) => showToast(message, 'info')
|
||
}), [showToast]);
|
||
|
||
return (
|
||
<ToastContext.Provider value={value}>
|
||
{children}
|
||
<div className="toast-viewport" aria-live="polite" aria-atomic="true">
|
||
{toasts.map((toast) => (
|
||
<div key={toast.id} className={`toast toast-${toast.tone}`}>
|
||
<div className="toast-message">{toast.message}</div>
|
||
<button className="toast-close" type="button" onClick={() => dismissToast(toast.id)} aria-label="Dismiss notification">
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</ToastContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const useToast = (): ToastContextValue => {
|
||
const context = useContext(ToastContext);
|
||
if (!context) {
|
||
throw new Error('useToast must be used within a ToastProvider');
|
||
}
|
||
return context;
|
||
};
|