ToastContext.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
  2. import Toast, { ToastType } from '../components/Toast';
  3. import { registerToastHandler } from '../src/utils/toast';
  4. interface ToastItem {
  5. id: string;
  6. type: ToastType;
  7. title?: string;
  8. message: string;
  9. duration?: number;
  10. }
  11. interface ToastContextType {
  12. showToast: (type: ToastType, message: string, title?: string, duration?: number) => void;
  13. showSuccess: (message: string, title?: string) => void;
  14. showError: (message: string, title?: string) => void;
  15. showWarning: (message: string, title?: string) => void;
  16. showInfo: (message: string, title?: string) => void;
  17. }
  18. const ToastContext = createContext<ToastContextType | undefined>(undefined);
  19. export const useToast = () => {
  20. const context = useContext(ToastContext);
  21. if (!context) {
  22. throw new Error('useToast must be used within a ToastProvider');
  23. }
  24. return context;
  25. };
  26. interface ToastProviderProps {
  27. children: ReactNode;
  28. }
  29. export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
  30. const [toasts, setToasts] = useState<ToastItem[]>([]);
  31. const showToast = (type: ToastType, message: string, title?: string, duration?: number) => {
  32. const id = Date.now().toString();
  33. const newToast: ToastItem = { id, type, message, title, duration };
  34. setToasts(prev => {
  35. // Deduplicate identical messages: discard old one if current type and content are the same
  36. const filtered = prev.filter(t => t.message !== message || t.type !== type);
  37. return [...filtered, newToast];
  38. });
  39. };
  40. useEffect(() => {
  41. registerToastHandler({ showToast });
  42. }, []);
  43. const removeToast = (id: string) => {
  44. setToasts(prev => prev.filter(toast => toast.id !== id));
  45. };
  46. const showSuccess = (message: string, title?: string) => showToast('success', message, title);
  47. const showError = (message: string, title?: string) => showToast('error', message, title);
  48. const showWarning = (message: string, title?: string) => showToast('warning', message, title);
  49. const showInfo = (message: string, title?: string) => showToast('info', message, title);
  50. return (
  51. <ToastContext.Provider value={{ showToast, showSuccess, showError, showWarning, showInfo }}>
  52. {children}
  53. <div className="fixed bottom-0 right-0 z-[9999] p-4 space-y-3 pointer-events-none w-full max-w-sm flex flex-col items-end">
  54. {toasts.map((toast) => (
  55. <div
  56. key={toast.id}
  57. className="pointer-events-auto w-full"
  58. >
  59. <Toast
  60. type={toast.type}
  61. title={toast.title}
  62. message={toast.message}
  63. duration={toast.duration}
  64. onClose={() => removeToast(toast.id)}
  65. />
  66. </div>
  67. ))}
  68. </div>
  69. </ToastContext.Provider>
  70. );
  71. };