"use client"; import * as React from "react"; import Link from "next/link"; import { Bell, CheckCircle2, AlertCircle, RefreshCw, Mail, Monitor, LayoutGrid, Globe, Clock, CheckCheck, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; type Notification = { id: string; createdAt: string; read: boolean; type: string; title: string; message: string; cardId: string | null; actionUrl: string | null; }; export function NotificationCenter() { const [open, setOpen] = React.useState(false); const [notifications, setNotifications] = React.useState([]); const [unreadCount, setUnreadCount] = React.useState(0); const fetchUnreadCount = React.useCallback(async () => { try { const res = await fetch("/api/notifications?unreadOnly=true&limit=1"); if (res.ok) { const data = await res.json(); setUnreadCount(data.unreadCount ?? 0); } } catch { /* ignore */ } }, []); React.useEffect(() => { fetchUnreadCount(); const interval = setInterval(fetchUnreadCount, 30_000); return () => clearInterval(interval); }, [fetchUnreadCount]); const fetchNotifications = React.useCallback(async () => { try { const res = await fetch("/api/notifications?limit=30"); if (res.ok) { const data = await res.json(); setNotifications(data.notifications ?? []); setUnreadCount(data.unreadCount ?? 0); } } catch { /* ignore */ } }, []); React.useEffect(() => { if (open) fetchNotifications(); }, [open, fetchNotifications]); const markAllRead = async () => { try { await fetch("/api/notifications", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "mark_all_read" }), }); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); } catch { /* ignore */ } }; const markRead = async (id: string) => { try { await fetch("/api/notifications", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "mark_read", id }), }); setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, read: true } : n)); setUnreadCount((c) => Math.max(0, c - 1)); } catch { /* ignore */ } }; return ( } > {unreadCount > 0 && ( {unreadCount > 9 ? "9+" : unreadCount} )} Notifications
Notifications {unreadCount > 0 && ( )}
{notifications.length === 0 ? (

No notifications

) : ( notifications.map((n) => ( setOpen(false)} /> )) )}
); } function NotificationItem({ notification: n, onRead, onClose, }: { notification: Notification; onRead: (id: string) => void; onClose: () => void; }) { const iconMap: Record = { ocr_complete: { icon: , color: "text-emerald-600 bg-emerald-500/10" }, ocr_error: { icon: , color: "text-red-600 bg-red-500/10" }, card_needs_review: { icon: , color: "text-amber-600 bg-amber-500/10" }, monday_sync: { icon: , color: "text-blue-600 bg-blue-500/10" }, monday_error: { icon: , color: "text-red-600 bg-red-500/10" }, webhook_error: { icon: , color: "text-red-600 bg-red-500/10" }, email_watcher: { icon: , color: "text-purple-600 bg-purple-500/10" }, system: { icon: , color: "text-muted-foreground bg-muted" }, }; const { icon, color } = iconMap[n.type] ?? iconMap.system; const handleClick = () => { if (!n.read) onRead(n.id); if (n.actionUrl) onClose(); }; const content = (
{icon}
{n.title} {!n.read && }

{n.message}

{formatTimeAgo(n.createdAt)}
); if (n.actionUrl) { return {content}; } return content; } function formatTimeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; return new Date(dateStr).toLocaleDateString(); }