import React, { useState, useRef, useEffect, useMemo } from "react";
import { Bell, Search, Menu, X, ArrowRight, MapPin, Calendar, ChevronRight, CreditCard, ChevronDown } from "lucide-react";
import { useBreakpoint } from "../hooks/useBreakpoint";
import { useAuth } from "../contexts/AuthContext";
import { useAppNav } from "../contexts/AppNavContext";
import { organizationApi, subscriptionApi, eventsApi } from "../../services/api";
import type { OrganizationRecord, SubscriptionPlanCatalog } from "../../services/api/types";
import { portfolioListOrganizationId } from "../../services/api/scopedOrganization";
import { ORGANIZATION_SUBSCRIPTION_UPDATED } from "../constants/appEvents";
import {
doSearch,
STATUS_BADGE,
mapApiEventToSearchEvent,
SearchEvent, SearchPage, SearchAction, SearchResults,
} from "./search/searchIndex";
import { EventBuzzBrandLogo } from "./EventBuzzBrandLogo";
/* ─────────────────────────────────────────
Props
───────────────────────────────────────── */
interface TopBarProps {
onMenuClick?: () => void;
onNavigate?: (navId: string) => void;
onEventSelect?: (eventId: string) => void;
onOpenSearchPage?: (query: string) => void;
}
/* ─────────────────────────────────────────
Inline highlight helper
───────────────────────────────────────── */
function Hl({ text, q }: { text: string; q: string }) {
if (!q.trim()) return <>{text}>;
const idx = text.toLowerCase().indexOf(q.toLowerCase());
if (idx === -1) return <>{text}>;
return (
<>
{text.slice(0, idx)}
{text.slice(idx, idx + q.length)}
{text.slice(idx + q.length)}
>
);
}
/* ─────────────────────────────────────────
Dropdown component (shared desktop + mobile)
───────────────────────────────────────── */
function SearchDropdown({
query, results, activeIndex, onEventClick, onPageClick, onActionClick, onViewAll,
}: {
query: string;
results: SearchResults | null;
activeIndex: number;
onEventClick: (e: SearchEvent) => void;
onPageClick: (p: SearchPage) => void;
onActionClick: (a: SearchAction) => void;
onViewAll: () => void;
}) {
const trimmed = query.trim();
/* Flat index offsets (to map activeIndex → each section) */
const eventsShown = results ? Math.min(results.events.length, 4) : 0;
const pagesShown = results ? Math.min(results.pages.length, 5) : 0;
const actionsShown = results ? Math.min(results.actions.length, 3) : 0;
const pageOff = eventsShown;
const actionOff = eventsShown + pagesShown;
/* ── Section label ── */
const SectionLabel = ({ label, count }: { label: string; count: number }) => (
{label}
{count}
);
/* ── Empty query: no menu (avoid static Suggested / Recent Events panel) ── */
if (!trimmed) {
return null;
}
/* ── No results ── */
if (!results || results.total === 0) {
return (
No results for "{trimmed}"
Try a different search term
);
}
/* ── Results ── */
return (
{/* Events */}
{results.events.length > 0 && (
{results.events.slice(0, 4).map((e, i) => {
const badge = STATUS_BADGE[e.status];
const isAct = activeIndex === i;
return (
);
})}
{results.events.length > 4 && (
+{results.events.length - 4} more event{results.events.length - 4 > 1 ? "s" : ""}
)}
)}
{/* Divider between events and pages */}
{results.events.length > 0 && results.pages.length > 0 && (
)}
{/* Pages */}
{results.pages.length > 0 && (
{results.pages.slice(0, 5).map((p, i) => {
const isAct = activeIndex === pageOff + i;
return (
);
})}
)}
{/* Divider before actions */}
{(results.events.length > 0 || results.pages.length > 0) && results.actions.length > 0 && (
)}
{/* Actions */}
{results.actions.length > 0 && (
{results.actions.slice(0, 3).map((a, i) => {
const isAct = activeIndex === actionOff + i;
return (
);
})}
)}
{/* Footer: view all */}
);
}
/* ─────────────────────────────────────────
Logged-in user (top right)
───────────────────────────────────────── */
function userDisplayName(user: { firstName?: string; lastName?: string; email: string } | null): string {
if (!user) return "User";
if (user.firstName || user.lastName) return [user.firstName, user.lastName].filter(Boolean).join(" ").trim();
return user.email?.split("@")[0] ?? "User";
}
function userInitials(user: { firstName?: string; lastName?: string; email: string } | null): string {
const name = userDisplayName(user);
return name.slice(0, 2).toUpperCase() || "?";
}
function formatPlanName(planId: string | undefined, catalog: SubscriptionPlanCatalog[]): string {
if (!planId) return "Free";
const p = catalog.find((x) => x.id === planId);
if (p?.name) return p.name;
return planId
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, (ch) => ch.toUpperCase());
}
/** Org may omit maxParticipants; fall back to catalog plan defaults (e.g. free tier). */
function resolveEffectiveMaxParticipants(
org: OrganizationRecord,
catalog: SubscriptionPlanCatalog[]
): number | null {
const raw = org.subscription?.maxParticipants;
if (raw != null && Number.isFinite(Number(raw))) return Number(raw);
const planId = org.subscription?.plan ?? "free";
const plan = catalog.find((p) => p.id === planId);
const fromCatalog = plan?.defaultLimits?.maxParticipants;
if (fromCatalog != null && Number.isFinite(Number(fromCatalog))) return Number(fromCatalog);
return null;
}
function formatRenewalSuffix(expiresAt: string | undefined): { desktop: string; mobile: string; title: string } {
if (!expiresAt) return { desktop: "", mobile: "", title: "" };
const d = new Date(expiresAt);
if (Number.isNaN(d.getTime())) return { desktop: "", mobile: "", title: "" };
const long = d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" });
const short = d.toLocaleDateString("en-IN", { day: "numeric", month: "short" });
return {
desktop: ` · Renews ${long}`,
mobile: ` · ${short}`,
title: ` Renews on ${long}.`,
};
}
function NotificationsBell() {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(true);
const wrapRef = useRef(null);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, [open]);
return (
{open && (
{expanded && (
)}
)}
);
}
function LoggedInUserButton({ isMobile }: { isMobile: boolean }) {
const { user } = useAuth();
const { setActiveNav } = useAppNav();
const name = userDisplayName(user ?? null);
const initials = userInitials(user ?? null);
return (
);
}
/* ─────────────────────────────────────────
TopBar
───────────────────────────────────────── */
export function TopBar({ onMenuClick, onNavigate, onEventSelect, onOpenSearchPage }: TopBarProps) {
const { user } = useAuth();
const { setActiveNav, setSettingsInitialSection } = useAppNav();
const [query, setQuery] = useState("");
const [dropdownOpen, setDropdownOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [focused, setFocused] = useState(false);
const [orgSummary, setOrgSummary] = useState(null);
const [planCatalog, setPlanCatalog] = useState([]);
/** Bumps when subscription/checkout updates org server-side so we refetch without relying on user._id alone. */
const [orgDataVersion, setOrgDataVersion] = useState(0);
const [orgSearchEvents, setOrgSearchEvents] = useState([]);
const bp = useBreakpoint();
const isMobile = bp === "mobile";
const isTablet = bp === "tablet";
const listOrgId = portfolioListOrganizationId(user);
const searchContainerRef = useRef(null);
const desktopInputRef = useRef(null);
const mobileInputRef = useRef(null);
/* ── Current-org events for search (never cross-tenant demo data) ── */
useEffect(() => {
if (!listOrgId) {
setOrgSearchEvents([]);
return;
}
let cancelled = false;
void (async () => {
const res = await eventsApi.list({
organization: listOrgId,
recordKind: "event",
page: 1,
limit: 200,
});
if (cancelled) return;
if (res.error || !res.data?.events) {
setOrgSearchEvents([]);
return;
}
setOrgSearchEvents(res.data.events.map(mapApiEventToSearchEvent));
})();
return () => {
cancelled = true;
};
}, [listOrgId]);
/* ── Search results ── */
const results: SearchResults | null = useMemo(
() => (query.trim() ? doSearch(query, orgSearchEvents) : null),
[query, orgSearchEvents],
);
/* ── Flat items for keyboard nav ── */
const flatItems = useMemo(() => {
if (!results) return [];
return [
...results.events.slice(0, 4).map(e => ({ type: "event" as const, data: e })),
...results.pages.slice(0, 5).map(p => ({ type: "page" as const, data: p })),
...results.actions.slice(0, 3).map(a => ({ type: "action" as const, data: a })),
];
}, [results]);
/* ── Click-outside to close dropdown ── */
useEffect(() => {
const handler = (e: MouseEvent) => {
if (searchContainerRef.current && !searchContainerRef.current.contains(e.target as Node)) {
setDropdownOpen(false);
setActiveIndex(-1);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
useEffect(() => {
const bump = () => setOrgDataVersion((v) => v + 1);
window.addEventListener(ORGANIZATION_SUBSCRIPTION_UPDATED, bump);
return () => window.removeEventListener(ORGANIZATION_SUBSCRIPTION_UPDATED, bump);
}, []);
/* ── Plan + participant limit (organisation subscription) ── */
useEffect(() => {
if (!user?.organization?._id) {
setOrgSummary(null);
setPlanCatalog([]);
return;
}
let cancelled = false;
void (async () => {
try {
const [org, catalog] = await Promise.all([
organizationApi.get({ cacheBust: orgDataVersion > 0 }),
subscriptionApi.catalog().then((c) => c.plans).catch(() => [] as SubscriptionPlanCatalog[]),
]);
if (!cancelled) {
setOrgSummary(org);
setPlanCatalog(catalog);
}
} catch {
if (!cancelled) {
setOrgSummary(null);
setPlanCatalog([]);
}
}
})();
return () => {
cancelled = true;
};
}, [user?.organization?._id, orgDataVersion]);
const planChip = useMemo(() => {
if (!user?.organization?._id || !orgSummary) return null;
const planId = orgSummary.subscription?.plan;
const name = formatPlanName(planId, planCatalog);
const effectiveMax = resolveEffectiveMaxParticipants(orgSummary, planCatalog);
const part =
effectiveMax == null
? "Unlimited participants"
: `${effectiveMax.toLocaleString("en-IN")} participants max`;
const renew = formatRenewalSuffix(orgSummary.subscription?.expiresAt);
const label = isMobile
? `${name} · ${effectiveMax == null ? "∞" : effectiveMax.toLocaleString("en-IN")}${renew.mobile}`
: `${name} · ${part}${renew.desktop}`;
const title = `${name} — ${part}.${renew.title} Open Billing in Settings.`;
return { label, title };
}, [user?.organization?._id, orgSummary, planCatalog, isMobile]);
/* ── ⌘K / Ctrl+K global shortcut ── */
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
if (isMobile) {
setMobileSearchOpen(true);
setTimeout(() => mobileInputRef.current?.focus(), 50);
} else {
desktopInputRef.current?.focus();
if (query.trim()) setDropdownOpen(true);
}
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [isMobile, query]);
/* ── Keyboard navigation inside input ── */
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveIndex(i => Math.min(i + 1, flatItems.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex(i => Math.max(i - 1, -1));
} else if (e.key === "Enter") {
if (activeIndex >= 0 && flatItems[activeIndex]) {
const item = flatItems[activeIndex];
handleItemSelect(item);
} else if (query.trim()) {
handleViewAll();
}
} else if (e.key === "Escape") {
setDropdownOpen(false);
setActiveIndex(-1);
setMobileSearchOpen(false);
}
};
/* ── Navigate to selected item ── */
const handleItemSelect = (item: typeof flatItems[0]) => {
if (item.type === "event") {
onEventSelect?.((item.data as SearchEvent).id);
} else if (item.type === "page") {
onNavigate?.((item.data as SearchPage).navId);
} else {
onNavigate?.((item.data as SearchAction).navTarget);
}
closeSearch();
};
const handleEventClick = (e: SearchEvent) => { onEventSelect?.(e.id); closeSearch(); };
const handlePageClick = (p: SearchPage) => { onNavigate?.(p.navId); closeSearch(); };
const handleActionClick = (a: SearchAction) => { onNavigate?.(a.navTarget); closeSearch(); };
const handleViewAll = () => { onOpenSearchPage?.(query); closeSearch(); };
const closeSearch = () => {
setDropdownOpen(false);
setActiveIndex(-1);
setMobileSearchOpen(false);
setQuery("");
};
/* ─────────────────────────────────────
Render
───────────────────────────────────── */
return (
<>
{/* ── Mobile search overlay ── */}
{mobileSearchOpen && (
{/* Backdrop */}
setMobileSearchOpen(false)}
style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" }}
/>
{/* Panel */}
{/* Search input bar */}
{ setQuery(e.target.value); setActiveIndex(-1); }}
onKeyDown={handleKeyDown}
style={{ flex: 1, background: "transparent", border: "none", outline: "none", fontSize: "var(--text-base)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)" }}
/>
{query && (
)}
{/* Dropdown results — only when typing */}
{query.trim() ? (
) : null}
)}
>
);
}