You've already forked Nano-Banana-AI-Image-Editor
125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
import React, { createContext, useContext, useReducer, useState, useEffect, useRef } from 'react';
|
|
import { Toast } from './Toast';
|
|
|
|
type ToastType = 'success' | 'error' | 'warning' | 'info';
|
|
|
|
export interface ToastMessage {
|
|
id: string;
|
|
message: string;
|
|
type: ToastType;
|
|
duration?: number;
|
|
details?: string;
|
|
}
|
|
|
|
export interface ToastContextType {
|
|
addToast: (message: string, type: ToastType, duration?: number, details?: string) => void;
|
|
removeToast: (id: string) => void;
|
|
}
|
|
|
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
|
|
type ToastAction =
|
|
| { type: 'ADD_TOAST'; payload: ToastMessage }
|
|
| { type: 'REMOVE_TOAST'; payload: string }
|
|
| { type: 'SET_HOVERED_TOAST', payload: { id: string, hovered: boolean } };
|
|
|
|
const toastReducer = (state: ToastMessage[], action: ToastAction): ToastMessage[] => {
|
|
switch (action.type) {
|
|
case 'ADD_TOAST':
|
|
return [...state, { ...action.payload, hovered: false }];
|
|
case 'REMOVE_TOAST':
|
|
return state.filter(toast => toast.id !== action.payload);
|
|
case 'SET_HOVERED_TOAST':
|
|
return state.map(toast =>
|
|
toast.id === action.payload.id
|
|
? { ...toast, hovered: action.payload.hovered }
|
|
: toast
|
|
);
|
|
default:
|
|
return state;
|
|
}
|
|
};
|
|
|
|
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [toasts, dispatch] = useReducer(toastReducer, []);
|
|
const hoverTimeouts = useRef<Record<string, NodeJS.Timeout>>({});
|
|
|
|
const addToast = (message: string, type: ToastType, duration: number = 5000, details?: string) => {
|
|
const id = Date.now().toString();
|
|
dispatch({ type: 'ADD_TOAST', payload: { id, message, type, duration, details } });
|
|
};
|
|
|
|
const removeToast = (id: string) => {
|
|
dispatch({ type: 'REMOVE_TOAST', payload: id });
|
|
};
|
|
|
|
const setToastHovered = (id: string, hovered: boolean) => {
|
|
dispatch({ type: 'SET_HOVERED_TOAST', payload: { id, hovered } });
|
|
};
|
|
|
|
// Auto remove toasts after duration, but respect hover state
|
|
useEffect(() => {
|
|
const timers = toasts.map(toast => {
|
|
// Clear any existing timeout for this toast
|
|
if (hoverTimeouts.current[toast.id]) {
|
|
clearTimeout(hoverTimeouts.current[toast.id]);
|
|
delete hoverTimeouts.current[toast.id];
|
|
}
|
|
|
|
// If toast is hovered, don't set a timer
|
|
if (toast.hovered) {
|
|
return null;
|
|
}
|
|
|
|
// If duration is 0, it's persistent
|
|
if (toast.duration === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Set timeout to remove toast
|
|
const timeout = setTimeout(() => {
|
|
removeToast(toast.id);
|
|
}, toast.duration);
|
|
|
|
return { id: toast.id, timeout };
|
|
});
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
timers.forEach(timer => {
|
|
if (timer) clearTimeout(timer.timeout);
|
|
});
|
|
};
|
|
}, [toasts]);
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ addToast, removeToast }}>
|
|
{children}
|
|
<div className="fixed top-4 right-4 z-50 space-y-2">
|
|
{toasts.map(toast => (
|
|
<div
|
|
key={toast.id}
|
|
className="animate-in slide-in-from-right duration-300"
|
|
>
|
|
<Toast
|
|
id={toast.id}
|
|
message={toast.message}
|
|
type={toast.type}
|
|
details={toast.details}
|
|
onClose={removeToast}
|
|
onHoverChange={(hovered) => setToastHovered(toast.id, hovered)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ToastContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useToast = () => {
|
|
const context = useContext(ToastContext);
|
|
if (!context) {
|
|
throw new Error('useToast must be used within a ToastProvider');
|
|
}
|
|
return context;
|
|
}; |