frontend_nyla/src/contexts/ToastContext.tsx
2026-01-21 10:59:34 +01:00

97 lines
3 KiB
TypeScript

import { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
import { ToastContainer, ToastData, ToastType } from '../components/UiComponents/Toast';
interface ToastOptions {
title: string;
message?: string;
duration?: number;
}
interface ToastContextValue {
showToast: (type: ToastType, options: ToastOptions) => void;
showSuccess: (title: string, message?: string) => void;
showError: (title: string, message?: string) => void;
showWarning: (title: string, message?: string) => void;
showInfo: (title: string, message?: string) => void;
closeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
const DEFAULT_DURATION = 5000;
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<ToastData[]>([]);
const timeoutRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
const closeToast = useCallback((id: string) => {
// Clear timeout if exists
const timeout = timeoutRefs.current.get(id);
if (timeout) {
clearTimeout(timeout);
timeoutRefs.current.delete(id);
}
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const showToast = useCallback((type: ToastType, options: ToastOptions) => {
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const duration = options.duration ?? DEFAULT_DURATION;
const newToast: ToastData = {
id,
type,
title: options.title,
message: options.message,
duration,
};
setToasts((prev) => [...prev, newToast]);
// Auto-close after duration
if (duration > 0) {
const timeout = setTimeout(() => {
closeToast(id);
}, duration);
timeoutRefs.current.set(id, timeout);
}
}, [closeToast]);
const showSuccess = useCallback((title: string, message?: string) => {
showToast('success', { title, message });
}, [showToast]);
const showError = useCallback((title: string, message?: string) => {
showToast('error', { title, message, duration: 8000 }); // Errors stay longer
}, [showToast]);
const showWarning = useCallback((title: string, message?: string) => {
showToast('warning', { title, message, duration: 6000 });
}, [showToast]);
const showInfo = useCallback((title: string, message?: string) => {
showToast('info', { title, message });
}, [showToast]);
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
timeoutRefs.current.forEach((timeout) => clearTimeout(timeout));
};
}, []);
return (
<ToastContext.Provider value={{ showToast, showSuccess, showError, showWarning, showInfo, closeToast }}>
{children}
<ToastContainer toasts={toasts} onClose={closeToast} />
</ToastContext.Provider>
);
};
export const useToast = (): ToastContextValue => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};