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() ? (
) : null}
{hasPublicExtras ? (
{person.organizationName?.trim() ? (
Organisation
{person.organizationName.trim()}
) : null}
{hasLinkedIn || hasTwitter || hasWebsite ? (
) : 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 ? (
) : 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}
);
}