'use client'; import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { v4 as uuid } from 'uuid'; import ModalNotification from './ModalNotification'; import ToastNotification from './ToastNotification'; export type NotificationVariant = 'toast' | 'modal' | 'center'; export interface NotificationAction { id: string; label: string; type?: 'primary' | 'secondary' | 'danger'; href?: string; } export interface Notification { id: string; type: string; title?: string; message: string; createdAt: Date; variant: NotificationVariant; actionUrl?: string; requiresResponse?: boolean; actions?: NotificationAction[]; data?: Record; read: boolean; } interface AddNotificationInput { id?: string; type: string; title?: string; message: string; createdAt?: Date; variant?: NotificationVariant; actionUrl?: string; requiresResponse?: boolean; actions?: NotificationAction[]; data?: Record; } interface NotificationContextValue { notifications: Notification[]; unreadCount: number; addNotification: (input: AddNotificationInput) => string; dismissNotification: (id: string) => void; clearNotifications: () => void; markAsRead: (id: string) => void; markAllAsRead: () => void; } const NotificationContext = createContext(null); export function useNotifications() { const context = useContext(NotificationContext); if (!context) { throw new Error('useNotifications must be used within NotificationProvider'); } return context; } interface NotificationProviderProps { children: ReactNode; } export default function NotificationProvider({ children }: NotificationProviderProps) { const [notifications, setNotifications] = useState([]); const addNotification = useCallback((input: AddNotificationInput): string => { const id = input.id ?? uuid(); const notification: Notification = { id, type: input.type, title: input.title, message: input.message, createdAt: input.createdAt ?? new Date(), variant: input.variant ?? 'toast', actionUrl: input.actionUrl, requiresResponse: input.requiresResponse, actions: input.actions, data: input.data, read: false, }; setNotifications((prev) => [notification, ...prev]); return id; }, []); const dismissNotification = useCallback((id: string) => { setNotifications((prev) => prev.filter((notification) => notification.id !== id)); }, []); const clearNotifications = useCallback(() => { setNotifications([]); }, []); const markAsRead = useCallback((id: string) => { setNotifications((prev) => prev.map((notification) => notification.id === id ? { ...notification, read: true } : notification, ), ); }, []); const markAllAsRead = useCallback(() => { setNotifications((prev) => prev.map((notification) => ({ ...notification, read: true }))); }, []); const unreadCount = useMemo( () => notifications.filter((notification) => !notification.read).length, [notifications], ); const modalNotification = useMemo( () => notifications.find((notification) => notification.variant === 'modal' && !notification.read) ?? null, [notifications], ); const toastNotifications = useMemo( () => notifications.filter((notification) => notification.variant === 'toast' && !notification.read), [notifications], ); useEffect(() => { if (!modalNotification) { return; } const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = previousOverflow; }; }, [modalNotification]); const value: NotificationContextValue = { notifications, unreadCount, addNotification, dismissNotification, clearNotifications, markAsRead, markAllAsRead, }; return ( {children} {/* Toast notifications container */}
{toastNotifications.map((notification) => ( dismissNotification(notification.id)} onRead={() => markAsRead(notification.id)} /> ))}
{/* Modal notification */} {modalNotification && ( { // For now we just mark as read and optionally navigate via ModalNotification markAsRead(notification.id); if (actionId === 'dismiss') { dismissNotification(notification.id); } }} onDismiss={(notification) => { markAsRead(notification.id); dismissNotification(notification.id); }} /> )}
); }