EventBuzz_Website/src/lib/api.ts

906 lines
32 KiB
TypeScript

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<unknown> {
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<ResolveCodeResponse> {
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<LoginPreviewResponse> {
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<VisitorEventTrackListItem[]> {
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<VisitorEventQuizPollListItem[]> {
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<number> {
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<string[]> {
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<string[]> {
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<VisitorPublicFeedbackForm> {
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<VisitorPublicFeedbackForm> {
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<string, unknown>,
participantId?: string | null,
): Promise<unknown> {
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<VisitorSessionPreview[]> {
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<VisitorSessionPreview> {
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<string, unknown> & {
reward?: { pointsEarned?: number };
};
export async function postVisitorQuizPollResponse(
eventId: string,
quizPollId: string,
payload: {
responses: VisitorQuizPollResponseItem[];
totalScore?: number;
timeSpent?: number;
},
): Promise<VisitorQuizPollSubmitResult> {
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<VisitorQuizPollMyResponse> {
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<VisitorPollResultsSummary> {
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<VisitorTrackPreview> {
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<VisitorSessionQuestion[]> {
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<string, string> = { "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<string, unknown> }).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<string, string> = { "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<VisitorLeaderboardResponse> {
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<string, string> = {};
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<void> {
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<VisitorAuthPayload> {
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<void> {
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<VisitorAuthPayload> {
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<VisitorAuthPayload> {
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<string, string | number | boolean | string[]>;
},
): Promise<VisitorAuthPayload> {
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<PublicQuizPollPresentation> {
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<PublicPresentationSessionActivities> {
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<PublicQuizPollQuestionStats> {
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<string, unknown>,
): Promise<PublicQuizPollPresentation> {
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<PublicQuizPollPresentation> {
return postPublicPresentationControl(token, "/publish");
}
export async function closePublicPresentation(token: string): Promise<PublicQuizPollPresentation> {
return postPublicPresentationControl(token, "/close");
}
export async function setPublicLiveQuestion(
token: string,
questionId: string | null,
): Promise<PublicQuizPollPresentation> {
return postPublicPresentationControl(token, "/live-question", { questionId });
}