167 lines
4.7 KiB
TypeScript
167 lines
4.7 KiB
TypeScript
'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';
|
|
|
|
import type { Notification, NotificationAction, NotificationVariant } from './notificationTypes';
|
|
|
|
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;
|
|
addNotification: (input: AddNotificationInput) => string;
|
|
dismissNotification: (id: string) => void;
|
|
clearNotifications: () => void;
|
|
markAsRead: (id: string) => void;
|
|
markAllAsRead: () => 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 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 (
|
|
<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={() => dismissNotification(notification.id)}
|
|
onRead={() => markAsRead(notification.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Modal notification */}
|
|
{modalNotification && (
|
|
<ModalNotification
|
|
notification={modalNotification}
|
|
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>
|
|
);
|
|
}
|