'use client' import { useEffect, useState } from 'react' import { signOut } from 'next-auth/react' import { usePathname } from 'next/navigation' import Link from 'next/link' import { Search, Bell, Plus, Menu, LogOut, Sun, Moon } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb' import { useTheme } from 'next-themes' import { useMounted } from '@/components/client-only' import { ACTIVE_PAGE_CHANGED } from '@/lib/pages' // Notion keeps the top bar minimal: a breadcrumb path on the left, a subtle // Quick Find pill, and ghost icon buttons on the right. const SECTION_LABELS: Record = { '/bookmarks': 'All Bookmarks', '/calendar': 'Calendar', '/tags': 'Tags', '/kanban': 'Kanban', '/contacts': 'Contacts', '/settings': 'Settings', '/admin': 'Admin', '/pages': 'Pages', } interface TopBarProps { user: { id: string; name?: string | null; email?: string | null; image?: string | null } alerts: any[] onDismissAlert: (id: string) => void onAddBookmark: () => void onToggleSidebar: () => void onQuickFind: () => void } export function TopBar({ user, alerts, onDismissAlert, onAddBookmark, onToggleSidebar, onQuickFind }: TopBarProps) { const [showAlerts, setShowAlerts] = useState(false) const pathname = usePathname() const mounted = useMounted() const { theme, setTheme } = useTheme() const [activePageTitle, setActivePageTitle] = useState(null) // The pages workspace announces the open page so breadcrumbs can show it. // The title only renders on /pages, so route changes need no reset effect. useEffect(() => { const handler = (e: Event) => setActivePageTitle((e as CustomEvent).detail) window.addEventListener(ACTIVE_PAGE_CHANGED, handler) return () => window.removeEventListener(ACTIVE_PAGE_CHANGED, handler) }, []) const pageTitle = pathname === '/pages' ? activePageTitle : null useEffect(() => { const handler = (e: MouseEvent) => { const el = document.getElementById('topbar-alerts') if (el && !el.contains(e.target as Node)) setShowAlerts(false) } document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, []) const section = SECTION_LABELS[pathname] ?? 'computer' const isMac = mounted && typeof navigator !== 'undefined' && /mac|iphone|ipad/i.test(navigator.userAgent) const quickFindHint = !mounted ? '⌘K' : isMac ? '⌘K' : 'Ctrl K' // Review CR-02: local sign-out is always available, but it is NOT the same // claim as confirmed global revocation. Ask the trusted server to revoke // (Go tokenVersion bump) first and report honestly when that fails — other // devices and already-issued assertions then survive until the backend // recovers, and the user is told so instead of being silently logged out. const [signingOut, setSigningOut] = useState(false) const doSignOut = async () => { if (signingOut) return setSigningOut(true) let revoked = false try { const res = await fetch('/api/session/revoke', { method: 'POST' }) const data = await res.json().catch(() => null) revoked = res.ok && data?.revoked === true } catch { /* unreachable Next route — treated as a revocation failure below */ } if (!revoked) { toast.error('Signed out on this device, but global revocation failed — other sessions may remain signed in until the backend recovers.') } void signOut({ redirectTo: '/login' }) } return (
computer {pageTitle ? ( <> Pages {pageTitle} ) : ( {section} )}
{showAlerts && (

Alerts

{(alerts?.length ?? 0) === 0 && (

No pending alerts

)} {(alerts ?? []).map((a: any) => (

{a?.title ?? 'Untitled'}

Alert due

))}
)}
{(user?.name ?? user?.email ?? '?')[0]?.toUpperCase()}
{user?.name ?? user?.email ?? 'User'}
) }