resolve todos in website and api
This commit is contained in:
@@ -1,21 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { Notification } from '@core/notifications/application';
|
||||
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<string, unknown>;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
interface AddNotificationInput {
|
||||
id?: string;
|
||||
type: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
createdAt?: Date;
|
||||
variant?: NotificationVariant;
|
||||
actionUrl?: string;
|
||||
requiresResponse?: boolean;
|
||||
actions?: NotificationAction[];
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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>;
|
||||
addNotification: (input: AddNotificationInput) => string;
|
||||
dismissNotification: (id: string) => void;
|
||||
clearNotifications: () => void;
|
||||
markAsRead: (id: string) => void;
|
||||
markAllAsRead: () => void;
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextValue | null>(null);
|
||||
@@ -34,133 +68,85 @@ interface NotificationProviderProps {
|
||||
|
||||
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);
|
||||
const addNotification = useCallback((input: AddNotificationInput): string => {
|
||||
const id = input.id ?? uuid();
|
||||
|
||||
// // 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]));
|
||||
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,
|
||||
};
|
||||
|
||||
// // 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);
|
||||
// }
|
||||
// };
|
||||
setNotifications((prev) => [notification, ...prev]);
|
||||
|
||||
// loadNotifications();
|
||||
|
||||
// // Poll every 2 seconds for responsiveness
|
||||
// const interval = setInterval(loadNotifications, 2000);
|
||||
// return () => clearInterval(interval);
|
||||
// }, [currentDriverId, seenNotificationIds]);
|
||||
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],
|
||||
);
|
||||
|
||||
// Prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (modalNotification) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
if (!modalNotification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow;
|
||||
};
|
||||
}, [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,
|
||||
addNotification,
|
||||
dismissNotification,
|
||||
clearNotifications,
|
||||
markAsRead,
|
||||
dismissToast,
|
||||
respondToModal,
|
||||
dismissModal,
|
||||
markAllAsRead,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -173,8 +159,8 @@ export default function NotificationProvider({ children }: NotificationProviderP
|
||||
<ToastNotification
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
onDismiss={dismissToast}
|
||||
onRead={markAsRead}
|
||||
onDismiss={() => dismissNotification(notification.id)}
|
||||
onRead={() => markAsRead(notification.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -183,8 +169,17 @@ export default function NotificationProvider({ children }: NotificationProviderP
|
||||
{modalNotification && (
|
||||
<ModalNotification
|
||||
notification={modalNotification}
|
||||
onAction={respondToModal}
|
||||
onDismiss={dismissModal}
|
||||
onAction={(notification, actionId) => {
|
||||
// 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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NotificationContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user