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
223 changes: 218 additions & 5 deletions server/api/routes/firebaseAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ 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";

router.post("/autosave", async (req, res) => {
// AUDIENCE COLLECTIONS
const AUDIENCE_COLLECTION = "audience";
const AUDIENCE_SURVEY_COLLECTION = "audienceSurvey";
const AUDIENCE_INCOMPLETE_SESSION_COLLECTION = "audienceIncompleteSession";

// ARTIST ROUTES
router.post("/artist/autosave", async (req, res) => {
try {
const { sessionId, data } = req.body;

Expand All @@ -32,7 +39,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,
Expand All @@ -49,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;

Expand All @@ -75,7 +84,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 = {
Expand All @@ -99,4 +108,208 @@ router.post("/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<number, string> = {
1: "captcha",
2: "consent",
3: "pre-survey",
4: "instructions",
5: "readPassage",
6: "poemEvaluation1",
7: "poemEvaluation2",
8: "poemEvaluation3",
9: "poemEvaluation4",
10: "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" });
}
});

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" });
}
});

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;
64 changes: 55 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
Audience,
ArtistSurvey,
AudienceSurvey,
SurveyAnswers,
} from "./types";
import { Provider } from "./components/ui/provider";
import { Toaster } from "./components/ui/toaster";
Expand All @@ -53,6 +54,11 @@ interface DataContextValue {
addPostSurvey: (
updates: Partial<ArtistSurvey> | Partial<AudienceSurvey>
) => void;
addPoemEvaluation: (
poemId: string,
answers: SurveyAnswers,
additionalData?: Partial<Audience>
) => void;
sessionId: string | null;
flushSaves: () => Promise<void>;
}
Expand Down Expand Up @@ -81,16 +87,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/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);
};

Expand Down Expand Up @@ -128,7 +142,7 @@ function App() {
...updates,
},
};
enqueueAutosave(next as UserData);
autoSave(next as UserData);
return next;
});
};
Expand Down Expand Up @@ -158,7 +172,7 @@ function App() {
},
},
};
enqueueAutosave(next as UserData);
autoSave(next as UserData);
return next;
});
};
Expand Down Expand Up @@ -188,7 +202,38 @@ function App() {
},
},
};
enqueueAutosave(next as UserData);
autoSave(next as UserData);
return next;
});
};

const addPoemEvaluation = (
poemId: string,
answers: SurveyAnswers,
additionalData?: Partial<Audience>
) => {
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,
...additionalData,
surveyResponse: {
...prev.data.surveyResponse,
poemAnswers: [...existingPoemAnswers, poemAnswer],
},
},
};
autoSave(next as UserData);
return next;
});
};
Expand Down Expand Up @@ -223,6 +268,7 @@ function App() {
addRoleSpecificData,
addPostSurvey,
addPreSurvey,
addPoemEvaluation,
sessionId,
flushSaves,
}}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/artist/PostSurvey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading