// In-App Notification Center - Bell icon with dropdown panel "use client"; import Link from "next/link"; import { useState, useEffect, useRef, useCallback } from "react"; interface Notification { id: string; title: string; message: string; type: "info" | "success" | "warning" | "error"; read: boolean; created_at: string; link?: string; } interface NotificationCenterProps { brandId?: string; userId?: string; } export default function NotificationCenter({ brandId, userId }: NotificationCenterProps) { const [isOpen, setIsOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const dropdownRef = useRef(null); const unreadCount = notifications.filter((n) => !n.read).length; // Define fetchNotifications before useEffect const fetchNotifications = useCallback(async () => { setLoading(true); try { const res = await fetch(`/api/notifications?brand_id=${brandId}`); if (res.ok) { const data = await res.json(); setNotifications(data.notifications || []); } } catch (error) { console.error("Failed to fetch notifications:", error); } finally { setLoading(false); } }, [brandId]); // Fetch notifications when opened — handled in the click handler so // we don't need a useEffect to watch `isOpen` and re-fetch on every // open transition. See handleToggleOpen below. // Close on click outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const markAsRead = async (id: string) => { try { await fetch("/api/notifications", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, read: true }), }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ); } catch (error) { console.error("Failed to mark as read:", error); } }; const markAllAsRead = async () => { try { await fetch("/api/notifications/mark-all-read", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ brand_id: brandId }), }); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); } catch (error) { console.error("Failed to mark all as read:", error); } }; const formatTime = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diff = now.getTime() - date.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return "Just now"; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; return `${days}d ago`; }; const getTypeStyles = (type: string) => { switch (type) { case "success": return "bg-emerald-100 text-emerald-600"; case "warning": return "bg-amber-100 text-amber-600"; case "error": return "bg-red-100 text-red-600"; default: return "bg-blue-100 text-blue-600"; } }; const handleToggleOpen = useCallback(async () => { const next = !isOpen; setIsOpen(next); if (next && brandId) { await fetchNotifications(); } }, [isOpen, brandId, fetchNotifications]); return (
{/* Bell Icon Button */} {/* Dropdown Panel */} {isOpen && (
{/* Header */}

Notifications

{unreadCount > 0 && ( )}
{/* Notifications List */}
{loading ? (
) : notifications.length === 0 ? (

No notifications yet

) : (
{notifications.map((notification) => (
{ if (e.key === "Enter" || e.key === " ") { if (!notification.read) markAsRead(notification.id); if (notification.link) window.location.href = notification.link; } }} className={`p-4 hover:bg-gray-50 transition-colors cursor-pointer ${ !notification.read ? "bg-emerald-50/50" : "" }`} onClick={() => { if (!notification.read) markAsRead(notification.id); if (notification.link) window.location.href = notification.link; }} >

{notification.title}

{notification.message}

{formatTime(notification.created_at)}

))}
)}
{/* Footer */}
View all notifications
)}
); }