import type { AttendeeParticipant } from "./attendeeAuth"; import { getAttendeeToken } from "./attendeeAuth"; import type { LoginPreviewResponse, ResolveCodeResponse, VisitorJoinType } from "../types/visitorContext"; function getBaseUrl(): string { const raw = import.meta.env.VITE_API_BASE_URL; if (typeof raw !== "string" || !raw.trim()) return ""; return raw.replace(/\/$/, ""); } async function parseJson(res: Response): Promise { const text = await res.text(); if (!text) return null; try { return JSON.parse(text) as unknown; } catch { return null; } } function messageFromBody(body: unknown): string { if (body && typeof body === "object" && "error" in body) { const err = (body as { error?: { message?: string } }).error; if (err?.message) return err.message; } return "Something went wrong"; } export async function postVisitorResolveCode(code: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/resolve-code`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); const body = await parseJson(res); if (!res.ok) { throw new Error(messageFromBody(body)); } if (!body || typeof body !== "object" || !("data" in body)) { throw new Error("Invalid response"); } const data = (body as { data: ResolveCodeResponse }).data; if (!data?.type || !data.eventId) { throw new Error("Invalid response"); } if (data.type === "session" && !data.sessionId) { throw new Error("Invalid response"); } if (data.type === "track" && !data.trackId) { throw new Error("Invalid response"); } return data; } export async function getVisitorLoginPreview(eventId: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/login-preview`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) { throw new Error(messageFromBody(body)); } if (!body || typeof body !== "object" || !("data" in body)) { throw new Error("Invalid response"); } return (body as { data: LoginPreviewResponse }).data; } export type VisitorEventTrackListItem = { _id: string; event: string; name: string; hall?: string; trackType?: string; status?: string; joinCode?: string; }; /** Public list of tracks for building event agenda links. */ export async function getVisitorEventTracks(eventId: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/tracks`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { tracks?: VisitorEventTrackListItem[] } }).data; return Array.isArray(data?.tracks) ? data.tracks : []; } export type VisitorEventQuizPollListItem = { _id: string; title: string; type: string; session: string; }; /** Public list of active session-scoped quizzes/polls (for take links). */ export async function getVisitorEventQuizPolls(eventId: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/quiz-polls`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { quizPolls?: VisitorEventQuizPollListItem[] } }).data; return Array.isArray(data?.quizPolls) ? data.quizPolls : []; } /** Requires attendee JWT; participant must belong to `eventId`. */ export async function getVisitorEventRewardBalance(eventId: string): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Not signed in"); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/reward-balance`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { total?: unknown } }).data; const n = typeof data?.total === "number" ? data.total : Number(data?.total); return Number.isFinite(n) ? n : 0; } export type VisitorRewardHistoryItem = { _id: string; points: number; type: string; source?: string; description?: string; createdAt?: string; }; /** Paginated ledger for the signed-in attendee and event. */ export async function getVisitorRewardHistory( eventId: string, opts?: { page?: number; limit?: number } ): Promise<{ items: VisitorRewardHistoryItem[]; total: number; page: number; limit: number }> { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Not signed in"); const sp = new URLSearchParams(); if (opts?.page != null) sp.set("page", String(opts.page)); if (opts?.limit != null) sp.set("limit", String(opts.limit)); const q = sp.toString(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/reward-history${q ? `?${q}` : ""}`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { items?: VisitorRewardHistoryItem[]; total?: number; page?: number; limit?: number } }).data; return { items: Array.isArray(data?.items) ? data.items : [], total: typeof data?.total === "number" ? data.total : 0, page: typeof data?.page === "number" ? data.page : 1, limit: typeof data?.limit === "number" ? data.limit : 20, }; } /** Attendee JWT required. Session ids bookmarked for this event. */ export async function getVisitorSessionBookmarks(eventId: string): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Not signed in"); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/session-bookmarks`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { sessionIds?: string[] } }).data; return Array.isArray(data?.sessionIds) ? data.sessionIds : []; } /** Attendee JWT required. Returns updated bookmark list for the event. */ export async function putVisitorSessionBookmark( eventId: string, sessionId: string, bookmarked: boolean, ): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Not signed in"); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/session-bookmarks`; const res = await fetch(url, { method: "PUT", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ sessionId, bookmarked }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { sessionIds?: string[] } }).data; return Array.isArray(data?.sessionIds) ? data.sessionIds : []; } export type VisitorSessionPreviewSpeakerSocial = { linkedin?: string; twitter?: string; website?: string; }; export type VisitorSessionPreviewSpeaker = { _id: string; firstName?: string; lastName?: string; title?: string; photo?: string; /** Faculty profile role (directory). Prefer sessionRole for this session. */ speakerRole?: string; /** Per-session key-person role assigned on the session. */ sessionRole?: string; /** Shown when present (private profiles still get bio / expertise / photo / title). */ bio?: string; expertise?: string[]; /** Only included when speaker profile visibility is public. */ organizationName?: string; /** Only included when speaker profile visibility is public. */ socialLinks?: VisitorSessionPreviewSpeakerSocial; }; export type VisitorSessionPreview = { _id: string; event: string; track: { _id: string; name?: string; hall?: string; trackType?: string } | null; title: string; description?: string; type: string; startTime: string; endTime: string; location?: string; capacity?: number; status: string; joinCode?: string; speakers: VisitorSessionPreviewSpeaker[]; moderator: VisitorSessionPreviewSpeaker | null; /** false = hide feedback link on session page (event must still allow feedback). */ audienceFeedbackEnabled?: boolean; /** false = hide ratings link on session page (event must still enable ratings). */ audienceRatingsEnabled?: boolean; /** false = hide Key Persons tab on session page. */ audienceKeyPersonsEnabled?: boolean; /** false = hide My Sessions tab on session page. */ audienceMySessionsEnabled?: boolean; /** false = hide Agenda tab on session page. */ audienceAgendaEnabled?: boolean; /** false = hide Q&A on visitor session page. */ enableQnA?: boolean; audienceFeedbackForm?: string | null; audienceRatingsForm?: string | null; }; export type VisitorPublicFeedbackFormField = { label?: string; type?: string; required?: boolean; options?: string[]; placeholder?: string; remarks?: string; }; export type VisitorPublicFeedbackForm = { _id: string; name: string; description?: string; fields: VisitorPublicFeedbackFormField[]; thankYouMessage?: string; showThankYou?: boolean; }; /** Public form schema for rendering; form must be type feedback and belong to event org. */ export async function getVisitorFeedbackFormSchema( eventId: string, formId: string, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/feedback-forms/${encodeURIComponent(formId)}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorPublicFeedbackForm }).data; } /** Resolve session-linked or default active feedback/survey form for an event. */ export async function resolveVisitorFeedbackFormSchema( eventId: string, opts?: { sessionId?: string; purpose?: "feedback" | "ratings" }, ): Promise { const base = getBaseUrl(); const qs = new URLSearchParams(); if (opts?.sessionId?.trim()) qs.set("session_id", opts.sessionId.trim()); if (opts?.purpose) qs.set("purpose", opts.purpose); const q = qs.toString(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/feedback-forms/resolve${q ? `?${q}` : ""}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorPublicFeedbackForm }).data; } export async function postPublicFormSubmit( formId: string, data: Record, participantId?: string | null, ): Promise { const base = getBaseUrl(); const url = `${base}/api/forms/${encodeURIComponent(formId)}/submit`; const token = getAttendeeToken(); const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ data, ...(participantId ? { participantId } : {}), }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object") throw new Error("Invalid response"); return body; } /** Public full event agenda (sessions with track/time) for attendee schedule. */ export async function getVisitorEventAgenda(eventId: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/agenda`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { sessions?: VisitorSessionPreview[] } }).data; return Array.isArray(data?.sessions) ? data.sessions : []; } export type VisitorTrackPreview = { _id: string; event: string; name: string; hall?: string; trackType?: string; status?: string; joinCode?: string; }; export async function getVisitorSessionPreview( sessionId: string, eventId: string, ): Promise { const base = getBaseUrl(); const qs = new URLSearchParams({ event_id: eventId }); const url = `${base}/api/visitor/sessions/${encodeURIComponent(sessionId)}/preview?${qs}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorSessionPreview }).data; } export type VisitorQuizPollResponseItem = { questionIndex: number; answer: unknown; isCorrect?: boolean; pointsEarned?: number; /** Per-question seconds (quiz master). */ timeSpent?: number; }; /** Attendee JWT required. Submits answers for a session quiz/poll (Mongo id from link / RTDB). */ export type VisitorQuizPollSubmitResult = Record & { reward?: { pointsEarned?: number }; }; export async function postVisitorQuizPollResponse( eventId: string, quizPollId: string, payload: { responses: VisitorQuizPollResponseItem[]; totalScore?: number; timeSpent?: number; }, ): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Sign in to submit"); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/quiz-polls/${encodeURIComponent(quizPollId)}/responses`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorQuizPollSubmitResult }).data; } export type VisitorQuizPollMyResponse = { hasResponse: boolean; answeredQuestionIndices: number[]; totalScore?: number; }; /** Attendee JWT — existing answers for this quiz/poll (quiz master per-question state). */ export async function getVisitorQuizPollMyResponse( eventId: string, quizPollId: string, ): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) { return { hasResponse: false, answeredQuestionIndices: [] }; } const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/quiz-polls/${encodeURIComponent(quizPollId)}/responses/me`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorQuizPollMyResponse }).data; } export type VisitorPollResultsOptionStat = { optionId: string; text: string; count: number; percent: number; }; export type VisitorPollResultsQuestion = { questionIndex: number; questionId: string; type: string; question: string; totalResponses: number; kind?: string; options?: VisitorPollResultsOptionStat[]; answeredCount?: number; percentAnswered?: number; }; export type VisitorPollResultsSummary = { totalSubmissions: number; questions: VisitorPollResultsQuestion[]; }; /** Attendee JWT. Aggregated percentages for poll/survey when showResults is enabled on the server. */ export async function getVisitorQuizPollResultsSummary( eventId: string, quizPollId: string, ): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); if (!token) throw new Error("Sign in to view results"); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/quiz-polls/${encodeURIComponent(quizPollId)}/results-summary`; const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: VisitorPollResultsSummary }).data; if (!data?.questions || !Array.isArray(data.questions)) throw new Error("Invalid response"); return { totalSubmissions: typeof data.totalSubmissions === "number" ? data.totalSubmissions : 0, questions: data.questions, }; } export async function getVisitorTrackPreview(trackId: string, eventId: string): Promise { const base = getBaseUrl(); const qs = new URLSearchParams({ event_id: eventId }); const url = `${base}/api/visitor/tracks/${encodeURIComponent(trackId)}/preview?${qs}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorTrackPreview }).data; } export type VisitorSessionQuestion = { _id: string; author: string; text: string; up: number; down: number; createdAt: string; }; export async function getVisitorSessionQuestions( sessionId: string, eventId: string, ): Promise { const base = getBaseUrl(); const qs = new URLSearchParams({ event_id: eventId }); const url = `${base}/api/visitor/sessions/${encodeURIComponent(sessionId)}/questions?${qs}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { questions?: VisitorSessionQuestion[] } }).data; return Array.isArray(data?.questions) ? data.questions : []; } export async function postVisitorSessionQuestionVote( sessionId: string, eventId: string, questionId: string, body: { voterKey: string; direction: "up" | "down" }, ): Promise<{ questionId: string; up: number; down: number; yourVote: "up" | "down" | null; reward?: { pointsEarned: number }; }> { const base = getBaseUrl(); const qs = new URLSearchParams({ event_id: eventId }); const url = `${base}/api/visitor/sessions/${encodeURIComponent(sessionId)}/questions/${encodeURIComponent(questionId)}/vote?${qs}`; const token = getAttendeeToken(); const headers: Record = { "Content-Type": "application/json" }; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(body), }); const parsed = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(parsed)); if (!parsed || typeof parsed !== "object" || !("data" in parsed)) throw new Error("Invalid response"); const data = (parsed as { data: Record }).data; const yourVoteRaw = data?.yourVote; const yourVote = yourVoteRaw === "up" || yourVoteRaw === "down" ? yourVoteRaw : null; const rewardRaw = data?.reward as { pointsEarned?: unknown } | undefined; const pe = Number(rewardRaw?.pointsEarned); return { questionId: String(data?.questionId ?? questionId), up: typeof data?.up === "number" ? data.up : Number(data?.up) || 0, down: typeof data?.down === "number" ? data.down : Number(data?.down) || 0, yourVote, ...(Number.isFinite(pe) && pe > 0 ? { reward: { pointsEarned: pe } } : {}), }; } export async function postVisitorSessionQuestion( sessionId: string, eventId: string, payload: { question: string; isAnonymous?: boolean }, ): Promise<{ question: VisitorSessionQuestion; reward?: { pointsEarned: number } }> { const base = getBaseUrl(); const qs = new URLSearchParams({ event_id: eventId }); const url = `${base}/api/visitor/sessions/${encodeURIComponent(sessionId)}/questions?${qs}`; const token = getAttendeeToken(); const headers: Record = { "Content-Type": "application/json" }; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ question: payload.question, isAnonymous: payload.isAnonymous ?? false, }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); const data = (body as { data: { question?: VisitorSessionQuestion; reward?: { pointsEarned?: number } } }).data; if (!data?.question) throw new Error("Invalid response"); const pe = Number(data.reward?.pointsEarned); return { question: data.question, ...(Number.isFinite(pe) && pe > 0 ? { reward: { pointsEarned: pe } } : {}), }; } export type VisitorLeaderboardRow = { rank: number; participantId: string; total: number; displayName: string; }; export type VisitorLeaderboardResponse = { leaderboard: VisitorLeaderboardRow[]; your: { total: number; rank: number | null } | null; }; /** Public; send attendee JWT optional to receive `your` rank and net total. */ export async function getVisitorEventLeaderboard( eventId: string, opts?: { limit?: number }, ): Promise { const base = getBaseUrl(); const token = getAttendeeToken(); const sp = new URLSearchParams(); if (opts?.limit != null) sp.set("limit", String(opts.limit)); const q = sp.toString(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/leaderboard${q ? `?${q}` : ""}`; const headers: Record = {}; if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(url, { headers }); const parsed = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(parsed)); if (!parsed || typeof parsed !== "object" || !("data" in parsed)) throw new Error("Invalid response"); const data = (parsed as { data: VisitorLeaderboardResponse }).data; return { leaderboard: Array.isArray(data?.leaderboard) ? data.leaderboard : [], your: data?.your && typeof data.your === "object" ? data.your : null, }; } export type VisitorAuthPayload = { token: string; participant: AttendeeParticipant; needsProfile: boolean; pendingApproval: boolean; guestToken?: string; /** Present when new registration loyalty points were awarded */ reward?: { pointsEarned: number }; }; function joinBody( type: VisitorJoinType, sessionId?: string, trackId?: string, inviteToken?: string, ) { return { joinType: type, sessionId: type === "session" ? sessionId : undefined, trackId: type === "track" ? trackId : undefined, inviteToken: inviteToken || undefined, }; } export async function postVisitorStartOtp( eventId: string, email: string, ctx: { type: VisitorJoinType; sessionId?: string; trackId?: string }, inviteToken?: string, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/auth/start-otp`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, ...joinBody(ctx.type, ctx.sessionId, ctx.trackId, inviteToken), }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); } export async function postVisitorVerifyOtp( eventId: string, payload: { email: string; otp: string; inviteToken?: string; firstName?: string; lastName?: string; phone?: string; displayName?: string; }, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/auth/verify-otp`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorAuthPayload }).data; } export async function postVisitorStartLink( eventId: string, email: string, ctx: { type: VisitorJoinType; sessionId?: string; trackId?: string }, inviteToken?: string, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/auth/start-link`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, ...joinBody(ctx.type, ctx.sessionId, ctx.trackId, inviteToken), }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); } export async function postVisitorExchangeLink(token: string): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/auth/exchange-link`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorAuthPayload }).data; } export async function postVisitorGuest( eventId: string, ctx: { type: VisitorJoinType; sessionId?: string; trackId?: string }, opts: { inviteToken?: string; guestToken?: string; displayName?: string; email?: string }, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/events/${encodeURIComponent(eventId)}/auth/guest`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...joinBody(ctx.type, ctx.sessionId, ctx.trackId, opts.inviteToken), guestToken: opts.guestToken, displayName: opts.displayName, email: opts.email, }), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorAuthPayload }).data; } export async function patchVisitorProfile( token: string, payload: { firstName?: string; lastName?: string; phone?: string; displayName?: string; customFields?: Record; }, ): Promise { const base = getBaseUrl(); const url = `${base}/api/visitor/me/profile`; const res = await fetch(url, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: VisitorAuthPayload }).data; } export type PublicQuizPollPresentationQuestion = { _id: string; question: string; type: string; options?: { _id: string; text: string }[]; points?: number; timeLimit?: number; }; export type PublicQuizPollPresentation = { _id: string; eventId: string; sessionId: string; title: string; type: string; settings: { displayMode?: string; isLive?: boolean; liveQuestionId?: string | null; showCorrectAnswer?: boolean; showResults?: boolean; }; questions: PublicQuizPollPresentationQuestion[]; }; export type PublicQuizPollResponder = { participantId: string; displayName: string; isCorrect: boolean | null; pointsEarned: number; answeredAt: string; responseTimeSec: number; answerText?: string; }; export type PublicQuizPollQuestionStats = { questionIndex: number; questionId: string; question: string; type: string; kind: string; totalResponses: number; options?: { optionId: string; text: string; count: number; percent: number; isCorrect?: boolean }[]; answeredCount?: number; percentAnswered?: number; responders: PublicQuizPollResponder[]; }; export async function getPublicQuizPollPresentation(token: string): Promise { const base = getBaseUrl(); const url = `${base}/api/public/quiz-polls/present/${encodeURIComponent(token)}`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: PublicQuizPollPresentation }).data; } export type PublicPresentationSessionActivity = { _id: string; title: string; type: string; presentationToken: string; presentationUrl: string; isLive: boolean; displayMode?: string; liveQuestionId?: string | null; }; export type PublicPresentationSessionActivities = { sessionId: string | null; activities: PublicPresentationSessionActivity[]; }; export async function getPublicPresentationSessionActivities( token: string, ): Promise { const base = getBaseUrl(); const url = `${base}/api/public/quiz-polls/present/${encodeURIComponent(token)}/session-activities`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: PublicPresentationSessionActivities }).data; } export async function getPublicQuizPollQuestionStats( token: string, questionIndex: number, ): Promise { const base = getBaseUrl(); const url = `${base}/api/public/quiz-polls/present/${encodeURIComponent(token)}/questions/${questionIndex}/stats`; const res = await fetch(url); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: PublicQuizPollQuestionStats }).data; } async function postPublicPresentationControl( token: string, pathSuffix: string, payload?: Record, ): Promise { const base = getBaseUrl(); const url = `${base}/api/public/quiz-polls/present/${encodeURIComponent(token)}${pathSuffix}`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: payload != null ? JSON.stringify(payload) : undefined, }); const body = await parseJson(res); if (!res.ok) throw new Error(messageFromBody(body)); if (!body || typeof body !== "object" || !("data" in body)) throw new Error("Invalid response"); return (body as { data: PublicQuizPollPresentation }).data; } export async function publishPublicPresentation(token: string): Promise { return postPublicPresentationControl(token, "/publish"); } export async function closePublicPresentation(token: string): Promise { return postPublicPresentationControl(token, "/close"); } export async function setPublicLiveQuestion( token: string, questionId: string | null, ): Promise { return postPublicPresentationControl(token, "/live-question", { questionId }); }