From b74a0e2d361be8c08f86a7d2bdf98b4a820c8ad6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 16:37:28 +0000 Subject: [PATCH] Wire up TopBar search across pages, integrations, and bookmarks Search now filters static nav pages, integrations, and bookmarks live as you type, with a results dropdown, Enter-to-navigate, and click-outside-to-close. Browser-verified end-to-end. --- src/components/TopBar.tsx | 115 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 91dc397..2543fb2 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,7 +1,8 @@ -import { useState, useRef, useEffect } from 'react' -import { useLocation } from 'react-router-dom' -import { Search, Bell, ChevronDown, User, Palette, LogOut, Shield, HelpCircle } from 'lucide-react' +import { useState, useRef, useEffect, useMemo } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { Search, Bell, ChevronDown, User, Palette, LogOut, Shield, HelpCircle, Server, Bookmark as BookmarkIcon, LayoutGrid } from 'lucide-react' import { useAuth } from '../lib/AuthContext' +import { api } from '../lib/api' const pageTitles: Record = { '/': 'Glance', @@ -15,14 +16,75 @@ const pageSubtitles: Record = { '/booknest': 'Your Digital Library', } +const staticPages: { name: string; path: string }[] = [ + { name: 'Glance', path: '/' }, + { name: 'Infrastructure', path: '/infrastructure' }, + { name: 'BookNest', path: '/booknest' }, + { name: 'Terminal', path: '/terminal' }, + { name: 'Tunnels', path: '/tunnels' }, + { name: 'Files', path: '/files' }, + { name: 'Containers', path: '/containers' }, + { name: 'Remote Desktop', path: '/remote-desktop' }, + { name: 'Host Metrics', path: '/host-metrics' }, + { name: 'Settings', path: '/settings' }, +] + +type SearchResult = + | { kind: 'page'; key: string; label: string; sublabel?: string; path: string } + | { kind: 'integration'; key: string; label: string; sublabel?: string; path: string } + | { kind: 'bookmark'; key: string; label: string; sublabel?: string; path: string } + export default function TopBar() { const { logout, user } = useAuth() const [userMenuOpen, setUserMenuOpen] = useState(false) const menuRef = useRef(null) const location = useLocation() + const navigate = useNavigate() const title = pageTitles[location.pathname] ?? 'Glance' const subtitle = pageSubtitles[location.pathname] + const [query, setQuery] = useState('') + const [searchOpen, setSearchOpen] = useState(false) + const [integrations, setIntegrations] = useState<{ id: number; name: string; type: string }[]>([]) + const [bookmarks, setBookmarks] = useState<{ id: number; title: string; url: string }[]>([]) + const searchRef = useRef(null) + + useEffect(() => { + api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)).catch(() => {}) + api.listBookmarks().then(({ bookmarks }) => setBookmarks(bookmarks)).catch(() => {}) + }, []) + + useEffect(() => { + function handleClick(e: MouseEvent) { + if (searchRef.current && !searchRef.current.contains(e.target as Node)) { + setSearchOpen(false) + } + } + document.addEventListener('mousedown', handleClick) + return () => document.removeEventListener('mousedown', handleClick) + }, []) + + const results = useMemo(() => { + const q = query.trim().toLowerCase() + if (!q) return [] + const pageMatches: SearchResult[] = staticPages + .filter((p) => p.name.toLowerCase().includes(q)) + .map((p) => ({ kind: 'page', key: `page-${p.path}`, label: p.name, path: p.path })) + const integrationMatches: SearchResult[] = integrations + .filter((i) => i.name.toLowerCase().includes(q) || i.type.toLowerCase().includes(q)) + .map((i) => ({ kind: 'integration', key: `int-${i.id}`, label: i.name, sublabel: i.type, path: '/infrastructure' })) + const bookmarkMatches: SearchResult[] = bookmarks + .filter((b) => b.title.toLowerCase().includes(q) || b.url.toLowerCase().includes(q)) + .map((b) => ({ kind: 'bookmark', key: `bm-${b.id}`, label: b.title, sublabel: b.url, path: '/booknest' })) + return [...pageMatches, ...integrationMatches, ...bookmarkMatches].slice(0, 8) + }, [query, integrations, bookmarks]) + + function handleSelectResult(r: SearchResult) { + navigate(r.path) + setQuery('') + setSearchOpen(false) + } + const displayName = user?.display_name || user?.username || '' const initials = displayName .split(/\s+/) @@ -60,14 +122,59 @@ export default function TopBar() { {/* Center section — Search bar */}
-
+
{ + setQuery(e.target.value) + setSearchOpen(true) + }} + onFocus={() => setSearchOpen(true)} + onKeyDown={(e) => { + if (e.key === 'Enter' && results.length > 0) { + handleSelectResult(results[0]) + } else if (e.key === 'Escape') { + setSearchOpen(false) + } + }} className="w-[300px] h-8 rounded-full border border-border text-[12px] text-text-primary placeholder:text-text-secondary focus:outline-none focus:border-gold transition-colors" style={{ paddingLeft: '36px', paddingRight: '16px', backgroundColor: 'rgba(255,255,255,0.04)', backdropFilter: 'blur(6px)' }} /> + + {searchOpen && query.trim() && ( +
+ {results.length === 0 ? ( +
+ No matches for "{query}" +
+ ) : ( +
+ {results.map((r) => ( + + ))} +
+ )} +
+ )}