import React from 'react'; import { useUploadQueue } from '../queue/hooks'; import type { QueueItem } from '../queue/queue'; import { dismissGuestNotification, fetchGuestNotifications, markGuestNotificationRead, type GuestNotificationItem, } from '../services/notificationApi'; export type NotificationCenterValue = { notifications: GuestNotificationItem[]; unreadCount: number; queueItems: QueueItem[]; queueCount: number; totalCount: number; loading: boolean; refresh: () => Promise; setFilters: (filters: { status?: 'new' | 'read' | 'dismissed' | 'all'; scope?: 'all' | 'uploads' | 'tips' | 'general' }) => void; markAsRead: (id: number) => Promise; dismiss: (id: number) => Promise; eventToken: string; lastFetchedAt: Date | null; isOffline: boolean; }; const NotificationCenterContext = React.createContext(null); export function NotificationCenterProvider({ eventToken, children }: { eventToken: string; children: React.ReactNode }) { const { items, loading: queueLoading, refresh: refreshQueue } = useUploadQueue(); const [notifications, setNotifications] = React.useState([]); const [unreadCount, setUnreadCount] = React.useState(0); const [loadingNotifications, setLoadingNotifications] = React.useState(true); const [filters, setFiltersState] = React.useState<{ status?: 'new' | 'read' | 'dismissed' | 'all'; scope?: 'all' | 'uploads' | 'tips' | 'general' }>({ status: 'new', scope: 'all', }); const etagRef = React.useRef(null); const fetchLockRef = React.useRef(false); const [lastFetchedAt, setLastFetchedAt] = React.useState(null); const [isOffline, setIsOffline] = React.useState(typeof navigator !== 'undefined' ? !navigator.onLine : false); const queueCount = React.useMemo( () => items.filter((item) => item.status !== 'done').length, [items] ); const loadNotifications = React.useCallback( async (options: { silent?: boolean } = {}) => { if (!eventToken) { if (!options.silent) { setLoadingNotifications(false); } return; } if (fetchLockRef.current) { return; } fetchLockRef.current = true; if (!options.silent) { setLoadingNotifications(true); } try { const statusFilter = filters.status && filters.status !== 'all' ? (filters.status === 'new' ? 'unread' : filters.status) : undefined; const result = await fetchGuestNotifications(eventToken, etagRef.current, { status: statusFilter as any, scope: filters.scope, }); if (!result.notModified) { setNotifications(result.notifications); setUnreadCount(result.unreadCount); setLastFetchedAt(new Date()); } etagRef.current = result.etag; setIsOffline(false); } catch (error) { console.error('Failed to load guest notifications', error); if (!options.silent) { setNotifications([]); setUnreadCount(0); } if (typeof navigator !== 'undefined' && !navigator.onLine) { setIsOffline(true); } } finally { fetchLockRef.current = false; if (!options.silent) { setLoadingNotifications(false); } } }, [eventToken] ); React.useEffect(() => { setNotifications([]); setUnreadCount(0); etagRef.current = null; if (!eventToken) { setLoadingNotifications(false); return; } setLoadingNotifications(true); void loadNotifications(); }, [eventToken, loadNotifications]); React.useEffect(() => { if (!eventToken) { return; } const interval = window.setInterval(() => { void loadNotifications({ silent: true }); }, 90000); return () => window.clearInterval(interval); }, [eventToken, loadNotifications]); React.useEffect(() => { const handleOnline = () => setIsOffline(false); const handleOffline = () => setIsOffline(true); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); React.useEffect(() => { const handler = (event: MessageEvent) => { if (event.data?.type === 'guest-notification-refresh') { void loadNotifications({ silent: true }); } }; navigator.serviceWorker?.addEventListener('message', handler); return () => { navigator.serviceWorker?.removeEventListener('message', handler); }; }, [loadNotifications]); const markAsRead = React.useCallback( async (id: number) => { if (!eventToken) { return; } let decremented = false; setNotifications((prev) => prev.map((item) => { if (item.id !== id) { return item; } if (item.status === 'new') { decremented = true; } return { ...item, status: 'read', readAt: new Date().toISOString(), }; }) ); if (decremented) { setUnreadCount((prev) => Math.max(0, prev - 1)); } try { await markGuestNotificationRead(eventToken, id); } catch (error) { console.error('Failed to mark notification as read', error); void loadNotifications({ silent: true }); } }, [eventToken, loadNotifications] ); const dismiss = React.useCallback( async (id: number) => { if (!eventToken) { return; } let decremented = false; setNotifications((prev) => prev.map((item) => { if (item.id !== id) { return item; } if (item.status === 'new') { decremented = true; } return { ...item, status: 'dismissed', dismissedAt: new Date().toISOString(), }; }) ); if (decremented) { setUnreadCount((prev) => Math.max(0, prev - 1)); } try { await dismissGuestNotification(eventToken, id); } catch (error) { console.error('Failed to dismiss notification', error); void loadNotifications({ silent: true }); } }, [eventToken, loadNotifications] ); const setFilters = React.useCallback((next: { status?: 'new' | 'read' | 'dismissed' | 'all'; scope?: 'all' | 'uploads' | 'tips' | 'general' }) => { setFiltersState((prev) => ({ ...prev, ...next })); void loadNotifications({ silent: true }); }, [loadNotifications]); const refresh = React.useCallback(async () => { await Promise.all([loadNotifications(), refreshQueue()]); }, [loadNotifications, refreshQueue]); const loading = loadingNotifications || queueLoading; const totalCount = unreadCount + queueCount; const value: NotificationCenterValue = { notifications, unreadCount, queueItems: items, queueCount, totalCount, loading, refresh, setFilters, markAsRead, dismiss, eventToken, lastFetchedAt, isOffline, }; return ( {children} ); } export function useNotificationCenter(): NotificationCenterValue { const ctx = React.useContext(NotificationCenterContext); if (!ctx) { throw new Error('useNotificationCenter must be used within NotificationCenterProvider'); } return ctx; } export function useOptionalNotificationCenter(): NotificationCenterValue | null { return React.useContext(NotificationCenterContext); }