Files
papercrate/frontend/src/lib/context/StatusToastContext.tsx
T

116 lines
3.4 KiB
TypeScript

import React, { useState, useCallback, useEffect, useRef } from 'react';
import { createSafeContext } from '../../utils/createSafeContext';
export type ToastVariant = 'info' | 'success' | 'error';
interface ToastMessage {
id: string;
message: string;
variant: ToastVariant;
timestamp: number;
duration: number;
}
interface StatusToastContextValue {
toasts: ToastMessage[];
showToast: (message: string, variant?: ToastVariant, duration?: number) => void;
removeToast: (id: string) => void;
}
const [StatusToastContext, useStatusToast] = createSafeContext<StatusToastContextValue>('StatusToast');
const DEFAULT_DURATIONS: Record<ToastVariant, number> = {
success: 3000,
info: 5000,
error: 8000,
};
const MAX_TOASTS = 3;
export const StatusToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const timeoutRefs = useRef<Map<string, number>>(new Map());
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
// Clear timeout if it exists
const timeout = timeoutRefs.current.get(id);
if (timeout) {
clearTimeout(timeout);
timeoutRefs.current.delete(id);
}
}, []);
const showToast = useCallback((message: string, variant: ToastVariant = 'info', duration?: number) => {
// Log to console based on variant
if (variant === 'error') {
console.error(`[Toast] ${message}`);
} else if (variant === 'success') {
console.log(`[Toast] ${message}`);
} else {
console.info(`[Toast] ${message}`);
}
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const finalDuration = duration ?? DEFAULT_DURATIONS[variant];
const newToast: ToastMessage = {
id,
message,
variant,
timestamp: Date.now(),
duration: finalDuration,
};
setToasts((prev) => {
const updated = [...prev, newToast];
// If we exceed max toasts, remove the oldest ones
if (updated.length > MAX_TOASTS) {
const removed = updated.slice(0, updated.length - MAX_TOASTS);
removed.forEach((toast) => {
const timeout = timeoutRefs.current.get(toast.id);
if (timeout) {
clearTimeout(timeout);
timeoutRefs.current.delete(toast.id);
}
});
return updated.slice(-MAX_TOASTS);
}
return updated;
});
// Set auto-dismiss timeout - trigger fade then remove
const timeout = setTimeout(() => {
removeToast(id);
}, finalDuration);
timeoutRefs.current.set(id, timeout);
}, [removeToast]);
// Cleanup all timeouts on unmount
useEffect(() => {
const timeouts = timeoutRefs.current;
return () => {
timeouts.forEach((timeout) => clearTimeout(timeout));
timeouts.clear();
};
}, []);
const value: StatusToastContextValue = {
toasts,
showToast,
removeToast,
};
return (
<StatusToastContext.Provider value={value}>
{children}
</StatusToastContext.Provider>
);
};
export { useStatusToast };