"use client"; import { HEALTH_URL, topbarNotificationsApi, type CommitHistoryNotificationRecord, } from "@/lib/api"; import { useCurrentUser } from "@/lib/auth-state"; import { navigation } from "@/lib/navigation"; import { addPreferencesListener, getStoredTheme, resolveThemePreference, setStoredTheme, } from "@/lib/preferences"; import { hasMinimumRole } from "@/lib/permissions"; import { Bell, LoaderCircle, Menu, Moon, RefreshCw, Search, SunMedium, Wifi, WifiOff, } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; type ConnectionState = "checking" | "online" | "offline" | "degraded"; const TOPBAR_COMMIT_READ_KEY = "doktainer-topbar-read-commit-notifications"; const MAX_TOPBAR_NOTIFICATIONS = 5; function readStoredCommitNotificationIds() { if (typeof window === "undefined") { return new Set(); } try { const stored = window.localStorage.getItem(TOPBAR_COMMIT_READ_KEY); if (!stored) { return new Set(); } const parsed = JSON.parse(stored) as string[]; return new Set(parsed.filter(Boolean)); } catch { window.localStorage.removeItem(TOPBAR_COMMIT_READ_KEY); return new Set(); } } interface TopbarProps { title: string; subtitle?: string; onMobileMenuToggle?: () => void; } export default function Topbar({ title, subtitle, onMobileMenuToggle, }: TopbarProps) { const currentUser = useCurrentUser(); const pathname = usePathname(); const router = useRouter(); const [time, setTime] = useState(""); const [notificationNow, setNotificationNow] = useState(0); const [connectionState, setConnectionState] = useState("checking"); const [query, setQuery] = useState(""); const [searchOpen, setSearchOpen] = useState(false); const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">("dark"); const [notifications, setNotifications] = useState< CommitHistoryNotificationRecord[] >([]); const [notificationsLoading, setNotificationsLoading] = useState(true); const [notificationsError, setNotificationsError] = useState( null, ); const [readNotificationIds, setReadNotificationIds] = useState>( readStoredCommitNotificationIds, ); const [notificationOpen, setNotificationOpen] = useState(false); const searchRef = useRef(null); const searchShellRef = useRef(null); const notificationShellRef = useRef(null); const markNotificationsRead = useCallback(() => { setReadNotificationIds((current) => { const next = new Set(current); notifications.forEach((notification) => next.add(notification.id)); window.localStorage.setItem( TOPBAR_COMMIT_READ_KEY, JSON.stringify([...next].slice(0, 100)), ); return next; }); }, [notifications]); const loadNotifications = useCallback(async () => { setNotificationsLoading(true); setNotificationsError(null); try { const response = await topbarNotificationsApi.listCommits(); setNotifications(response.data.slice(0, MAX_TOPBAR_NOTIFICATIONS)); } catch (error) { setNotifications([]); setNotificationsError( error instanceof Error ? error.message : "Failed to load commit history", ); } finally { setNotificationsLoading(false); } }, []); useEffect(() => { const timeout = window.setTimeout(() => { void loadNotifications(); }, 0); return () => window.clearTimeout(timeout); }, [loadNotifications]); useEffect(() => { const update = () => { const now = new Date(); setNotificationNow(now.getTime()); setTime( now.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", }), ); }; update(); const interval = setInterval(update, 1000); return () => clearInterval(interval); }, []); useEffect(() => { const syncTheme = () => { setResolvedTheme(resolveThemePreference(getStoredTheme())); }; syncTheme(); const media = window.matchMedia("(prefers-color-scheme: light)"); const onThemeMediaChange = () => syncTheme(); media.addEventListener("change", onThemeMediaChange); const removePreferencesListener = addPreferencesListener(syncTheme); return () => { media.removeEventListener("change", onThemeMediaChange); removePreferencesListener(); }; }, []); const visibleRoutes = useMemo( () => navigation .flatMap((section) => section.items.map((item) => ({ ...item, sectionLabel: section.label, })), ) .filter((item) => hasMinimumRole(currentUser?.role, item.minRole)), [currentUser?.role], ); const filteredRoutes = useMemo(() => { const normalized = query.trim().toLowerCase(); if (!normalized) { return visibleRoutes.slice(0, 8); } return visibleRoutes .filter((item) => { const haystack = [ item.label, item.href, item.sectionLabel, ...(item.keywords ?? []), ] .join(" ") .toLowerCase(); return haystack.includes(normalized); }) .slice(0, 8); }, [query, visibleRoutes]); const checkConnection = useCallback(async () => { if (typeof navigator !== "undefined" && !navigator.onLine) { setConnectionState("offline"); return; } setConnectionState("checking"); const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), 5000); try { const response = await fetch(HEALTH_URL, { method: "GET", cache: "no-store", signal: controller.signal, }); setConnectionState(response.ok ? "online" : "degraded"); } catch { setConnectionState("degraded"); } finally { window.clearTimeout(timeoutId); } }, []); useEffect(() => { const initialCheck = window.setTimeout(() => { void checkConnection(); }, 0); const handleOnline = () => { void checkConnection(); }; const handleOffline = () => { setConnectionState("offline"); }; const interval = window.setInterval(() => { void checkConnection(); }, 30000); window.addEventListener("online", handleOnline); window.addEventListener("offline", handleOffline); return () => { window.clearTimeout(initialCheck); window.clearInterval(interval); window.removeEventListener("online", handleOnline); window.removeEventListener("offline", handleOffline); }; }, [checkConnection]); useEffect(() => { const onPointerDown = (event: MouseEvent) => { const target = event.target as Node; if (!searchShellRef.current?.contains(target)) { setSearchOpen(false); } if (!notificationShellRef.current?.contains(target)) { setNotificationOpen(false); } }; const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setSearchOpen(false); setNotificationOpen(false); return; } const shortcutPressed = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k"; if (!shortcutPressed) return; event.preventDefault(); setNotificationOpen(false); setSearchOpen(true); window.setTimeout(() => { searchRef.current?.focus(); searchRef.current?.select(); }, 0); }; document.addEventListener("pointerdown", onPointerDown); window.addEventListener("keydown", onKeyDown); return () => { document.removeEventListener("pointerdown", onPointerDown); window.removeEventListener("keydown", onKeyDown); }; }, []); useEffect(() => { const timeout = window.setTimeout(() => { setQuery(""); setSearchOpen(false); setNotificationOpen(false); }, 0); return () => window.clearTimeout(timeout); }, [pathname]); useEffect(() => { if (!notificationOpen) { return; } const timeout = window.setTimeout(markNotificationsRead, 0); return () => window.clearTimeout(timeout); }, [markNotificationsRead, notificationOpen]); const shortcutLabel = useMemo(() => { if (typeof navigator !== "undefined" && /mac/i.test(navigator.platform)) { return ["Cmd", "K"] as const; } return ["Ctrl", "K"] as const; }, []); const connectionMeta = { checking: { label: "Checking panel connection", color: "#f59e0b", background: "rgba(245, 158, 11, 0.1)", border: "rgba(245, 158, 11, 0.3)", icon: , }, online: { label: "Browser online and API reachable", color: "#10b981", background: "rgba(16, 185, 129, 0.1)", border: "rgba(16, 185, 129, 0.3)", icon: , }, offline: { label: "Browser offline. Check your network.", color: "#ef4444", background: "rgba(239, 68, 68, 0.1)", border: "rgba(239, 68, 68, 0.3)", icon: , }, degraded: { label: "Browser online, but backend API is not responding", color: "#f59e0b", background: "rgba(245, 158, 11, 0.1)", border: "rgba(245, 158, 11, 0.3)", icon: , }, }[connectionState]; const handleNavigate = (href: string) => { setSearchOpen(false); setNotificationOpen(false); setQuery(""); router.push(href); }; const handleThemeToggle = () => { const nextTheme = resolvedTheme === "light" ? "dark" : "light"; setStoredTheme(nextTheme); }; const unreadCount = useMemo( () => notifications.filter( (notification) => !readNotificationIds.has(notification.id), ).length, [notifications, readNotificationIds], ); const notificationToneMeta = { dot: "#3b82f6", border: "rgba(59, 130, 246, 0.2)", background: "rgba(59, 130, 246, 0.08)", }; const formatNotificationTimestamp = (timestamp: string) => { const timeMs = new Date(timestamp).getTime(); if (!Number.isFinite(timeMs)) { return ""; } const diff = notificationNow - timeMs; if (diff < 60_000) { return "Just now"; } if (diff < 3_600_000) { return `${Math.floor(diff / 60_000)}m ago`; } if (diff < 86_400_000) { return `${Math.floor(diff / 3_600_000)}h ago`; } return new Date(timeMs).toLocaleDateString("en-US", { month: "short", day: "numeric", }); }; const handleRefresh = () => { window.location.reload(); }; return (
{/* Left: hamburger (mobile) + title */}
{onMobileMenuToggle && ( )}

{title}

{subtitle && (

{subtitle}

)}
{/* Right side */}
{/* Search */}
{ setQuery(event.target.value); setSearchOpen(true); }} onFocus={() => setSearchOpen(true)} onKeyDown={(event) => { if (event.key === "Enter" && filteredRoutes[0]) { event.preventDefault(); handleNavigate(filteredRoutes[0].href); } if (event.key === "Escape") { event.preventDefault(); setSearchOpen(false); searchRef.current?.blur(); } }} placeholder="Search pages..." style={{ background: "transparent", border: "none", outline: "none", color: "var(--text-primary)", fontSize: 13, width: "100%", }} /> {!query && (
{shortcutLabel.map((label) => ( {label} ))}
)} {searchOpen && (
{query ? `Showing ${filteredRoutes.length} result${filteredRoutes.length === 1 ? "" : "s"}` : "Quick navigation"}
{filteredRoutes.length > 0 ? ( filteredRoutes.map((item) => { const Icon = item.icon; const active = pathname === item.href; return ( ); }) ) : (
No matching pages.
)}
)}
{/* Live clock */}
{time}
{/* Connection status */} {/* Refresh */} {/* Notifications */}
{notificationOpen && (
Notifications
Latest {notifications.length} commits
{notificationsLoading && (
Loading commit history...
)} {!notificationsLoading && notificationsError && (
{notificationsError}
)} {!notificationsLoading && !notificationsError && notifications.length === 0 && (
No recent commits found.
)} {!notificationsLoading && !notificationsError && notifications.map((notification) => { const isRead = readNotificationIds.has(notification.id); const toneMeta = notificationToneMeta; return (
{notification.title} {formatNotificationTimestamp( notification.committedAt, )}

{notification.message}

); })}
)}
); }