/** * Centralized notification service and provider. * * Provides a single channel for displaying user-facing notifications (errors, success, warnings, info). * Prevents duplicate messaging, handles auto-dismiss, and maintains a queue of active notifications. * Consumers use the `useNotification` hook to access the service. */ import { createContext, useCallback, useContext, useMemo, useState } from "react"; import type { Notification, NotificationIntent, NotificationService } from "../types/notification"; /** Default auto-dismiss durations (milliseconds) for each notification type. */ const DEFAULT_DURATIONS: Record = { success: 5000, error: 8000, warning: 6000, info: 5000, }; /** Context holding active notifications and the service API. */ const NotificationContext = createContext(null); /** * NotificationProvider component. * Must wrap the entire application to make notifications available to all children. */ export function NotificationProvider({ children }: { children: React.ReactNode }): React.JSX.Element { const [notifications, setNotifications] = useState([]); /** * Remove a notification by ID. */ const dismiss = useCallback((id: string): void => { setNotifications((prev) => prev.filter((n) => n.id !== id)); }, []); /** * Check if a notification with this message and intent already exists to prevent duplicates. */ const isDuplicate = useCallback( (message: string, intent: NotificationIntent): boolean => notifications.some((n) => n.message === message && n.intent === intent), [notifications], ); /** * Generate a unique ID for a notification. */ const generateId = useCallback((): string => { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 9); return `notif-${timestamp}-${random}`; }, []); /** * Add a notification to the queue. */ const add = useCallback( (intent: NotificationIntent, message: string, autoCloseMsec?: number): void => { // Prevent duplicate identical notifications if (isDuplicate(message, intent)) { return; } const id = generateId(); const duration = autoCloseMsec !== undefined ? autoCloseMsec : DEFAULT_DURATIONS[intent]; setNotifications((prev) => [...prev, { id, intent, message, autoCloseMsec: duration }]); // Schedule auto-dismiss if duration is set if (duration) { setTimeout(() => { dismiss(id); }, duration); } }, [isDuplicate, generateId, dismiss], ); /** * Public service API. */ const service: NotificationService = useMemo((): NotificationService => { return { success: (message: string, autoCloseMsec?: number): void => { add("success", message, autoCloseMsec); }, error: (message: string, autoCloseMsec?: number): void => { add("error", message, autoCloseMsec); }, warning: (message: string, autoCloseMsec?: number): void => { add("warning", message, autoCloseMsec); }, info: (message: string, autoCloseMsec?: number): void => { add("info", message, autoCloseMsec); }, dismiss, }; }, [add, dismiss]); return ( {children} ); } /** * Internal context for accessing the notification queue (for rendering). * This is separate from the public context to keep the public API clean. */ interface NotificationContextValue { notifications: Notification[]; service: NotificationService; } const NotificationContextInternal = createContext(null); /** * Hook to access the notification service. * Use this in any component to show success, error, warning, or info messages. * * @returns NotificationService API for showing messages. * @throws Error if used outside NotificationProvider. * * @example * ```tsx * function MyComponent() { * const notification = useNotification(); * * const handleSave = async () => { * try { * await saveData(); * notification.success("Data saved successfully"); * } catch (err) { * notification.error("Failed to save data"); * } * }; * * return ; * } * ``` */ export function useNotification(): NotificationService { const context = useContext(NotificationContext); if (!context) { throw new Error("useNotification must be used within NotificationProvider"); } return context; } /** * Internal hook to access the notifications queue for rendering. * Only used by NotificationContainer. */ export function useNotificationQueue(): Notification[] { const context = useContext(NotificationContextInternal); if (!context) { throw new Error("NotificationContextInternal must be used within NotificationProvider"); } return context.notifications; }