Spaces:
Sleeping
Sleeping
BRIDGE Platform Agent
feat(platform): BRIDGE Modules hub with /modules/e and /modules/s routed workspaces
1955a0f Download src/components/TopBar.tsx from sitcod3/BRIDGE_Modul-S_prod: direct link, hf CLI and curl.
- Browser
- Download file 14.7 kB
-
https://huggingface.co/spaces/sitcod3/BRIDGE_Modul-S_prod/resolve/main/src/components/TopBar.tsx
- Command line
-
hf download hf://spaces/sitcod3/BRIDGE_Modul-S_prod/src/components/TopBar.tsx
-
curl -L -o TopBar.tsx https://huggingface.co/spaces/sitcod3/BRIDGE_Modul-S_prod/resolve/main/src/components/TopBar.tsx
14.7 kB
| import { useState, useEffect } from "react"; | |
| import { | |
| KeyRound, | |
| CheckCircle2, | |
| XCircle, | |
| Menu, | |
| Search, | |
| Sparkles, | |
| X, | |
| ArrowRight, | |
| Megaphone, | |
| Users, | |
| Mail, | |
| Inbox, | |
| ShieldCheck, | |
| LayoutDashboard, | |
| CreditCard, | |
| Settings, | |
| Webhook, | |
| BarChart2, | |
| Building2, | |
| Flame | |
| } from "lucide-react"; | |
| import { useApiKey } from "../context/ApiKeyContext"; | |
| import { useTheme } from "../context/ThemeContext"; | |
| import { Link, useNavigate } from "react-router-dom"; | |
| import { useQuery } from "@tanstack/react-query"; | |
| import { apiClient } from "../lib/apiClient"; | |
| import { MODULES, ModuleId, moduleHref } from "../modules/registry"; | |
| interface SearchItem { | |
| title: string; | |
| category: string; | |
| link: string; | |
| icon: any; | |
| shortcut?: string; | |
| detail?: string; | |
| } | |
| export default function TopBar({ onMenuClick, module = "e" }: { onMenuClick?: () => void; module?: ModuleId }) { | |
| const { apiKeys, activeKeyId, setActiveKeyId, activeKey } = useApiKey(); | |
| const moduleDef = MODULES[module]; | |
| const base = moduleDef.basePath; | |
| const { config } = useTheme(); | |
| const [isSearchOpen, setIsSearchOpen] = useState(false); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const navigate = useNavigate(); | |
| // Fetch real data for dynamic search | |
| const { data: campaigns = [] } = useQuery({ | |
| queryKey: ["campaigns"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/campaigns"); | |
| return res.data || []; | |
| }, | |
| enabled: isSearchOpen, | |
| }); | |
| const { data: leads = [] } = useQuery({ | |
| queryKey: ["leads-search-topbar"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/leads/search"); | |
| return res.data || []; | |
| }, | |
| enabled: isSearchOpen, | |
| }); | |
| const { data: emailAccounts = [] } = useQuery({ | |
| queryKey: ["email-accounts-topbar"], | |
| queryFn: async () => { | |
| const res = await apiClient.get("/email-accounts"); | |
| return res.data || []; | |
| }, | |
| enabled: isSearchOpen, | |
| }); | |
| // Keyboard shortcut handlers: Cmd/Ctrl+K, Cmd/Ctrl+Shift+D (Dashboard), Cmd/Ctrl+Shift+I (Inbox), etc. | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| const isCmdOrCtrl = e.metaKey || e.ctrlKey; | |
| if (isCmdOrCtrl && e.key.toLowerCase() === "k") { | |
| e.preventDefault(); | |
| setIsSearchOpen((prev) => !prev); | |
| return; | |
| } | |
| if (e.key === "Escape") { | |
| setIsSearchOpen(false); | |
| return; | |
| } | |
| // Quick navigation shortcuts when not typing in an input | |
| const targetTag = (e.target as HTMLElement)?.tagName?.toLowerCase(); | |
| const isInput = targetTag === "input" || targetTag === "textarea" || targetTag === "select"; | |
| if (!isInput) { | |
| if (isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === "d") { | |
| e.preventDefault(); | |
| navigate(base); | |
| } else if (isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === "i") { | |
| e.preventDefault(); | |
| navigate(moduleHref(module, "inbox")); | |
| } else if (isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === "l") { | |
| e.preventDefault(); | |
| navigate(moduleHref(module, "leads")); | |
| } else if (isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === "c") { | |
| e.preventDefault(); | |
| navigate(moduleHref(module, "campaigns")); | |
| } | |
| } | |
| }; | |
| window.addEventListener("keydown", handleKeyDown); | |
| return () => window.removeEventListener("keydown", handleKeyDown); | |
| }, [navigate, base, module]); | |
| // Static navigation routes | |
| const navItems: SearchItem[] = [ | |
| { title: `${moduleDef.label} Dashboard`, category: "Core Navigation", link: base, icon: LayoutDashboard, shortcut: "⌘⇧D" }, | |
| { title: "Unified Inbox", category: "Core Navigation", link: moduleHref(module, "inbox"), icon: Inbox, shortcut: "⌘⇧I" }, | |
| { title: "Lead Intelligence", category: "Core Navigation", link: moduleHref(module, "leads"), icon: Users, shortcut: "⌘⇧L" }, | |
| { title: "Outbound Campaigns", category: "Core Navigation", link: moduleHref(module, "campaigns"), icon: Megaphone, shortcut: "⌘⇧C" }, | |
| { title: "Email Accounts & Domains", category: "Core Navigation", link: moduleHref(module, "email-accounts"), icon: Mail }, | |
| { title: "Autonomous Warmup Radar", category: "Core Navigation", link: moduleHref(module, "warmup"), icon: Flame }, | |
| { title: "Deliverability Analytics", category: "Core Navigation", link: moduleHref(module, "analytics"), icon: BarChart2 }, | |
| { title: "Agency Clients", category: "Core Navigation", link: moduleHref(module, "clients"), icon: Building2 }, | |
| { title: "Credits & Billing", category: "Core Navigation", link: moduleHref(module, "pricing"), icon: CreditCard }, | |
| { title: "Webhook Dispatcher", category: "Core Navigation", link: moduleHref(module, "webhooks"), icon: Webhook }, | |
| { title: "System Settings", category: "Core Navigation", link: moduleHref(module, "settings"), icon: Settings }, | |
| ]; | |
| // Dynamic items from backend APIs | |
| const realCampaignItems: SearchItem[] = (Array.isArray(campaigns) ? campaigns : []).map((c: any) => ({ | |
| title: c.name || "Campaign", | |
| category: "Campaigns", | |
| link: moduleHref(module, `campaigns/${c.id}`), | |
| icon: Megaphone, | |
| detail: `${c.lead_count || 0} leads • ${c.sent_count || 0} sent` | |
| })); | |
| const realLeadItems: SearchItem[] = (Array.isArray(leads) ? leads : []).slice(0, 8).map((l: any) => ({ | |
| title: `${l.first_name || ""} ${l.last_name || ""}`.trim() || l.email, | |
| category: "Leads", | |
| link: moduleHref(module, "leads"), | |
| icon: Users, | |
| detail: `${l.company_name || ""} • ${l.email || ""}` | |
| })); | |
| const realInboxItems: SearchItem[] = (Array.isArray(emailAccounts) ? emailAccounts : []).map((e: any) => ({ | |
| title: e.email || "Email Account", | |
| category: "Inboxes", | |
| link: moduleHref(module, "email-accounts"), | |
| icon: Mail, | |
| detail: `Status: ${e.status || "active"}` | |
| })); | |
| const allSearchItems: SearchItem[] = [ | |
| ...navItems, | |
| ...realCampaignItems, | |
| ...realLeadItems, | |
| ...realInboxItems, | |
| ]; | |
| const filteredItems = searchQuery | |
| ? allSearchItems.filter( | |
| (item) => | |
| item.title.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| item.category.toLowerCase().includes(searchQuery.toLowerCase()) || | |
| (item.detail && item.detail.toLowerCase().includes(searchQuery.toLowerCase())) | |
| ) | |
| : allSearchItems; | |
| return ( | |
| <header className="h-16 border-b border-white/5 bg-zinc-950/40 backdrop-blur-xl flex items-center justify-between px-4 md:px-6 shrink-0 relative z-30 shadow-[0_4px_24px_-4px_rgba(0,0,0,0.5)]"> | |
| <div className="flex items-center gap-4"> | |
| {onMenuClick && ( | |
| <button | |
| onClick={onMenuClick} | |
| className="p-2 -ml-2 text-zinc-400 hover:text-zinc-200 md:hidden" | |
| aria-label="Open navigation menu" | |
| > | |
| <Menu className="w-5 h-5" /> | |
| </button> | |
| )} | |
| <div className="hidden sm:flex items-center gap-2 text-xs font-mono"> | |
| <span className="text-zinc-500">SYSTEM</span> | |
| <span className="text-zinc-700">/</span> | |
| <span className="text-zinc-300 font-semibold tracking-wide flex items-center gap-1.5"> | |
| <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse shadow-[0_0_8px_var(--theme-accent,#00e5bc)]" /> | |
| {moduleDef.engineLabel} | |
| </span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2 md:gap-3"> | |
| {/* Global Search Button */} | |
| <button | |
| onClick={() => setIsSearchOpen(true)} | |
| className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/5 bg-zinc-900/50 hover:bg-zinc-800/80 text-zinc-400 hover:text-white transition-all text-xs group" | |
| aria-label="Global search (Ctrl+K)" | |
| > | |
| <Search className="w-3.5 h-3.5 text-zinc-500 group-hover:text-white transition-colors" /> | |
| <span className="hidden md:inline text-[11px]">Quick Search...</span> | |
| <kbd className="hidden md:inline text-[9px] font-mono px-1.5 py-0.5 rounded bg-black/40 border border-white/10 text-zinc-400"> | |
| ⌘K | |
| </kbd> | |
| </button> | |
| {/* Reputation Shield Indicator */} | |
| <div className="hidden md:flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-[10px] font-bold uppercase tracking-wider text-emerald-400"> | |
| <ShieldCheck className="w-3.5 h-3.5" /> | |
| <span>Reputation 98.4%</span> | |
| </div> | |
| {/* API Status Pill */} | |
| <div className="flex items-center gap-2"> | |
| {activeKey ? ( | |
| <div | |
| className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-full border shadow-sm" | |
| style={{ | |
| backgroundColor: config.badgeBg, | |
| borderColor: config.badgeBorder, | |
| color: config.badgeText, | |
| }} | |
| > | |
| <CheckCircle2 className="w-3 h-3" /> | |
| API Connected | |
| </div> | |
| ) : ( | |
| <div className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-rose-300 bg-rose-500/10 border border-rose-500/20 px-2.5 py-1 rounded-full shadow-[0_0_10px_rgba(255,107,107,0.3)]"> | |
| <XCircle className="w-3 h-3" /> | |
| Key Missing | |
| </div> | |
| )} | |
| </div> | |
| {/* User Profile Avatar */} | |
| <div | |
| className="w-8 h-8 flex items-center justify-center rounded-lg border text-[10px] font-bold font-mono transition-transform hover:scale-105" | |
| style={{ | |
| backgroundColor: config.badgeBg, | |
| borderColor: config.badgeBorder, | |
| color: config.accent, | |
| }} | |
| > | |
| JM | |
| </div> | |
| <div className="h-5 w-px bg-white/10 mx-1 hidden sm:block" /> | |
| {/* Key switcher */} | |
| <div className="hidden sm:flex items-center gap-1.5"> | |
| <KeyRound className="w-3.5 h-3.5" style={{ color: config.accent }} /> | |
| <select | |
| value={activeKeyId || ""} | |
| onChange={(e) => setActiveKeyId(e.target.value)} | |
| className="bg-white/5 border border-white/10 rounded-lg py-1.5 pl-2 pr-6 text-xs text-zinc-200 focus:outline-none w-28 md:w-36 transition-all hover:bg-white/10 font-mono" | |
| style={{ | |
| borderColor: activeKey ? config.badgeBorder : undefined | |
| }} | |
| > | |
| <option value="" disabled className="bg-zinc-950">Select Key...</option> | |
| {apiKeys.map((key) => ( | |
| <option key={key.id} value={key.id} className="bg-zinc-950"> | |
| {key.name} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| {/* Global Quick Search Modal */} | |
| {isSearchOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black/70 backdrop-blur-md z-50 flex items-start justify-center pt-20 p-4 animate-fadeIn" | |
| onClick={() => setIsSearchOpen(false)} | |
| > | |
| <div | |
| className="bg-zinc-900 border border-white/10 rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden animate-slideDown" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <div className="flex items-center px-4 py-3.5 border-b border-white/10 gap-3"> | |
| <Search className="w-4 h-4 text-zinc-400" /> | |
| <input | |
| autoFocus | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| placeholder="Search campaigns, prospects, mailboxes, routes..." | |
| className="w-full bg-transparent text-sm text-white placeholder:text-zinc-500 focus:outline-none" | |
| /> | |
| {searchQuery && ( | |
| <button onClick={() => setSearchQuery("")} className="text-zinc-500 hover:text-white"> | |
| <X className="w-4 h-4" /> | |
| </button> | |
| )} | |
| <kbd className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-black/40 border border-white/10 text-zinc-400"> | |
| ESC | |
| </kbd> | |
| </div> | |
| <div className="max-h-80 overflow-y-auto p-2 divide-y divide-white/5"> | |
| {filteredItems.length === 0 ? ( | |
| <div className="p-8 text-center text-xs text-zinc-500"> | |
| No matching telemetry found for "{searchQuery}" | |
| </div> | |
| ) : ( | |
| filteredItems.map((item, idx) => ( | |
| <button | |
| key={idx} | |
| onClick={() => { | |
| setIsSearchOpen(false); | |
| navigate(item.link); | |
| }} | |
| className="w-full flex items-center justify-between p-3 rounded-xl hover:bg-white/5 text-left group transition-all" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <div | |
| className="p-2 rounded-lg bg-black/40 border border-white/5 text-zinc-400 group-hover:text-emerald-400 group-hover:border-emerald-500/30 transition-all" | |
| > | |
| <item.icon className="w-4 h-4" /> | |
| </div> | |
| <div> | |
| <div className="text-xs font-bold text-white group-hover:text-emerald-400 transition-colors flex items-center gap-2"> | |
| <span>{item.title}</span> | |
| {item.shortcut && ( | |
| <kbd className="text-[9px] font-mono px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 font-bold"> | |
| {item.shortcut} | |
| </kbd> | |
| )} | |
| </div> | |
| <div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2 mt-0.5"> | |
| <span className="text-zinc-400 font-semibold">{item.category}</span> | |
| {item.detail && ( | |
| <> | |
| <span>•</span> | |
| <span>{item.detail}</span> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <ArrowRight className="w-3.5 h-3.5 text-zinc-600 group-hover:text-white group-hover:translate-x-1 transition-all" /> | |
| </button> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </header> | |
| ); | |
| } | |