Files
gridpilot.gg/apps/website/components/notifications/NotificationProvider.tsx
2025-12-24 21:44:58 +01:00

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>
);
}