192 lines
6.2 KiB
TypeScript
192 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
|
|
|
import type { Notification } from '@core/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>;
|
|
dismissModal: (notification: Notification) => 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
|
|
// TODO
|
|
// 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) => {
|
|
// // Check both unread and action_required status for modals
|
|
// const shouldDisplay = (notification.isUnread() || notification.isActionRequired()) &&
|
|
// !seenNotificationIds.has(notification.id);
|
|
|
|
// if (shouldDisplay) {
|
|
// // 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]);
|
|
|
|
// Prevent body scroll when modal is open
|
|
useEffect(() => {
|
|
if (modalNotification) {
|
|
document.body.style.overflow = 'hidden';
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
}
|
|
}, [modalNotification]);
|
|
|
|
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 dismissModal = useCallback(async (notification: Notification) => {
|
|
try {
|
|
// Dismiss the notification
|
|
const repo = getNotificationRepository();
|
|
const updated = notification.dismiss();
|
|
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 dismiss notification:', error);
|
|
}
|
|
}, []);
|
|
|
|
const unreadCount = notifications.filter((n) => n.isUnread() || n.isActionRequired()).length;
|
|
|
|
const value: NotificationContextValue = {
|
|
notifications,
|
|
unreadCount,
|
|
toastNotifications,
|
|
modalNotification,
|
|
markAsRead,
|
|
dismissToast,
|
|
respondToModal,
|
|
dismissModal,
|
|
};
|
|
|
|
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}
|
|
onDismiss={dismissModal}
|
|
/>
|
|
)}
|
|
</NotificationContext.Provider>
|
|
);
|
|
} |