import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; import { useTheme } from "../../context/ThemeContext"; import { useWorkspaceContext } from "../../context/WorkspaceContext"; import { listActivity } from "../../api/activity"; import "./Topbar.css"; const NOTIF_ICONS = { workflow_completed: "โœ…", review_requested: "๐Ÿ‘๏ธ", workflow_started: "โ–ถ๏ธ", proposal_approved: "โœ“", proposal_rejected: "โœ—", document_uploaded: "๐Ÿ“„", commit_created: "๐Ÿ’พ", proposal_created: "๐Ÿ“", workflow_failed: "โŒ", }; export function Topbar() { const { user, logout } = useAuth(); const { darkMode, toggleTheme } = useTheme(); const { workspace, workspaces, loading, selectWorkspace, addWorkspace } = useWorkspaceContext(); const navigate = useNavigate(); const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false); const [creating, setCreating] = useState(false); const [newName, setNewName] = useState(""); const [searchQuery, setSearchQuery] = useState(""); // Notifications state const [notifOpen, setNotifOpen] = useState(false); const [notifications, setNotifications] = useState([]); const [notifLoading, setNotifLoading] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const lastReadRef = useRef(localStorage.getItem("dw_notif_read_at") || ""); const notifRef = useRef(null); const pollRef = useRef(null); const displayName = user?.username || user?.email || "Workspace user"; // Fetch notifications const fetchNotifications = useCallback(async () => { if (!workspace) return; try { const response = await listActivity(workspace.id, 8); const events = response?.events ?? (Array.isArray(response) ? response : []); if (Array.isArray(events)) { setNotifications(events); // Count events newer than last time user opened the bell const readAt = lastReadRef.current; if (readAt) { const readTime = new Date(readAt).getTime(); const unread = events.filter( (e) => e.timestamp && new Date(e.timestamp).getTime() > readTime ); setUnreadCount(unread.length); } else { // Never opened before โ€” show all as unread setUnreadCount(events.length); } } } catch { // silently handle } }, [workspace]); // Poll every 15 seconds for new notifications useEffect(() => { fetchNotifications(); pollRef.current = setInterval(fetchNotifications, 15000); return () => clearInterval(pollRef.current); }, [fetchNotifications]); // Close notif dropdown on outside click useEffect(() => { const handleClick = (e) => { if (notifRef.current && !notifRef.current.contains(e.target)) { setNotifOpen(false); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, []); const handleOpenNotifs = () => { setNotifOpen((v) => !v); // Mark all current notifications as read const now = new Date().toISOString(); lastReadRef.current = now; localStorage.setItem("dw_notif_read_at", now); setUnreadCount(0); }; const handleLogout = () => { logout(); navigate("/login"); }; const handleSelect = (id) => { selectWorkspace(id); setWorkspaceMenuOpen(false); }; const handleCreate = async (e) => { e.preventDefault(); const name = newName.trim(); if (!name) return; await addWorkspace(name); setNewName(""); setCreating(false); setWorkspaceMenuOpen(false); }; const timeAgo = (timestamp) => { if (!timestamp) return ""; const diff = Date.now() - new Date(timestamp).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; }; return (
{workspaceMenuOpen && (
{ setWorkspaceMenuOpen(false); setCreating(false); }} > {workspaces.map((w) => ( ))}
{creating && (
setNewName(e.target.value)} />
)}
)}
{/* New workspace button */}
setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && searchQuery.trim()) { navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`); setSearchQuery(""); } }} />
{/* Notification Bell */}
{notifOpen && (
Notifications setNotifOpen(false)} > View all
{notifications.length === 0 ? (
No recent activity.
) : (
{notifications.map((n) => { const targetUrl = n.metadata?.workflow_id ? `/workflows/${n.metadata.workflow_id}` : "/activity"; return ( setNotifOpen(false)} > {NOTIF_ICONS[n.type] || "โ€ข"} {n.message} {timeAgo(n.timestamp)} ); })}
)}
)}
); }