diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 5f8fda9..7072cca 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -96,7 +96,11 @@ router.post("/artist/commit-session", async (req, res) => { batch.set(artistRef, artist); batch.set(surveyRef, { artistId: artistRef.id, ...surveyData }); - batch.set(poemRef, { artistId: artistRef.id, ...poemData }); + batch.set(poemRef, { + artistId: artistRef.id, + ...poemData, + random: Math.random(), + }); batch.delete(incompleteRef); await batch.commit(); @@ -181,14 +185,13 @@ router.post("/audience/commit-session", async (req, res) => { // Main audience document with references and metadata const existingTimestamps = (audienceData.timeStamps ?? []).map( - (ts: string | Date) => new Date(ts) + (ts: string | Date) => new Date(ts), ); const audience = { passageId: audienceData.passageId, poemsViewed: audienceData.poemsViewed ?? [], surveyResponse: surveyRef, timestamps: [...existingTimestamps, new Date()], - }; // Survey document with all survey responses @@ -223,90 +226,75 @@ router.get("/audience/poems", async (req, res) => { return res.status(400).json({ error: "Missing or invalid passageId" }); } - // query all poems with the given passageId - const snapshot = await db + const rand = Math.random(); + const POEM_LIMIT = 4; + + // First query: random >= rand + const firstSnapshot = await db .collection(POEM_COLLECTION) .where("passageId", "==", passageId) + .where("random", ">=", rand) + .orderBy("random") + .limit(POEM_LIMIT) .get(); - if (snapshot.empty) { - return res.status(404).json({ error: "No poems found for this passage" }); - } + let poemDocs = firstSnapshot.docs; - // map to { poemId, text } format - const allPoems = snapshot.docs.map((doc) => ({ - poemId: doc.id, - text: doc.data().text as number[], - })); + // Fallback query if fewer than 4 results + if (poemDocs.length < POEM_LIMIT) { + const remaining = POEM_LIMIT - poemDocs.length; + const fallbackSnapshot = await db + .collection(POEM_COLLECTION) + .where("passageId", "==", passageId) + .where("random", "<", rand) + .orderBy("random") + .limit(remaining) + .get(); - // Fisher-Yates shuffle for true randomness - for (let i = allPoems.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [allPoems[i], allPoems[j]] = [allPoems[j], allPoems[i]]; + poemDocs = [...poemDocs, ...fallbackSnapshot.docs]; } - // Take first 4 (or fewer if not enough poems exist) - const randomPoems = allPoems.slice(0, 4); - - res.json({ poems: randomPoems }); - } catch (error) { - console.error(error); - res.status(500).json({ error: "Failed to get poems" }); - } -}); - -router.post("/audience/artist-statements", async (req, res) => { - try { - const { poemIds } = req.body; + if (poemDocs.length < POEM_LIMIT) { + console.warn( + `[audience/poems] Only ${poemDocs.length} poems found for passageId: ${passageId}`, + ); + } - if (!poemIds || !Array.isArray(poemIds) || poemIds.length === 0) { - return res - .status(400) - .json({ error: "Missing or invalid poemIds array" }); + if (poemDocs.length === 0) { + return res.status(404).json({ error: "No poems found for this passage" }); } - // Get statements for the requested poem IDs - const poemStatements = await Promise.all( - poemIds.map((id: string) => getArtistStatement(id)) + // Fetch artist statements for each poem + const poems = await Promise.all( + poemDocs.map(async (doc) => { + const data = doc.data(); + const artistId = data.artistId; + + let statement: string = ""; + if (artistId) { + const surveySnapshot = await db + .collection(ARTIST_SURVEY_COLLECTION) + .where("artistId", "==", artistId) + .limit(1) + .get(); + + statement = + surveySnapshot.docs[0]?.data()?.postSurveyAnswers?.q14 ?? ""; + } + + return { + poemId: doc.id, + text: data.text as number[], + statement, + }; + }), ); - // Fisher-Yates shuffle to randomize statement order - for (let i = poemStatements.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [poemStatements[i], poemStatements[j]] = [poemStatements[j], poemStatements[i]]; - } - - res.json({ poemStatements }); + res.json({ poems }); } catch (error) { console.error(error); - res.status(500).json({ error: "Failed to get artist statements" }); + res.status(500).json({ error: "Failed to get poems" }); } }); -const getArtistStatement = async ( - poemId: string -): Promise<{ poemId: string; statement: string } | null> => { - // 1. get artistId from poemId - const poemDoc = await db.collection(POEM_COLLECTION).doc(poemId).get(); - if (!poemDoc.exists) return null; - - const artistId = poemDoc.data()?.artistId; - if (!artistId) return null; - - // 2. query survey collection for matching artistId - const surveySnapshot = await db - .collection(ARTIST_SURVEY_COLLECTION) - .where("artistId", "==", artistId) - .limit(1) - .get(); - - if (surveySnapshot.empty) return null; - - // 3. extract q14 from postAnswers - const statement = surveySnapshot.docs[0].data()?.postSurveyAnswers.q14; - if (!statement) return null; - - return { poemId, statement }; -}; - export default router; diff --git a/server/api/scripts/backfill-random.ts b/server/api/scripts/backfill-random.ts new file mode 100644 index 0000000..622ccfc --- /dev/null +++ b/server/api/scripts/backfill-random.ts @@ -0,0 +1,24 @@ +import { db } from "../firebase/firebase"; + +async function backfillRandom() { + const snapshot = await db.collection("poem").get(); + + const batch = db.batch(); + let count = 0; + + snapshot.docs.forEach((doc) => { + if (doc.data().random === undefined) { + batch.update(doc.ref, { random: Math.random() }); + count++; + } + }); + + if (count > 0) { + await batch.commit(); + console.log(`Updated ${count} poems with random field`); + } else { + console.log("All poems already have random field"); + } +} + +backfillRandom().catch(console.error); diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index c7374a6..bba8c16 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -13,6 +13,7 @@ import type { SurveyAnswers } from "../../../types"; interface FetchedPoem { poemId: string; text: number[]; + statement: string; } const AudiencePoems = () => { @@ -48,8 +49,8 @@ const AudiencePoems = () => { setIsLoading(true); const response = await fetch( `/api/firebase/audience/poems?passageId=${encodeURIComponent( - passageId - )}` + passageId, + )}`, ); if (!response.ok) { throw new Error("Failed to fetch poems"); @@ -59,7 +60,7 @@ const AudiencePoems = () => { // Save the poem IDs to user data const poemIds = data.poems.map((p: FetchedPoem) => p.poemId); - addRoleSpecificData({ poemsViewed: poemIds }); + addRoleSpecificData({ poemsViewed: poemIds, poemData: data.poems }); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load poems"); } finally { @@ -72,7 +73,7 @@ const AudiencePoems = () => { useEffect(() => { const container = document.querySelector( - ".overflow-y-auto" + ".overflow-y-auto", ) as HTMLElement | null; const onScroll = () => { if (container) { @@ -102,7 +103,7 @@ const AudiencePoems = () => { if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( - ".overflow-y-auto" + ".overflow-y-auto", ) as HTMLElement | null; if (container) { container.scrollTo({ top: 0, behavior: "smooth" }); @@ -198,7 +199,7 @@ const AudiencePoems = () => {