import { useEffect, useMemo, useState } from "react"; import { Bookmark, Calendar, ChevronDown, ChevronRight, Clock, ExternalLink, MapPin, MessageCircle, Star } from "lucide-react"; import { Link, useSearchParams } from "react-router-dom"; import { HeaderMobile } from "../components/HeaderMobile"; import { groupSessionsIntoAgendaDays, LandingAgendaCards, LandingMySessionsList, } from "../components/EventLandingAgenda"; import { HyperNavigationFAB } from "../components/HyperNavigationFAB"; import { MobileShell } from "../components/MobileShell"; import { SessionQuestionsCard } from "../components/SessionQuestionsCard"; import { SessionQuizPollsSection } from "../components/SessionQuizPollsSection"; import { getVisitorEventAgenda, getVisitorLoginPreview, getVisitorSessionPreview, type VisitorSessionPreview, type VisitorSessionPreviewSpeaker, } from "../lib/api"; import { formatDateLong, formatDurationMinutes, formatTimeShort, sessionTypeDisplay, } from "@/lib/sessionDisplay"; import type { LoginPreviewResponse } from "../types/visitorContext"; import { readStoredVisitorContext } from "../types/visitorContext"; import { useSessionBookmarks } from "../hooks/useSessionBookmarks"; import { useEventRegistrationSettings } from "../hooks/useEventRegistrationSettings"; import { useAutoNavigateToLiveQuizPoll } from "../hooks/useAutoNavigateToLiveQuizPoll"; type SessionTabId = "key-persons" | "my-sessions" | "agenda"; const SESSION_TABS: { id: SessionTabId; label: string }[] = [ { id: "key-persons", label: "Key Persons" }, { id: "my-sessions", label: "My Sessions" }, { id: "agenda", label: "Agenda" }, ]; function sessionTabEnabled(session: VisitorSessionPreview, tabId: SessionTabId): boolean { if (tabId === "key-persons") return session.audienceKeyPersonsEnabled !== false; if (tabId === "my-sessions") return session.audienceMySessionsEnabled !== false; if (tabId === "agenda") return session.audienceAgendaEnabled !== false; return true; } function speakerName(s: { firstName?: string; lastName?: string }): string { return [s.firstName, s.lastName].filter(Boolean).join(" ").trim() || "Speaker"; } function absoluteUrl(href: string): string { const t = href.trim(); if (!t) return ""; if (/^https?:\/\//i.test(t)) return t; return `https://${t}`; } function KeyPersonRow({ person, tag, }: { person: VisitorSessionPreviewSpeaker; tag: string; }) { const [open, setOpen] = useState(false); const name = speakerName(person); const expertise = (person.expertise || []).filter((x) => String(x).trim()); const social = person.socialLinks; const hasLinkedIn = Boolean(social?.linkedin?.trim()); const hasTwitter = Boolean(social?.twitter?.trim()); const hasWebsite = Boolean(social?.website?.trim()); const hasPublicExtras = Boolean(person.organizationName?.trim()) || hasLinkedIn || hasTwitter || hasWebsite; const hasExpandableContent = Boolean( person.photo?.trim() || person.title?.trim() || person.bio?.trim() || expertise.length > 0 || hasPublicExtras, ); return (
  • {open && hasExpandableContent ? (
    {person.photo?.trim() ? ( ) : null} {person.title?.trim() ? (

    Title

    {person.title.trim()}

    ) : null} {expertise.length > 0 ? (

    Specialization

    {expertise.map((x) => ( {x} ))}
    ) : null} {person.bio?.trim() ? (

    Bio

    {person.bio.trim()}

    ) : null} {hasPublicExtras ? (
    {person.organizationName?.trim() ? (

    Organisation

    {person.organizationName.trim()}

    ) : null} {hasLinkedIn || hasTwitter || hasWebsite ? (

    Links

    {hasLinkedIn ? ( LinkedIn ) : null} {hasTwitter ? ( X (Twitter) ) : null} {hasWebsite ? ( Website ) : null}
    ) : null}
    ) : null}
    ) : null}
  • ); } function headerLogoForEvent( eventId: string, ev: LoginPreviewResponse | null | undefined, ctx: ReturnType, ): string | undefined { const fromPreview = ev?.logoUrl?.trim(); if (fromPreview) return fromPreview; if (ctx?.eventId === eventId && ctx.event?.logoUrl?.trim()) return ctx.event.logoUrl.trim(); return undefined; } function headerEventNameForEvent( eventId: string, ev: LoginPreviewResponse | null | undefined, ctx: ReturnType, ): string | undefined { const fromPreview = ev?.name?.trim(); if (fromPreview) return fromPreview; if (ctx?.eventId === eventId && ctx.event?.name?.trim()) return ctx.event.name.trim(); return undefined; } export function SingleEventPage() { const [searchParams] = useSearchParams(); const eventId = searchParams.get("event_id")?.trim() || ""; const sessionId = searchParams.get("session_id")?.trim() || ""; const ctx = readStoredVisitorContext(); const [session, setSession] = useState(null); const [eventInfo, setEventInfo] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [tab, setTab] = useState(0); const [sessionMetaOpen, setSessionMetaOpen] = useState(false); const { bookmarkedIds, toggle: toggleBookmark } = useSessionBookmarks(eventId); const [agendaSessions, setAgendaSessions] = useState([]); const [agendaLoading, setAgendaLoading] = useState(false); const [agendaErr, setAgendaErr] = useState(null); const [dayOpen, setDayOpen] = useState>({}); const reg = useEventRegistrationSettings(eventInfo); useAutoNavigateToLiveQuizPoll({ eventId, sessionId, enabled: Boolean(eventId && sessionId && !loading && !err && session), }); useEffect(() => { if (!eventId || !sessionId) { setErr("Missing session link. Open the session from your event QR code or invite."); setLoading(false); setSession(null); setEventInfo(null); return; } let cancelled = false; setLoading(true); setErr(null); (async () => { try { const sess = await getVisitorSessionPreview(sessionId, eventId); if (cancelled) return; setSession(sess); if (ctx?.eventId === eventId && ctx.event) { setEventInfo(ctx.event); } else { const ev = await getVisitorLoginPreview(eventId); if (cancelled) return; setEventInfo(ev); } } catch (e) { if (!cancelled) { setErr(e instanceof Error ? e.message : "Could not load session."); setSession(null); setEventInfo(null); } } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [eventId, sessionId, ctx?.eventId]); const visibleTabs = useMemo(() => { if (!session) return []; return SESSION_TABS.filter((t) => sessionTabEnabled(session, t.id)); }, [session]); useEffect(() => { setTab((t) => (t >= visibleTabs.length ? 0 : t)); }, [visibleTabs.length]); const activeTabId = visibleTabs[tab]?.id ?? null; const needsAgendaData = session ? session.audienceMySessionsEnabled !== false || session.audienceAgendaEnabled !== false : true; useEffect(() => { if (!eventId || !needsAgendaData) { setAgendaSessions([]); setAgendaErr(null); setAgendaLoading(false); return; } let cancelled = false; setAgendaLoading(true); setAgendaErr(null); setAgendaSessions([]); getVisitorEventAgenda(eventId) .then((sessions) => { if (!cancelled) setAgendaSessions(sessions); }) .catch((e) => { if (!cancelled) { setAgendaErr(e instanceof Error ? e.message : "Could not load agenda."); setAgendaSessions([]); } }) .finally(() => { if (!cancelled) setAgendaLoading(false); }); return () => { cancelled = true; }; }, [eventId, needsAgendaData]); useEffect(() => { setDayOpen({}); }, [eventId]); const agendaDays = useMemo(() => groupSessionsIntoAgendaDays(agendaSessions), [agendaSessions]); const bookmarkedSessionsSorted = useMemo(() => { if (!bookmarkedIds.size) return []; return agendaSessions .filter((s) => bookmarkedIds.has(s._id)) .sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()); }, [agendaSessions, bookmarkedIds]); useEffect(() => { const first = agendaDays[0]?.dateKey; if (!first) return; setDayOpen((prev) => { if (Object.keys(prev).length > 0) return prev; return { [first]: true }; }); }, [agendaDays]); if (!eventId || !sessionId) { return (

    {err}

    Home
    ); } if (loading) { return (

    Loading session…

    ); } if (err || !session) { return (

    {err ?? "Session not found."}

    ); } const ev = eventInfo; const typeLabel = sessionTypeDisplay(session.type); const trackTitle = session.track?.name?.trim() || typeLabel; const startT = formatTimeShort(session.startTime); const endT = formatTimeShort(session.endTime); const timeLine = startT && endT ? `${startT} – ${endT}` : startT; const duration = formatDurationMinutes(session.startTime, session.endTime); const sessionDate = formatDateLong(session.startTime); const live = session.status === "ongoing"; const sessionBookmarked = bookmarkedIds.has(sessionId); const headerLogoUrl = headerLogoForEvent(eventId, ev, ctx); const headerEventName = headerEventNameForEvent(eventId, ev, ctx); const sessionAllowsFeedback = session.audienceFeedbackEnabled !== false; const sessionAllowsRatings = session.audienceRatingsEnabled !== false; const showSessionFeedback = reg.enableFeedback && sessionAllowsFeedback; const showSessionRatings = reg.enableRatings && sessionAllowsRatings; const sessionFeedbackHref = (() => { const q = new URLSearchParams({ event_id: eventId, session_id: sessionId }); const fid = session.audienceFeedbackForm?.trim(); if (fid) q.set("form_id", fid); return `/feedback?${q.toString()}`; })(); const sessionRatingsHref = (() => { const q = new URLSearchParams({ event_id: eventId, session_id: sessionId }); const fid = session.audienceRatingsForm?.trim(); if (fid) q.set("form_id", fid); return `/ratings?${q.toString()}`; })(); const sessionNotes = session.description?.trim() ?? ""; const sessionLocation = session.location?.trim() ?? ""; const trackHall = session.track?.hall?.trim() ?? ""; const hasCapacity = session.capacity != null && session.capacity > 0; const hasExpandableSessionMeta = Boolean(sessionNotes || sessionLocation || trackHall || hasCapacity); return ( {ev?.bannerImage?.trim() ? (
    ) : null}
    {live ? ( LIVE ) : null}
    {typeLabel} {timeLine ? {timeLine} : null} {trackTitle !== typeLabel ? {trackTitle} : null}

    {session.title}

    {duration ? ( {startT} ({duration}) ) : startT ? ( {startT} ) : null} {sessionDate ? ( {sessionDate} ) : null} {hasExpandableSessionMeta ? ( ) : null}
    {sessionMetaOpen && hasExpandableSessionMeta ? (
    {sessionNotes ? (

    Notes

    {sessionNotes}

    ) : null} {sessionLocation || trackHall ? (

    Location

      {sessionLocation ? (
    • {sessionLocation}
    • ) : null} {trackHall ? (
    • {trackHall}
    • ) : null}
    ) : null} {hasCapacity ? (

    Capacity: {session.capacity}

    ) : null}
    ) : null}
    {session && session.enableQnA !== false ? ( ) : null} {showSessionFeedback || showSessionRatings ? (

    Feedback & ratings

    {showSessionFeedback ? ( Session feedback ) : null} {showSessionRatings ? ( Rate this session ) : null}
    ) : null} {visibleTabs.length > 0 ? (
    {visibleTabs.map((t, i) => ( ))}
    ) : null} {activeTabId === "key-persons" ? (
      {(session.speakers || []).map((s) => ( ))} {session.moderator ? ( ) : null} {(!session.speakers || session.speakers.length === 0) && !session.moderator ? (

      No speakers listed yet.

      ) : null}
    ) : null} {activeTabId === "my-sessions" ? (
    {agendaLoading && !agendaSessions.length ? (

    Loading…

    ) : bookmarkedSessionsSorted.length === 0 ? (

    No bookmarked sessions yet. Tap the bookmark on any agenda session to save it here.

    ) : ( )}
    ) : null} {activeTabId === "agenda" ? (
    {agendaErr ?

    {agendaErr}

    : null} {agendaLoading && !agendaSessions.length ? (

    Loading agenda…

    ) : agendaDays.length === 0 ? (

    No sessions scheduled for this event yet.

    ) : ( )}
    ) : null}
    ); }