import { useEffect, useMemo, useState } from "react"; import { AlertTriangle, Bell, CheckCircle2, Info, Loader2, Sparkles, XCircle, } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { useAuth } from "@/contexts/AuthContext"; import { aethexNotificationService, aethexRealtimeService, } from "@/lib/aethex-database-adapter"; import { aethexToast } from "@/lib/aethex-toast"; interface AethexNotification { id: string; title: string; message: string | null; type: string | null; created_at: string; read: boolean | null; } const typeIconMap: Record< string, React.ComponentType<{ className?: string }> > = { success: CheckCircle2, warning: AlertTriangle, error: XCircle, info: Info, default: Sparkles, }; const typeAccentMap: Record = { success: "text-emerald-300", warning: "text-amber-300", error: "text-rose-300", info: "text-sky-300", default: "text-aethex-300", }; export default function NotificationBell({ className, }: { className?: string; }) { const { user } = useAuth(); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const [markingAll, setMarkingAll] = useState(false); useEffect(() => { if (!user?.id) { setNotifications([]); return; } let isActive = true; setLoading(true); aethexNotificationService .getUserNotifications(user.id) .then((data) => { if (!isActive) return; console.debug( "[Notifications] Loaded", Array.isArray(data) ? data.length : 0, "notifications", ); setNotifications( Array.isArray(data) ? (data as AethexNotification[]) : [], ); }) .catch((err) => { if (!isActive) return; console.warn("[Notifications] Failed to load:", err); setNotifications([]); }) .finally(() => { if (!isActive) return; setLoading(false); }); const subscription = aethexRealtimeService.subscribeToUserNotifications( user.id, (payload: any) => { if (!isActive) return; const next = (payload?.new ?? payload) as | AethexNotification | undefined; if (!next?.id) return; setNotifications((prev) => { // Check if notification already exists const exists = prev.some((item) => item.id === next.id); if (exists) return prev; // Add new notification to the top and keep up to 50 in the list return [next, ...prev].slice(0, 50); }); aethexToast.aethex({ title: next.title || "New notification", description: next.message ?? undefined, }); }, ); return () => { isActive = false; subscription?.unsubscribe?.(); }; }, [user?.id]); const unreadCount = useMemo( () => notifications.filter((notification) => !notification.read).length, [notifications], ); const markAsRead = async (id: string) => { setNotifications((prev) => prev.map((notification) => notification.id === id ? { ...notification, read: true } : notification, ), ); try { await aethexNotificationService.markAsRead(id); } catch { // Non-blocking; keep optimistic state. } }; const markAllAsRead = async () => { if (!notifications.length) return; setMarkingAll(true); const ids = notifications.filter((n) => !n.read).map((n) => n.id); if (!ids.length) { setMarkingAll(false); return; } setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); try { await Promise.all( ids.map((id) => aethexNotificationService.markAsRead(id)), ); } catch { // Soft fail silently } finally { setMarkingAll(false); } }; const renderNotification = (notification: AethexNotification) => { const typeKey = notification.type?.toLowerCase() ?? "default"; const Icon = typeIconMap[typeKey] ?? typeIconMap.default; const accent = typeAccentMap[typeKey] ?? typeAccentMap.default; return ( { event.preventDefault(); void markAsRead(notification.id); }} className="focus:bg-background/80" >

{notification.title}

{!notification.read && ( New )}
{notification.message ? (

{notification.message}

) : null}

{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true, })}

); }; if (!user?.id) { return null; } return (
{unreadCount > 0 ? ( {unreadCount > 9 ? "9+" : unreadCount} ) : null}
Notifications {unreadCount > 0 ? ( {unreadCount} unread ) : null}
{loading ? (
Loading notifications…
) : notifications.length ? (
{notifications.map((notification) => renderNotification(notification), )}
) : (
No notifications yet. You'll see activity updates here.
)}
); }