134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import {
|
|
subscribeSessionQuestions,
|
|
type RtdbQuestionItem,
|
|
} from "../lib/sessionQuestionsRtdb";
|
|
import { getVisitorSessionQuestions, type VisitorSessionQuestion } from "../lib/api";
|
|
import { isFirebaseClientConfigured } from "../lib/firebaseClient";
|
|
|
|
const API_POLL_MS = 4000;
|
|
|
|
function isFirebasePermissionError(message: string): boolean {
|
|
return /permission_denied|PERMISSION_DENIED|permission denied/i.test(message);
|
|
}
|
|
|
|
function apiQuestionsToRtdbItems(rows: VisitorSessionQuestion[]): RtdbQuestionItem[] {
|
|
return rows
|
|
.map((q) => ({
|
|
_id: String(q._id),
|
|
author: String(q.author ?? "Anonymous"),
|
|
text: String(q.text ?? ""),
|
|
up: typeof q.up === "number" ? q.up : Number(q.up) || 0,
|
|
down: typeof q.down === "number" ? q.down : Number(q.down) || 0,
|
|
createdAt: q.createdAt != null ? String(q.createdAt) : undefined,
|
|
status: "pending",
|
|
audienceListed: true,
|
|
}))
|
|
.sort((a, b) => {
|
|
if (b.up !== a.up) return b.up - a.up;
|
|
const ta = a.createdAt ? Date.parse(a.createdAt) : 0;
|
|
const tb = b.createdAt ? Date.parse(b.createdAt) : 0;
|
|
return tb - ta;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Live session questions for attendees.
|
|
* - Tries Firebase RTDB when configured.
|
|
* - On `permission_denied` (rules require auth), falls back to GET /api/visitor/sessions/:id/questions with polling.
|
|
*/
|
|
export function useSessionQuestionsRtdb(
|
|
sessionId: string | null | undefined,
|
|
eventId?: string | null,
|
|
) {
|
|
const [items, setItems] = useState<RtdbQuestionItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const sid = sessionId?.trim() ?? "";
|
|
const eid = eventId?.trim() ?? "";
|
|
|
|
useEffect(() => {
|
|
if (!sid || !eid) {
|
|
setItems([]);
|
|
setLoading(false);
|
|
setError(
|
|
sid && !eid
|
|
? "event_id is required to load questions."
|
|
: null,
|
|
);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
let unsubRtdb: (() => void) | null = null;
|
|
let usingApi = false;
|
|
|
|
const loadViaApi = async () => {
|
|
try {
|
|
const rows = await getVisitorSessionQuestions(sid, eid);
|
|
if (cancelled) return;
|
|
setItems(apiQuestionsToRtdbItems(rows));
|
|
setLoading(false);
|
|
setError(null);
|
|
} catch (e) {
|
|
if (cancelled) return;
|
|
setError(e instanceof Error ? e.message : "Could not load questions.");
|
|
setItems([]);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const startApiPolling = () => {
|
|
if (usingApi) return;
|
|
usingApi = true;
|
|
if (unsubRtdb) {
|
|
unsubRtdb();
|
|
unsubRtdb = null;
|
|
}
|
|
void loadViaApi();
|
|
pollTimer = setInterval(() => {
|
|
if (cancelled || document.visibilityState !== "visible") return;
|
|
void loadViaApi();
|
|
}, API_POLL_MS);
|
|
};
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
if (!isFirebaseClientConfigured()) {
|
|
startApiPolling();
|
|
} else {
|
|
unsubRtdb = subscribeSessionQuestions(
|
|
sid,
|
|
(list) => {
|
|
if (cancelled || usingApi) return;
|
|
setItems(list);
|
|
setLoading(false);
|
|
setError(null);
|
|
},
|
|
(err) => {
|
|
if (cancelled) return;
|
|
const msg = err.message || String(err);
|
|
if (isFirebasePermissionError(msg)) {
|
|
startApiPolling();
|
|
return;
|
|
}
|
|
setError(msg);
|
|
setItems([]);
|
|
setLoading(false);
|
|
},
|
|
);
|
|
}
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (unsubRtdb) unsubRtdb();
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
};
|
|
}, [sid, eid]);
|
|
|
|
return { items, loading, error };
|
|
}
|