Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 59 additions & 71 deletions server/api/routes/firebaseAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
24 changes: 24 additions & 0 deletions server/api/scripts/backfill-random.ts
Original file line number Diff line number Diff line change
@@ -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);
13 changes: 7 additions & 6 deletions src/pages/audience/step2/Step2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { SurveyAnswers } from "../../../types";
interface FetchedPoem {
poemId: string;
text: number[];
statement: string;
}

const AudiencePoems = () => {
Expand Down Expand Up @@ -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");
Expand All @@ -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 {
Expand All @@ -72,7 +73,7 @@ const AudiencePoems = () => {

useEffect(() => {
const container = document.querySelector(
".overflow-y-auto"
".overflow-y-auto",
) as HTMLElement | null;
const onScroll = () => {
if (container) {
Expand Down Expand Up @@ -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" });
Expand Down Expand Up @@ -198,7 +199,7 @@ const AudiencePoems = () => {
<button
onClick={() => {
const container = document.querySelector(
".overflow-y-auto"
".overflow-y-auto",
) as HTMLElement | null;
if (container) {
container.scrollTo({ top: 0, behavior: "smooth" });
Expand Down
Loading