'use client'; import Link from 'next/link'; import { Bell } from 'lucide-react'; import { toast } from 'sonner'; import { useConvexAuth, useMutation, useQuery } from 'convex/react'; import { api } from '@spoon/backend/convex/_generated/api.js'; import type { Id } from '@spoon/backend/convex/_generated/dataModel.js'; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@spoon/ui'; const RELATIVE_UNITS: [Intl.RelativeTimeFormatUnit, number][] = [ ['year', 365 * 24 * 60 * 60 * 1000], ['month', 30 * 24 * 60 * 60 * 1000], ['day', 24 * 60 * 60 * 1000], ['hour', 60 * 60 * 1000], ['minute', 60 * 1000], ]; const relativeFormatter = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', }); const formatRelativeTime = (timestamp: number): string => { const delta = timestamp - Date.now(); const magnitude = Math.abs(delta); for (const [unit, ms] of RELATIVE_UNITS) { if (magnitude >= ms) { return relativeFormatter.format(Math.round(delta / ms), unit); } } return 'just now'; }; export const NotificationBell = () => { const { isAuthenticated } = useConvexAuth(); const unreadCount = useQuery( api.notifications.unreadCount, isAuthenticated ? {} : 'skip', ); const notifications = useQuery( api.notifications.listMine, isAuthenticated ? { limit: 10 } : 'skip', ); const markRead = useMutation(api.notifications.markRead); const markAllRead = useMutation(api.notifications.markAllRead); if (!isAuthenticated) { return null; } const count = unreadCount ?? 0; const badgeLabel = count > 9 ? '9+' : String(count); const items = notifications ?? []; // Fire-and-forget: never block navigation on the read write, but surface a // toast if it fails so we don't silently assume success. const handleMarkRead = async (notificationId: Id<'notifications'>) => { try { await markRead({ notificationId }); } catch { toast.error('Could not mark the notification as read.'); } }; const handleMarkAllRead = async () => { try { await markAllRead({}); } catch { toast.error('Could not mark notifications as read.'); } }; return (
Notifications {count > 0 && ( )}
{items.length === 0 ? (

No notifications yet.

) : ( items.map((notification) => ( void handleMarkRead(notification._id)} className='flex w-full cursor-pointer flex-col items-start gap-0.5' > {notification.title} {notification.readAt === undefined && ( )} {notification.body} {formatRelativeTime(notification.createdAt)} )) )}
); };