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 && (

No new notifications

)}
)}
); } 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 ( <>
{/* ── Left: hamburger + logo (mobile) + search (desktop) ── */}
{/* Hamburger */} {(isMobile || isTablet) && ( )} {/* Logo — mobile only */} {isMobile && } {/* ── Desktop search with dropdown ── */} {!isMobile && (
{/* Input pill */}
{ setQuery(e.target.value); setActiveIndex(-1); setDropdownOpen(!!e.target.value.trim()); }} onFocus={() => { setFocused(true); if (query.trim()) setDropdownOpen(true); }} onBlur={() => setFocused(false)} onKeyDown={handleKeyDown} style={{ background: "transparent", border: "none", outline: "none", fontSize: "var(--text-sm)", fontFamily: "var(--font-noto-sans)", color: "var(--card-foreground)", width: "100%" }} /> {query ? ( ) : ( !isTablet && ( ⌘K ) )}
{/* ── Dropdown (only when there is a query) ── */} {dropdownOpen && query.trim() && (
)}
)}
{/* ── Right: icons ── */}
{/* Mobile search icon */} {isMobile && ( )} {planChip && ( )}
{/* User */}
{/* ── 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}
)} ); }