'use client' import { useCallback, useEffect, useRef, useState } from 'react' import Link from 'next/link' type Notification = { id: string title: string | null link: string | null sent_at: string read_at: string | null } const POLL_MS = 60_000 export function NotificationBell() { const [notifications, setNotifications] = useState([]) const [unread, setUnread] = useState(0) const [open, setOpen] = useState(false) const panelRef = useRef(null) const load = useCallback(() => { fetch('/api/notifications') .then(r => (r.ok ? r.json() : Promise.reject())) .then((data: { notifications: Notification[]; unread: number }) => { setNotifications(data.notifications) setUnread(data.unread) }) .catch(() => {}) }, []) useEffect(() => { load() const id = setInterval(load, POLL_MS) return () => clearInterval(id) }, [load]) useEffect(() => { if (!open) return const onClick = (e: MouseEvent) => { if (panelRef.current && !panelRef.current.contains(e.target as Node)) setOpen(false) } document.addEventListener('mousedown', onClick) return () => document.removeEventListener('mousedown', onClick) }, [open]) const markAllRead = () => { fetch('/api/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }), }) .then(() => { setUnread(0) setNotifications(prev => prev.map(n => ({ ...n, read_at: n.read_at ?? new Date().toISOString() }))) }) .catch(() => {}) } return (
{open && (
Notifications {unread > 0 && ( )}
{notifications.length === 0 ? (

No notifications

) : (
    {notifications.map(n => { const inner = ( <>

    {n.title ?? 'Notification'}

    {new Date(n.sent_at).toLocaleString()}

    ) return (
  • {n.link ? ( setOpen(false)}> {inner} ) : (
    {inner}
    )}
  • ) })}
)}
)}
) }