"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { BellIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import { notificationLabel } from "@/lib/notifications"; import { markAllNotificationsRead, markNotificationsRead, } from "@/app/(frontend)/notifications/actions"; interface NotificationItem { id: number; type?: string | null; title: string; message?: string | null; link?: string | null; read: boolean; createdAt: string; } function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } const POLL_MS = 30_000; export function NotificationsBell() { const router = useRouter(); const [notifications, setNotifications] = useState([]); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const intervalRef = useRef | null>(null); const unreadCount = notifications.filter((n) => !n.read).length; const fetchNotifications = useCallback(async () => { try { const res = await fetch("/api/user-notifications?limit=12&sort=-createdAt", { cache: "no-store", }); if (!res.ok) throw new Error("Failed to fetch"); const data = (await res.json()) as { docs?: NotificationItem[] }; setNotifications(data.docs ?? []); setError(false); } catch { setError(true); } finally { setLoading(false); } }, []); useEffect(() => { void fetchNotifications(); intervalRef.current = setInterval(() => void fetchNotifications(), POLL_MS); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchNotifications]); async function handleItemClick(item: NotificationItem) { if (!item.read) { await markNotificationsRead([item.id]); setNotifications((prev) => prev.map((n) => (n.id === item.id ? { ...n, read: true } : n))); } if (item.link) { router.push(item.link); setOpen(false); } } async function handleMarkAll() { if (unreadCount === 0) return; await markAllNotificationsRead(); setNotifications((prev) => prev.map((n) => (n.read ? n : { ...n, read: true }))); } return (
Notifications {unreadCount > 0 && ( )}
{loading ? (

Loading...

) : error ? (

Could not load notifications.

) : notifications.length === 0 ? (

No notifications yet.

) : ( notifications.map((item) => ( void handleItemClick(item)} > {!item.read && ( )} {item.title} {timeAgo(item.createdAt)} {item.message && ( {item.message} )} {notificationLabel(item.type)} )) )}
View all
); }