import { ActionIcon, Badge, Box, Button, Drawer, Group, Loader, ScrollArea, Stack, Text, ThemeIcon, } from "@mantine/core"; import { Bell, CheckCheck, Inbox, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { NotificationItem } from "./NotificationItem"; import { edr } from "./PriorityTag"; import type { NotificationItemData, ResolveNotificationVisual, } from "./types"; export interface NotificationDrawerProps { opened: boolean; onClose: () => void; /** Unread items (newest first), already flattened across pages. */ unread: NotificationItemData[]; /** Read items (newest first), already flattened across pages. */ read: NotificationItemData[]; unreadCount?: number; /** Initial load spinner (before any page resolves). */ loading?: boolean; hasMoreUnread?: boolean; hasMoreRead?: boolean; loadingMoreUnread?: boolean; loadingMoreRead?: boolean; onLoadMoreUnread?: () => void; onLoadMoreRead?: () => void; onItemClick?: (item: NotificationItemData) => void; onMarkAllRead?: () => void; /** App-owned registry: `item` → icon + color. */ resolveVisual?: ResolveNotificationVisual; emptyLabel?: string; title?: string; width?: number; } /** Fires `onVisible` when it scrolls into view within `root`. Infinite-scroll trigger. */ function Sentinel({ root, onVisible, }: { root: HTMLElement | null; onVisible: () => void; }) { const ref = useRef(null); const cb = useRef(onVisible); cb.current = onVisible; useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver( (entries) => { if (entries.some((e) => e.isIntersecting)) cb.current(); }, { root, rootMargin: "160px" }, ); obs.observe(el); return () => obs.disconnect(); }, [root]); return
; } function SectionLabel({ label, count, }: { label: string; count?: number; }) { return ( {label} {typeof count === "number" && count > 0 && ( {count} )} ); } function LoadingRow() { return ( ); } /** * Right slide-in notification center, styled to the EDR design system: a * branded header, an "Unread" section over an "Earlier" (read) section, each * with its own infinite-scroll trigger in one scroll surface. Presentational — * the host wires data, paging, and the visual registry. */ export function NotificationDrawer({ opened, onClose, unread, read, unreadCount, loading = false, hasMoreUnread = false, hasMoreRead = false, loadingMoreUnread = false, loadingMoreRead = false, onLoadMoreUnread, onLoadMoreRead, onItemClick, onMarkAllRead, resolveVisual, emptyLabel = "You're all caught up", title = "Notifications", width = 424, }: NotificationDrawerProps) { const [viewport, setViewport] = useState(null); const viewportRef = useRef(null); // Capture the scroll viewport so the sentinels observe within it, not the page. useEffect(() => { if (opened) setViewport(viewportRef.current); }, [opened]); const totalUnread = unreadCount ?? unread.length; const isEmpty = !loading && unread.length === 0 && read.length === 0; const renderItem = (item: NotificationItemData) => ( ); return ( {title} {totalUnread > 0 ? `${totalUnread} unread` : "All caught up"} {totalUnread > 0 && ( {totalUnread > 99 ? "99+" : totalUnread} )} {totalUnread > 0 && onMarkAllRead && ( )} {loading && unread.length === 0 && read.length === 0 ? ( ) : isEmpty ? ( {emptyLabel} New notifications about your bookings, clearances and invoices will show up here. ) : ( {unread.length > 0 && ( <> {unread.map(renderItem)} {hasMoreUnread && ( onLoadMoreUnread?.()} /> )} {loadingMoreUnread && } )} {read.length > 0 && ( <> {read.map(renderItem)} {hasMoreRead && ( onLoadMoreRead?.()} /> )} {loadingMoreRead && } )} )} ); } export default NotificationDrawer;