"use client"; import { useState, useEffect, useCallback } from "react"; import { Bell, CheckCheck, Film, MessageSquare, CheckCircle2, XCircle, AlertCircle, UserPlus, Clock, CornerDownRight, X, Trash2, } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { formatRelativeDate } from "@/lib/utils"; import { cn } from "@/lib/utils"; import Link from "next/link"; interface Notification { id: string; type: string; title: string; message: string; data: Record | null; isRead: boolean; createdAt: string; } const TYPE_CONFIG: Record< string, { icon: React.ElementType; color: string; bg: string } > = { VERSION_UPLOADED: { icon: Film, color: "text-blue-400", bg: "bg-blue-500/10" }, FEEDBACK_ADDED: { icon: MessageSquare, color: "text-amber-400", bg: "bg-amber-500/10" }, SHOT_APPROVED: { icon: CheckCircle2, color: "text-emerald-400", bg: "bg-emerald-500/10" }, SHOT_REJECTED: { icon: XCircle, color: "text-red-400", bg: "bg-red-500/10" }, REVISION_REQUESTED: { icon: AlertCircle, color: "text-orange-400", bg: "bg-orange-500/10" }, COMMENT_REPLY: { icon: CornerDownRight, color: "text-zinc-400", bg: "bg-zinc-500/10" }, MENTION: { icon: MessageSquare, color: "text-sky-400", bg: "bg-sky-500/10" }, TASK_ASSIGNED: { icon: UserPlus, color: "text-sky-400", bg: "bg-sky-500/10" }, TASK_READY_FOR_REVIEW: { icon: Clock, color: "text-purple-400", bg: "bg-purple-500/10" }, TASK_APPROVED: { icon: CheckCircle2, color: "text-emerald-400", bg: "bg-emerald-500/10" }, TASK_CHANGES_REQUESTED: { icon: AlertCircle, color: "text-orange-400", bg: "bg-orange-500/10" }, TASK_OVERDUE: { icon: AlertCircle, color: "text-red-400", bg: "bg-red-500/10" }, }; function getNotificationHref(n: Notification): string | null { const data = n.data as Record | null; if (data?.versionId) return `/review/${data.versionId}`; if (data?.taskId) return `/tasks?highlight=${data.taskId}`; return null; } export function NotificationBell() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [isLoading, setIsLoading] = useState(false); const [open, setOpen] = useState(false); const fetchNotifications = useCallback(async () => { setIsLoading(true); try { const res = await fetch("/api/notifications"); if (res.ok) { const data = await res.json(); setNotifications(data.notifications ?? []); setUnreadCount(data.unreadCount ?? 0); } } catch { // silent } finally { setIsLoading(false); } }, []); // Initial load + poll every 30s (skip while panel is open to avoid flicker) useEffect(() => { fetchNotifications(); const interval = setInterval(() => { if (!open) fetchNotifications(); }, 30_000); return () => clearInterval(interval); }, [fetchNotifications, open]); // Refresh full list whenever the panel opens useEffect(() => { if (open) fetchNotifications(); }, [open, fetchNotifications]); const markAllRead = async () => { try { await fetch("/api/notifications", { method: "PATCH" }); setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); setUnreadCount(0); } catch { // silent } }; const markOneRead = useCallback( async (id: string) => { const n = notifications.find((n) => n.id === id); if (!n || n.isRead) return; try { await fetch(`/api/notifications/${id}`, { method: "PATCH" }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, isRead: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { // silent } }, [notifications] ); const dismiss = async (e: React.MouseEvent, id: string) => { e.preventDefault(); e.stopPropagation(); const wasUnread = notifications.find((n) => n.id === id)?.isRead === false; try { await fetch(`/api/notifications/${id}`, { method: "DELETE" }); setNotifications((prev) => prev.filter((n) => n.id !== id)); if (wasUnread) setUnreadCount((prev) => Math.max(0, prev - 1)); } catch { // silent } }; const clearAll = async () => { try { await fetch("/api/notifications", { method: "DELETE" }); setNotifications([]); setUnreadCount(0); } catch { // silent } }; return ( {/* Header */}
Notifications {unreadCount > 0 && ( {unreadCount} )}
{unreadCount > 0 && ( )}
{/* Notification list */} {isLoading && notifications.length === 0 ? (
) : notifications.length === 0 ? (

You’re all caught up

) : (
{notifications.map((n) => { const href = getNotificationHref(n); const cfg = TYPE_CONFIG[n.type] ?? { icon: Bell, color: "text-zinc-400", bg: "bg-zinc-500/10", }; const Icon = cfg.icon; const inner = (
{/* Type icon */}
{/* Message + time */}

{n.message}

{formatRelativeDate(new Date(n.createdAt))}

{/* Unread dot */} {!n.isRead && (
)} {/* Dismiss button */}
); if (href) { return ( markOneRead(n.id)} > {inner} ); } return (
markOneRead(n.id)} className="cursor-default" > {inner}
); })}
)} {/* Footer */} {notifications.length > 0 && (
)} ); }