From 78594696a40a24f14d1007d580932eb597e452b0 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:17:38 -0800 Subject: [PATCH 01/12] remove audience conditions --- src/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/types.ts b/src/types.ts index 85f3ca4..1052904 100644 --- a/src/types.ts +++ b/src/types.ts @@ -104,13 +104,6 @@ export interface Passage { text: string; } -// export const AudienceCondition = { -// NO_KNOWLEDGE: "NO_KNOWLEDGE", -// FULL_TRANSPARENCY: "FULL_TRANSPARENCY", -// } as const; -// export type AudienceCondition = -// (typeof AudienceCondition)[keyof typeof AudienceCondition]; - export type QuestionType = | "multipleChoice" | "openEnded" From 3945e43b63cd9b0e90676e07c7227aa6ad95fe8a Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:21:21 -0800 Subject: [PATCH 02/12] create audience collections --- server/api/routes/firebaseAPI.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index c21fe19..7855557 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -3,10 +3,16 @@ import { db, FieldValue } from "../firebase/firebase"; const router = express.Router(); +// ARTIST COLLECTIONS const ARTIST_COLLECTION = "artist"; const ARTIST_SURVEY_COLLECTION = "artistSurvey"; const POEM_COLLECTION = "poem"; -const INCOMPLETE_SESSION_COLLECTION = "incompleteSession"; +const ARTIST_INCOMPLETE_SESSION_COLLECTION = "artistIncompleteSession"; + +// AUDIENCE COLLECTIONS +const AUDIENCE_COLLECTION = "audience"; +const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey"; +const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession"; router.post("/autosave", async (req, res) => { try { @@ -32,7 +38,9 @@ router.post("/autosave", async (req, res) => { ? statusMap[data.data.timeStamps.length] || "started" : "started"; - const ref = db.collection(INCOMPLETE_SESSION_COLLECTION).doc(sessionId); + const ref = db + .collection(ARTIST_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); const payload = { sessionId, role: data.role, @@ -75,7 +83,7 @@ router.post("/commit-session", async (req, res) => { const surveyRef = db.collection(ARTIST_SURVEY_COLLECTION).doc(); const poemRef = db.collection(POEM_COLLECTION).doc(); const incompleteRef = db - .collection(INCOMPLETE_SESSION_COLLECTION) + .collection(ARTIST_INCOMPLETE_SESSION_COLLECTION) .doc(sessionId); const artist = { From db5e525145a894fac4210977321f92df8f2f76b1 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:24:42 -0800 Subject: [PATCH 03/12] set correct path --- server/api/routes/firebaseAPI.ts | 7 +++++-- src/App.tsx | 2 +- src/pages/artist/PostSurvey.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 7855557..afc3924 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -14,7 +14,8 @@ const AUDIENCE_COLLECTION = "audience"; const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey"; const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession"; -router.post("/autosave", async (req, res) => { +// ARTIST ROUTES +router.post("/artist/autosave", async (req, res) => { try { const { sessionId, data } = req.body; @@ -57,7 +58,7 @@ router.post("/autosave", async (req, res) => { } }); -router.post("/commit-session", async (req, res) => { +router.post("/artist/commit-session", async (req, res) => { try { const { artistData, surveyData, poemData, sessionId } = req.body; @@ -107,4 +108,6 @@ router.post("/commit-session", async (req, res) => { } }); +// AUDIENCE ROUTES + export default router; diff --git a/src/App.tsx b/src/App.tsx index 27576ac..e5869a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -86,7 +86,7 @@ function App() { if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(async () => { - await fetch("/api/firebase/autosave", { + await fetch("/api/firebase/artist/autosave", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, data }), diff --git a/src/pages/artist/PostSurvey.tsx b/src/pages/artist/PostSurvey.tsx index 48c858a..8649cd2 100644 --- a/src/pages/artist/PostSurvey.tsx +++ b/src/pages/artist/PostSurvey.tsx @@ -48,7 +48,7 @@ const ArtistPostSurvey = () => { // SEND IT RAHHHH try { - await fetch("/api/firebase/commit-session", { + await fetch("/api/firebase/artist/commit-session", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ From fa796d2ca87ac995837811d3da763b04fc70a68a Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Thu, 8 Jan 2026 16:34:57 -0800 Subject: [PATCH 04/12] audience routes first draft --- server/api/routes/firebaseAPI.ts | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index afc3924..2db1d85 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -109,5 +109,89 @@ router.post("/artist/commit-session", async (req, res) => { }); // AUDIENCE ROUTES +router.post("/audience/autosave", async (req, res) => { + try { + const { sessionId, data } = req.body; + + if (!sessionId || !data) { + return res + .status(400) + .json({ error: "Missing sessionId or data objects" }); + } + + const statusMap: Record = { + 1: "captcha", + 2: "consent", + 3: "pre-survey", + 4: "instructions", + 5: "passage", + 6: "poems", + 7: "post-survey", + }; + + const status = data.data?.timeStamps + ? statusMap[data.data.timeStamps.length] || "started" + : "started"; + + const ref = db + .collection(AUDIENCE_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); + const payload = { + sessionId, + role: data.role, + partialData: data.data, + lastUpdated: FieldValue.serverTimestamp(), + completionStatus: status, + }; + + await ref.set(payload, { merge: true }); + res.json({ success: true }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to autosave" }); + } +}); + +router.post("/audience/commit-session", async (req, res) => { + try { + const { audienceData, surveyData, sessionId } = req.body; + + if (!audienceData) { + return res.status(400).json({ error: "Missing audienceData" }); + } + + if (!surveyData) { + return res.status(400).json({ error: "Missing surveyData" }); + } + + if (!sessionId) { + return res.status(400).json({ error: "Missing sessionId" }); + } + + const batch = db.batch(); + + const audienceRef = db.collection(AUDIENCE_COLLECTION).doc(); + const surveyRef = db.collection(AUDIENCE_SURVEY_COLLECTION).doc(); + const incompleteRef = db + .collection(AUDIENCE_INCOMPLETE_SESSION_COLLECTION) + .doc(sessionId); + + const audience = { + surveyResponse: surveyRef, + timestamps: [...(audienceData.timeStamps ?? []), new Date()], + }; + + batch.set(audienceRef, audience); + batch.set(surveyRef, { audienceId: audienceRef.id, ...surveyData }); + batch.delete(incompleteRef); + + await batch.commit(); + + res.json({ success: true, audienceId: audienceRef.id }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Batch commit failed" }); + } +}); export default router; From 2703db7cc371a87ceb1083ad9ad2eab1e88b38da Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:00:58 -0500 Subject: [PATCH 05/12] status updates --- server/api/routes/firebaseAPI.ts | 9 ++++++--- src/App.tsx | 26 +++++++++++++++++--------- src/pages/audience/step2/Step2.tsx | 6 +++--- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 2db1d85..4e01532 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -124,9 +124,12 @@ router.post("/audience/autosave", async (req, res) => { 2: "consent", 3: "pre-survey", 4: "instructions", - 5: "passage", - 6: "poems", - 7: "post-survey", + 5: "readPassage", + 6: "poemEvaluation1", + 7: "poemEvaluation2", + 8: "poemEvaluation3", + 9: "poemEvaluation4", + 10: "post-survey", }; const status = data.data?.timeStamps diff --git a/src/App.tsx b/src/App.tsx index e5869a1..712e74e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,16 +81,24 @@ function App() { setSessionId(id); }, []); - const enqueueAutosave = (data: UserData | null) => { + const autoSave = (data: UserData | null) => { if (!data || !sessionId) return; if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current); saveTimerRef.current = window.setTimeout(async () => { - await fetch("/api/firebase/artist/autosave", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sessionId, data }), - }); + if (data.role === "artist") { + await fetch("/api/firebase/artist/autosave", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId, data }), + }); + } else { + await fetch("/api/firebase/audience/autosave", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId, data }), + }); + } }, 500); }; @@ -128,7 +136,7 @@ function App() { ...updates, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -158,7 +166,7 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; @@ -188,7 +196,7 @@ function App() { }, }, }; - enqueueAutosave(next as UserData); + autoSave(next as UserData); return next; }); }; diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index cf038d2..bcb9328 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -56,6 +56,9 @@ const AudiencePoems = () => { }, []); const handleSubmit = () => { + addRoleSpecificData({ + timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], + }); if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( @@ -68,9 +71,6 @@ const AudiencePoems = () => { } return; } - addRoleSpecificData({ - timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], - }); navigate("/audience/passage"); }; From a90207043b1e888d8915338f1c5f4f6a2fa4bbaf Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:23:48 -0500 Subject: [PATCH 06/12] add poem data --- src/App.tsx | 29 +++++++++++++++++++++++++++++ src/pages/audience/step1/Step1.tsx | 1 + src/pages/audience/step2/Step2.tsx | 8 ++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 712e74e..4c582cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import type { Audience, ArtistSurvey, AudienceSurvey, + SurveyAnswers, } from "./types"; import { Provider } from "./components/ui/provider"; import { Toaster } from "./components/ui/toaster"; @@ -53,6 +54,7 @@ interface DataContextValue { addPostSurvey: ( updates: Partial | Partial ) => void; + addPoemEvaluation: (poemId: string, answers: SurveyAnswers) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -201,6 +203,32 @@ function App() { }); }; + const addPoemEvaluation = (poemId: string, answers: SurveyAnswers) => { + setUserData((prev: any) => { + if (!prev || !prev.data) { + throw new Error( + "Tried to update poem evaluation when userData is null." + ); + } + + const poemAnswer = { poemId, ...answers }; + const existingPoemAnswers = prev.data.surveyResponse?.poemAnswers ?? []; + + const next = { + ...prev, + data: { + ...prev.data, + surveyResponse: { + ...prev.data.surveyResponse, + poemAnswers: [...existingPoemAnswers, poemAnswer], + }, + }, + }; + autoSave(next as UserData); + return next; + }); + }; + // Flush saves on tab hide/close useEffect(() => { const onVisibility = () => { @@ -231,6 +259,7 @@ function App() { addRoleSpecificData, addPostSurvey, addPreSurvey, + addPoemEvaluation, sessionId, flushSaves, }} diff --git a/src/pages/audience/step1/Step1.tsx b/src/pages/audience/step1/Step1.tsx index abb3e27..94b4778 100644 --- a/src/pages/audience/step1/Step1.tsx +++ b/src/pages/audience/step1/Step1.tsx @@ -20,6 +20,7 @@ const AudiencePassage = () => { const handleSubmit = () => { addRoleSpecificData({ + passageId: passageId, timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/audience/poems"); diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index bcb9328..dae03c1 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -9,6 +9,7 @@ import { AudiencePoemQuestions } from "../../../consts/surveyQuestions"; import { Button } from "@chakra-ui/react"; import { LuEyeClosed } from "react-icons/lu"; import { HiOutlineDocumentText } from "react-icons/hi2"; +import type { SurveyAnswers } from "../../../types"; const AudiencePoems = () => { const [currPoem, setCurrPoem] = useState(0); @@ -22,7 +23,7 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData } = context; + const { userData, addRoleSpecificData, addPoemEvaluation } = context; const passageId = (userData as any)?.data?.passage || "1"; @@ -55,10 +56,13 @@ const AudiencePoems = () => { } }, []); - const handleSubmit = () => { + const handleSubmit = (answers: SurveyAnswers) => { + addPoemEvaluation(String(currPoem), answers); + addRoleSpecificData({ timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); + if (currPoem < poems.length - 1) { setCurrPoem(currPoem + 1); const container = document.querySelector( From 55be15b35a71eb2be6ee4f2abaf4a95923a30e5e Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:33:26 -0500 Subject: [PATCH 07/12] poem fixes --- src/App.tsx | 15 ++++++++++++--- src/pages/audience/step2/Step2.tsx | 6 ++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4c582cb..35a0b73 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -54,7 +54,11 @@ interface DataContextValue { addPostSurvey: ( updates: Partial | Partial ) => void; - addPoemEvaluation: (poemId: string, answers: SurveyAnswers) => void; + addPoemEvaluation: ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => void; sessionId: string | null; flushSaves: () => Promise; } @@ -203,7 +207,11 @@ function App() { }); }; - const addPoemEvaluation = (poemId: string, answers: SurveyAnswers) => { + const addPoemEvaluation = ( + poemId: string, + answers: SurveyAnswers, + additionalData?: Partial + ) => { setUserData((prev: any) => { if (!prev || !prev.data) { throw new Error( @@ -218,10 +226,11 @@ function App() { ...prev, data: { ...prev.data, + ...additionalData, surveyResponse: { ...prev.data.surveyResponse, - poemAnswers: [...existingPoemAnswers, poemAnswer], }, + poemAnswers: [...existingPoemAnswers, poemAnswer], }, }; autoSave(next as UserData); diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index dae03c1..0fa6f8f 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -23,7 +23,7 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addRoleSpecificData, addPoemEvaluation } = context; + const { userData, addPoemEvaluation } = context; const passageId = (userData as any)?.data?.passage || "1"; @@ -57,9 +57,7 @@ const AudiencePoems = () => { }, []); const handleSubmit = (answers: SurveyAnswers) => { - addPoemEvaluation(String(currPoem), answers); - - addRoleSpecificData({ + addPoemEvaluation(String(currPoem), answers, { timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); From 19f4aaa152a262875db9e5ceaabaee75e0adaa66 Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 12:46:00 -0500 Subject: [PATCH 08/12] poem eval fix --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 35a0b73..33bf26d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -229,8 +229,8 @@ function App() { ...additionalData, surveyResponse: { ...prev.data.surveyResponse, + poemAnswers: [...existingPoemAnswers, poemAnswer], }, - poemAnswers: [...existingPoemAnswers, poemAnswer], }, }; autoSave(next as UserData); From a735d4b054ab68972218f7a41b9d7a88d292606f Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 15:48:37 -0500 Subject: [PATCH 09/12] randomize passages --- src/pages/audience/AudienceCaptcha.tsx | 9 +++++++++ src/pages/audience/step1/Step1.tsx | 2 +- src/pages/audience/step2/Step2.tsx | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/audience/AudienceCaptcha.tsx b/src/pages/audience/AudienceCaptcha.tsx index a5bacdf..a44a714 100644 --- a/src/pages/audience/AudienceCaptcha.tsx +++ b/src/pages/audience/AudienceCaptcha.tsx @@ -4,6 +4,7 @@ import HalfPageTemplate from "../../components/shared/pages/halfPage"; import { Button, Input } from "@chakra-ui/react"; import { toaster } from "../../components/ui/toaster"; import { DataContext } from "../../App"; +import { Passages } from "../../consts/passages"; const TEST_CAPTCHA = "*TEST"; const Captcha = () => { @@ -35,6 +36,12 @@ const Captcha = () => { setCaptchaMessage(captcha_text); }; + const getRandomPassage = () => { + const numPassages = Passages.length; + const randomIndex = Math.floor(Math.random() * numPassages); + return randomIndex.toString(); + }; + useEffect(() => { if (canvasRef.current) { const canvas = canvasRef.current; @@ -71,12 +78,14 @@ const Captcha = () => { if (inputCaptcha === captchaMessage) { addUserData({ role: "audience" }); addRoleSpecificData({ + passageId: getRandomPassage(), timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/consent"); } else if (inputCaptcha == TEST_CAPTCHA) { addUserData({ role: "audience" }); addRoleSpecificData({ + passageId: getRandomPassage(), timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); navigate("/consent"); diff --git a/src/pages/audience/step1/Step1.tsx b/src/pages/audience/step1/Step1.tsx index 94b4778..c4bc142 100644 --- a/src/pages/audience/step1/Step1.tsx +++ b/src/pages/audience/step1/Step1.tsx @@ -14,7 +14,7 @@ const AudiencePassage = () => { const { userData, addRoleSpecificData } = context; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index 0fa6f8f..0e3790e 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -25,7 +25,7 @@ const AudiencePoems = () => { const { userData, addPoemEvaluation } = context; - const passageId = (userData as any)?.data?.passage || "1"; + const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); From afc4421e949a68ea38521e581735668485a129bf Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 16:02:19 -0500 Subject: [PATCH 10/12] get poems endpoint --- server/api/routes/firebaseAPI.ts | 40 ++++++++++++++++++++++++++++++++ src/types.ts | 13 +---------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 4e01532..9b992f9 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -197,4 +197,44 @@ router.post("/audience/commit-session", async (req, res) => { } }); +router.get("/audience/poems", async (req, res) => { + try { + const { passageId } = req.query; + + if (!passageId || typeof passageId !== "string") { + return res.status(400).json({ error: "Missing or invalid passageId" }); + } + + // query all poems with the given passageId + const snapshot = await db + .collection(POEM_COLLECTION) + .where("passageId", "==", passageId) + .get(); + + if (snapshot.empty) { + return res.status(404).json({ error: "No poems found for this passage" }); + } + + // map to { poemId, text } format + const allPoems = snapshot.docs.map((doc) => ({ + poemId: doc.id, + text: doc.data().text as number[], + })); + + // 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]]; + } + + // 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" }); + } +}); + export default router; diff --git a/src/types.ts b/src/types.ts index 1052904..246e8bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,17 +14,6 @@ export interface ArtistSurvey { postAnswers: SurveyAnswers; } -export interface AudiencePoem { - id: string; - poemId: string; -} - -// export interface SurveyQuestion { -// id: string; -// q: string; -// answerType: -// } - export interface Poem { passageId: string; // passageId in Passage.id passage: Passage; @@ -75,7 +64,7 @@ export type Role = (typeof Role)[keyof typeof Role]; export interface Audience { passageId: string; surveyResponse: AudienceSurvey; - poemsViewed: AudiencePoem[]; + poemsViewed: string[]; timeStamps: Date[]; } From ff758911a6fff5ba2b0257f94c7eb1a03bd9de4f Mon Sep 17 00:00:00 2001 From: Sarah Wang Date: Sun, 11 Jan 2026 16:25:17 -0500 Subject: [PATCH 11/12] get randomized poems --- src/pages/audience/step2/Step2.tsx | 76 +++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/pages/audience/step2/Step2.tsx b/src/pages/audience/step2/Step2.tsx index 0e3790e..bb433d8 100644 --- a/src/pages/audience/step2/Step2.tsx +++ b/src/pages/audience/step2/Step2.tsx @@ -1,20 +1,28 @@ import PageTemplate from "../../../components/shared/pages/audiencePages/scrollFullPage"; import { useNavigate } from "react-router-dom"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { DataContext } from "../../../App"; import { Passages } from "../../../consts/passages"; -import { Poems } from "../../../consts/poems"; import SurveyScroll from "../../../components/survey/surveyScroll"; import { AudiencePoemQuestions } from "../../../consts/surveyQuestions"; -import { Button } from "@chakra-ui/react"; +import { Button, Spinner } from "@chakra-ui/react"; import { LuEyeClosed } from "react-icons/lu"; import { HiOutlineDocumentText } from "react-icons/hi2"; import type { SurveyAnswers } from "../../../types"; +interface FetchedPoem { + poemId: string; + text: number[]; +} + const AudiencePoems = () => { const [currPoem, setCurrPoem] = useState(0); const [showScrollTop, setShowScrollTop] = useState(false); const [showPassage, setShowPassage] = useState(false); + const [poems, setPoems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const hasFetched = useRef(false); const navigate = useNavigate(); const context = useContext(DataContext); @@ -23,14 +31,44 @@ const AudiencePoems = () => { throw new Error("Component must be used within a DataContext.Provider"); } - const { userData, addPoemEvaluation } = context; + const { userData, addPoemEvaluation, addRoleSpecificData } = context; const passageId = (userData as any)?.data?.passageId || "1"; const passage = Passages.find((p) => p.id === passageId) || Passages[0]; const words = passage.text.split(" "); - const poems = Poems; + // Fetch poems from API + useEffect(() => { + if (hasFetched.current) return; + hasFetched.current = true; + + const fetchPoems = async () => { + try { + setIsLoading(true); + const response = await fetch( + `/api/firebase/audience/poems?passageId=${encodeURIComponent( + passageId + )}` + ); + if (!response.ok) { + throw new Error("Failed to fetch poems"); + } + const data = await response.json(); + setPoems(data.poems); + + // Save the poem IDs to user data + const poemIds = data.poems.map((p: FetchedPoem) => p.poemId); + addRoleSpecificData({ poemsViewed: poemIds }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load poems"); + } finally { + setIsLoading(false); + } + }; + + fetchPoems(); + }, []); useEffect(() => { const container = document.querySelector( @@ -57,7 +95,7 @@ const AudiencePoems = () => { }, []); const handleSubmit = (answers: SurveyAnswers) => { - addPoemEvaluation(String(currPoem), answers, { + addPoemEvaluation(poems[currPoem].poemId, answers, { timeStamps: [...(userData?.data?.timeStamps ?? []), new Date()], }); @@ -76,6 +114,32 @@ const AudiencePoems = () => { navigate("/audience/passage"); }; + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + if (error || poems.length === 0) { + return ( + +
+ {error || "No poems available for this passage"} +
+
+ ); + } + return ( Date: Sun, 11 Jan 2026 16:41:02 -0500 Subject: [PATCH 12/12] get artist statements - consider refactoring db --- server/api/routes/firebaseAPI.ts | 75 ++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/server/api/routes/firebaseAPI.ts b/server/api/routes/firebaseAPI.ts index 9b992f9..eaa6afc 100644 --- a/server/api/routes/firebaseAPI.ts +++ b/server/api/routes/firebaseAPI.ts @@ -237,4 +237,79 @@ router.get("/audience/poems", async (req, res) => { } }); +router.post("/audience/artist-statements", async (req, res) => { + try { + const { poemIds } = req.body; + + if (!poemIds || !Array.isArray(poemIds) || poemIds.length === 0) { + return res + .status(400) + .json({ error: "Missing or invalid poemIds array" }); + } + + // Get statements for the requested poem IDs + const poemStatements = await Promise.all( + poemIds.map((id: string) => getArtistStatement(id)) + ); + + // Get all poems to find 4 random other statements + const allPoemsSnapshot = await db.collection(POEM_COLLECTION).get(); + const requestedPoemIdSet = new Set(poemIds); + + // Filter out the requested poems + const otherPoemIds = allPoemsSnapshot.docs + .map((doc) => doc.id) + .filter((id) => !requestedPoemIdSet.has(id)); + + // Fisher-Yates shuffle for random selection + for (let i = otherPoemIds.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [otherPoemIds[i], otherPoemIds[j]] = [otherPoemIds[j], otherPoemIds[i]]; + } + + // Get statements for 4 random other poems + const randomPoemIds = otherPoemIds.slice(0, 4); + const randomStatementsResults = await Promise.all( + randomPoemIds.map((id) => getArtistStatement(id)) + ); + const randomStatements = randomStatementsResults + .filter((s): s is { poemId: string; statement: string } => s !== null) + .map((s) => s.statement); + + res.json({ + poemStatements, + randomStatements, + }); + } catch (error) { + console.error(error); + res.status(500).json({ error: "Failed to get artist statements" }); + } +}); + +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;