'use client'; import { useEffect, useState } from 'react'; import { Bell } from 'lucide-react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { apiFetch } from '@/lib/api/client'; import { useNotifications } from '@/hooks/use-notifications'; import { NotificationItem } from './notification-item'; interface NotificationListResponse { data: Array<{ id: string; type: string; title: string; description: string | null; link: string | null; isRead: boolean; createdAt: Date; }>; total: number; } export function NotificationBell() { const { unreadCount } = useNotifications(); const queryClient = useQueryClient(); // Track popover open state so we only fire the list fetch when the user // actually opens the bell. Without this, an instance of NotificationBell // mounted alongside would populate the same ['notifications', // 'list'] cache key without the gating Inbox carefully applies, defeating // Inbox's enabled-on-open optimization. const [open, setOpen] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['notifications', 'list'], queryFn: () => apiFetch('/api/v1/notifications?limit=20'), staleTime: 30_000, enabled: open, }); const markReadMutation = useMutation({ mutationFn: (notificationId: string) => apiFetch(`/api/v1/notifications/${notificationId}`, { method: 'PATCH' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const markAllReadMutation = useMutation({ mutationFn: () => apiFetch('/api/v1/notifications/read-all', { method: 'POST' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['notifications'] }); }, }); const notifications = data?.data ?? []; // Auto-mark-as-read on display: when the dropdown opens and lists land, // POST /read-all so the badge clears once the user has actually seen the // items. Individual rows still link out - the auto-clear here is the // "I've seen these" gesture; the per-row mark-read action stays // available for selective dismissal in the inbox page. useEffect(() => { if (!open || isLoading) return; if (notifications.some((n) => !n.isRead)) { markAllReadMutation.mutate(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, isLoading, notifications.length]); return ( {/* Header */}

Notifications

{unreadCount > 0 && ( )}
{/* Notification list */} {isLoading ? (
Loading...
) : notifications.length === 0 ? (
No notifications
) : (
{notifications.map((notification) => ( markReadMutation.mutate(id)} /> ))}
)}
); }