EventBuzz_Website/src/pages/SingleEventPage.tsx

707 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (
<li className="overflow-hidden rounded-lg border border-[rgba(116,112,112,0.2)] bg-white text-sm">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
disabled={!hasExpandableContent}
className={`flex w-full items-center justify-between gap-3 px-3 py-3 text-left ${
!hasExpandableContent ? "cursor-default opacity-90" : ""
}`}
>
<div className="min-w-0 flex-1">
<p className="font-semibold text-[#24282f]">{name}</p>
<span className="mt-1 inline-block rounded bg-[#e5e5ea] px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-[#24282f]">
{tag}
</span>
</div>
{hasExpandableContent ? (
<ChevronDown
className={`size-5 shrink-0 text-[#1e72ef] transition-transform ${open ? "rotate-180" : ""}`}
strokeWidth={2}
aria-hidden
/>
) : null}
</button>
{open && hasExpandableContent ? (
<div className="border-t border-[#f2f2f7] bg-[#fafafa] px-3 py-3 text-[#24282f]">
<div className="flex flex-col gap-3">
{person.photo?.trim() ? (
<img
src={person.photo.trim()}
alt=""
className="h-24 w-24 rounded-full object-cover"
/>
) : null}
{person.title?.trim() ? (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">Title</p>
<p className="mt-1 text-sm leading-snug">{person.title.trim()}</p>
</div>
) : null}
{expertise.length > 0 ? (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">
Specialization
</p>
<div className="mt-1.5 flex flex-wrap gap-1.5">
{expertise.map((x) => (
<span
key={x}
className="rounded-md bg-[#e8f0fe] px-2 py-0.5 text-xs font-medium text-[#1e72ef]"
>
{x}
</span>
))}
</div>
</div>
) : null}
{person.bio?.trim() ? (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">Bio</p>
<p className="mt-1 whitespace-pre-wrap text-sm leading-snug">{person.bio.trim()}</p>
</div>
) : null}
{hasPublicExtras ? (
<div className="border-t border-[#e5e5ea] pt-3">
{person.organizationName?.trim() ? (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">
Organisation
</p>
<p className="mt-1 text-sm leading-snug">{person.organizationName.trim()}</p>
</div>
) : null}
{hasLinkedIn || hasTwitter || hasWebsite ? (
<div className={person.organizationName?.trim() ? "mt-3" : ""}>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">
Links
</p>
<div className="mt-1.5 flex flex-col gap-1.5">
{hasLinkedIn ? (
<a
href={absoluteUrl(social!.linkedin!)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-[#1e72ef] underline"
>
<ExternalLink className="size-3.5 shrink-0" strokeWidth={2} />
LinkedIn
</a>
) : null}
{hasTwitter ? (
<a
href={absoluteUrl(social!.twitter!)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-[#1e72ef] underline"
>
<ExternalLink className="size-3.5 shrink-0" strokeWidth={2} />
X (Twitter)
</a>
) : null}
{hasWebsite ? (
<a
href={absoluteUrl(social!.website!)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm font-medium text-[#1e72ef] underline"
>
<ExternalLink className="size-3.5 shrink-0" strokeWidth={2} />
Website
</a>
) : null}
</div>
</div>
) : null}
</div>
) : null}
</div>
</div>
) : null}
</li>
);
}
function headerLogoForEvent(
eventId: string,
ev: LoginPreviewResponse | null | undefined,
ctx: ReturnType<typeof readStoredVisitorContext>,
): 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<typeof readStoredVisitorContext>,
): 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<VisitorSessionPreview | null>(null);
const [eventInfo, setEventInfo] = useState<LoginPreviewResponse | null>(null);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string | null>(null);
const [tab, setTab] = useState(0);
const [sessionMetaOpen, setSessionMetaOpen] = useState(false);
const { bookmarkedIds, toggle: toggleBookmark } = useSessionBookmarks(eventId);
const [agendaSessions, setAgendaSessions] = useState<VisitorSessionPreview[]>([]);
const [agendaLoading, setAgendaLoading] = useState(false);
const [agendaErr, setAgendaErr] = useState<string | null>(null);
const [dayOpen, setDayOpen] = useState<Record<string, boolean>>({});
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 (
<MobileShell className="relative">
<HeaderMobile
eventName={headerEventNameForEvent(eventId, null, ctx)}
eventId={eventId || null}
showBack={false}
/>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-center text-sm text-[#24282f]">{err}</p>
<Link to="/" className="mt-4 text-sm font-semibold text-[#1e72ef] underline">
Home
</Link>
</main>
</MobileShell>
);
}
if (loading) {
return (
<MobileShell className="relative">
<HeaderMobile
logoUrl={headerLogoForEvent(eventId, null, ctx)}
eventName={headerEventNameForEvent(eventId, null, ctx)}
eventId={eventId || null}
showBack={false}
/>
<main className="flex flex-1 items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-sm text-[#24282f]">Loading session…</p>
</main>
</MobileShell>
);
}
if (err || !session) {
return (
<MobileShell className="relative">
<HeaderMobile
logoUrl={headerLogoForEvent(eventId, eventInfo, ctx)}
eventName={headerEventNameForEvent(eventId, eventInfo, ctx)}
eventId={eventId || null}
showBack={false}
/>
<main className="flex flex-1 flex-col items-center justify-center bg-[#f2f2f7] px-4 py-10">
<p className="text-center text-sm text-red-600">{err ?? "Session not found."}</p>
</main>
</MobileShell>
);
}
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 (
<MobileShell className="relative bg-transparent">
<HeaderMobile
logoUrl={headerLogoUrl}
eventName={headerEventName}
eventId={eventId || null}
showBack={false}
/>
{ev?.bannerImage?.trim() ? (
<div className="w-full overflow-hidden bg-[#e5e5ea]">
<img
src={ev.bannerImage.trim()}
alt=""
className="max-h-[200px] w-full object-cover object-center"
/>
</div>
) : null}
<div className="eb-gradient-banne card-bg relative flex flex-col items-cente justify-center rounded-b-lg gap-1.5 px-6 pb-4 pt-3 text-cente text-whit text-[var(--theme-card-text-color)]">
<div className="absolute right-3 top-2.5 z-10 flex max-w-[calc(100%-4.5rem)] flex-col items-end gap-1.5">
{live ? (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-[#14ae5c] px-2 py-0.5 text-[10px] font-semibold text-white">
<span className="size-1 rounded-full bg-white" />
LIVE
</span>
) : null}
<button
type="button"
onClick={() => toggleBookmark(sessionId)}
className="shrink-0 rounded-md p-1 text-[var(--theme-button-color)] hover:bg-white/15"
aria-label={sessionBookmarked ? "Remove bookmark" : "Bookmark session"}
aria-pressed={sessionBookmarked}
>
<Bookmark className={`size-6 ${sessionBookmarked ? "fill-current" : ""}`} strokeWidth={1.75} />
</button>
</div>
<div className="flex max-w-full flex-wrap items-center justify-cente gap-x-2 gap-y-1 pe-2 text-[10px]">
<span className="rounded bg-[var(--theme-button-color)]/20 px-2 py-0.5">{typeLabel}</span>
{timeLine ? <span>{timeLine}</span> : null}
{trackTitle !== typeLabel ? <span className="opacity-90">{trackTitle}</span> : null}
</div>
<h1 className="font-inter text-2xl font-bold tracking-tight">{session.title}</h1>
<div className="flex w-full max-w-full flex-wrap items-center justify-end gap-x-3 gap-y-1 pr-1 text-sm text-[var(--theme-card-text-color)]/70">
{duration ? (
<span className="flex items-center gap-1.5">
<Clock className="size-4 shrink-0" strokeWidth={1.75} />
{startT} ({duration})
</span>
) : startT ? (
<span className="flex items-center gap-1.5">
<Clock className="size-4 shrink-0" strokeWidth={1.75} />
{startT}
</span>
) : null}
{sessionDate ? (
<span className="flex items-center gap-1.5">
<Calendar className="size-4 shrink-0" strokeWidth={1.75} />
{sessionDate}
</span>
) : null}
{hasExpandableSessionMeta ? (
<button
type="button"
onClick={() => setSessionMetaOpen((o) => !o)}
aria-expanded={sessionMetaOpen}
aria-label={
sessionMetaOpen ? "Hide session notes and location" : "Show session notes and location"
}
className="inline-flex shrink-0 items-center rounded-md p-0.5 text-[var(--theme-card-text-color)] hover:bg-[var(--theme-button-color)]/15"
>
<ChevronDown
className={`size-5 transition-transform ${sessionMetaOpen ? "rotate-180" : ""}`}
strokeWidth={2}
aria-hidden
/>
</button>
) : null}
</div>
{sessionMetaOpen && hasExpandableSessionMeta ? (
<div className="mt-2 w-full max-w-full border-t border-[var(--theme-card-color)]/35 px-1 pt-3 text-left text-sm text-[var(--theme-card-text-color)]/95">
{sessionNotes ? (
<div className="mb-3">
<p className="text-[10px] font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/80">Notes</p>
<p className="mt-1.5 whitespace-pre-wrap leading-snug">{sessionNotes}</p>
</div>
) : null}
{sessionLocation || trackHall ? (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[var(--theme-card-text-color)]/80">Location</p>
<ul className="mt-1.5 flex flex-col gap-1.5">
{sessionLocation ? (
<li className="flex items-start gap-2 leading-snug">
<MapPin className="mt-0.5 size-4 shrink-0 text-[var(--theme-button-color)]" strokeWidth={1.75} />
<span>{sessionLocation}</span>
</li>
) : null}
{trackHall ? (
<li className="flex items-start gap-2 leading-snug">
<MapPin className="mt-0.5 size-4 shrink-0 text-white/90" strokeWidth={1.75} />
<span>{trackHall}</span>
</li>
) : null}
</ul>
</div>
) : null}
{hasCapacity ? (
<p className="mt-3 text-xs text-white/85">Capacity: {session.capacity}</p>
) : null}
</div>
) : null}
</div>
<main className="test-class relative z-0 w-full max-w-none flex-1 bg-transparent px-4 pb-28 pt-4">
<div className="flex w-full max-w-none flex-col gap-4">
{session && session.enableQnA !== false ? (
<SessionQuestionsCard
title="Audience Questions"
showQuestionNav
eventId={eventId}
sessionId={sessionId}
/>
) : null}
<SessionQuizPollsSection eventId={eventId} sessionId={sessionId} />
{showSessionFeedback || showSessionRatings ? (
<div className="flex w-full max-w-none flex-col gap-2">
<p className="px-1 text-[10px] font-semibold uppercase tracking-wide text-[#8e8e93]">
Feedback & ratings
</p>
<div className="flex flex-col gap-2">
{showSessionFeedback ? (
<Link
to={sessionFeedbackHref}
className="flex items-center justify-between gap-3 rounded-lg border border-[rgba(116,112,112,0.2)] bg-whit card-bg px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]"
>
<span className="flex min-w-0 items-center gap-2.5">
<MessageCircle className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} />
<span className="truncate">Session feedback</span>
</span>
<ChevronRight className="size-4 shrink-0 text-[#c7c7cc]" strokeWidth={2} aria-hidden />
</Link>
) : null}
{showSessionRatings ? (
<Link
to={sessionRatingsHref}
className="flex items-center justify-between gap-3 rounded-lg border border-[rgba(116,112,112,0.2)] bg-whit card-bg px-3 py-3 text-sm font-medium text-[#24282f] shadow-[0_1px_4px_rgba(12,12,13,0.05)] active:bg-[#f2f2f7]"
>
<span className="flex min-w-0 items-center gap-2.5">
<Star className="size-[18px] shrink-0 text-[#1e72ef]" strokeWidth={1.75} />
<span className="truncate">Rate this session</span>
</span>
<ChevronRight className="size-4 shrink-0 text-[#c7c7cc]" strokeWidth={2} aria-hidden />
</Link>
) : null}
</div>
</div>
) : null}
{visibleTabs.length > 0 ? (
<div className="flex w-full border-b border-[#c7c7cc] bg-transparent">
{visibleTabs.map((t, i) => (
<button
key={t.id}
type="button"
onClick={() => setTab(i)}
className={`flex-1 px-1 pb-2 pt-3 text-center text-sm sm:px-2 ${
tab === i
? "border-b-2 border-[#1e72ef] font-semibold text-[#24282f]"
: "-mb-px border-b-2 border-transparent font-normal text-[#24282f]/80"
}`}
>
{t.label}
</button>
))}
</div>
) : null}
{activeTabId === "key-persons" ? (
<ul className="flex flex-col gap-3">
{(session.speakers || []).map((s) => (
<KeyPersonRow
key={s._id}
person={s}
tag={s.sessionRole?.trim() || s.speakerRole?.trim() || "Speaker"}
/>
))}
{session.moderator ? (
<KeyPersonRow
key={`mod-${session.moderator._id}`}
person={session.moderator}
tag={session.moderator.speakerRole?.trim() || "Moderator"}
/>
) : null}
{(!session.speakers || session.speakers.length === 0) && !session.moderator ? (
<p className="text-sm text-[rgba(36,40,47,0.6)]">No speakers listed yet.</p>
) : null}
</ul>
) : null}
{activeTabId === "my-sessions" ? (
<div className="w-full max-w-none">
{agendaLoading && !agendaSessions.length ? (
<p className="text-sm text-[rgba(36,40,47,0.55)]">Loading…</p>
) : bookmarkedSessionsSorted.length === 0 ? (
<p className="text-sm text-[rgba(36,40,47,0.55)]">
No bookmarked sessions yet. Tap the bookmark on any agenda session to save it here.
</p>
) : (
<LandingMySessionsList
sessions={bookmarkedSessionsSorted}
eventId={eventId}
bookmarkedIds={bookmarkedIds}
toggleBookmark={toggleBookmark}
/>
)}
</div>
) : null}
{activeTabId === "agenda" ? (
<div className="w-full max-w-none">
{agendaErr ? <p className="text-sm text-red-600">{agendaErr}</p> : null}
{agendaLoading && !agendaSessions.length ? (
<p className="text-sm text-[rgba(36,40,47,0.55)]">Loading agenda…</p>
) : agendaDays.length === 0 ? (
<p className="text-sm text-[rgba(36,40,47,0.55)]">No sessions scheduled for this event yet.</p>
) : (
<LandingAgendaCards
days={agendaDays}
dayOpen={dayOpen}
setDayOpen={setDayOpen}
eventId={eventId}
bookmarkedIds={bookmarkedIds}
toggleBookmark={toggleBookmark}
/>
)}
</div>
) : null}
</div>
</main>
<HyperNavigationFAB
eventId={eventId}
sessionId={sessionId}
showFeedbackNav={showSessionFeedback}
showRatingsNav={showSessionRatings}
feedbackFormId={showSessionFeedback ? session.audienceFeedbackForm?.trim() || undefined : undefined}
ratingsFormId={showSessionRatings ? session.audienceRatingsForm?.trim() || undefined : undefined}
/>
</MobileShell>
);
}