Files
gridpilot.gg/apps/website/components/notifications/NotificationProvider.tsx
2025-12-09 22:45:03 +01:00

158 lines
5.2 KiB
TypeScript

'use client';
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import {
getNotificationRepository,
getMarkNotificationReadUseCase,
} from '@/lib/di-container';
import type { Notification } from '@gridpilot/notifications/application';
import ToastNotification from './ToastNotification';
import ModalNotification from './ModalNotification';
interface NotificationContextValue {
notifications: Notification[];
unreadCount: number;
toastNotifications: Notification[];
modalNotification: Notification | null;
markAsRead: (notification: Notification) => Promise<void>;
dismissToast: (notification: Notification) => void;
respondToModal: (notification: Notification, actionId?: string) => Promise<void>;
}
const NotificationContext = createContext<NotificationContextValue | null>(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<Notification[]>([]);
const [toastNotifications, setToastNotifications] = useState<Notification[]>([]);
const [modalNotification, setModalNotification] = useState<Notification | null>(null);
const [seenNotificationIds, setSeenNotificationIds] = useState<Set<string>>(new Set());
const currentDriverId = useEffectiveDriverId();
// Poll for new notifications
useEffect(() => {
const loadNotifications = async () => {
try {
const repo = getNotificationRepository();
const allNotifications = await repo.findByRecipientId(currentDriverId);
setNotifications(allNotifications);
// Check for new notifications that need toast/modal display
allNotifications.forEach((notification) => {
if (notification.isUnread() && !seenNotificationIds.has(notification.id)) {
// Mark as seen to prevent duplicate displays
setSeenNotificationIds((prev) => new Set([...prev, notification.id]));
// Handle based on urgency
if (notification.isModal()) {
// Modal takes priority - show immediately
setModalNotification(notification);
} else if (notification.isToast()) {
// Add to toast queue
setToastNotifications((prev) => [...prev, notification]);
}
// Silent notifications just appear in the notification center
}
});
} catch (error) {
console.error('Failed to load notifications:', error);
}
};
loadNotifications();
// Poll every 2 seconds for responsiveness
const interval = setInterval(loadNotifications, 2000);
return () => clearInterval(interval);
}, [currentDriverId, seenNotificationIds]);
const markAsRead = useCallback(async (notification: Notification) => {
try {
const markRead = getMarkNotificationReadUseCase();
await markRead.execute({
notificationId: notification.id,
recipientId: currentDriverId,
});
setNotifications((prev) =>
prev.map((n) => (n.id === notification.id ? n.markAsRead() : n))
);
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}, [currentDriverId]);
const dismissToast = useCallback((notification: Notification) => {
setToastNotifications((prev) => prev.filter((n) => n.id !== notification.id));
}, []);
const respondToModal = useCallback(async (notification: Notification, actionId?: string) => {
try {
// Mark as responded
const repo = getNotificationRepository();
const updated = notification.markAsResponded(actionId);
await repo.update(updated);
// Update local state
setNotifications((prev) =>
prev.map((n) => (n.id === notification.id ? updated : n))
);
// Clear modal
setModalNotification(null);
} catch (error) {
console.error('Failed to respond to notification:', error);
}
}, []);
const unreadCount = notifications.filter((n) => n.isUnread() || n.isActionRequired()).length;
const value: NotificationContextValue = {
notifications,
unreadCount,
toastNotifications,
modalNotification,
markAsRead,
dismissToast,
respondToModal,
};
return (
<NotificationContext.Provider value={value}>
{children}
{/* Toast notifications container */}
<div className="fixed top-20 right-4 z-50 space-y-3">
{toastNotifications.map((notification) => (
<ToastNotification
key={notification.id}
notification={notification}
onDismiss={dismissToast}
onRead={markAsRead}
/>
))}
</div>
{/* Modal notification */}
{modalNotification && (
<ModalNotification
notification={modalNotification}
onAction={respondToModal}
/>
)}
</NotificationContext.Provider>
);
}