BRIDGE Platform Agent
feat(platform): BRIDGE Modules hub with /modules/e and /modules/s routed workspaces
1955a0f
Raw History Blame Contribute Delete
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>
);
}