From 1813de499d06a4ff6a2024b4758bd1d8922e20a5 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 8 May 2023 10:44:16 +0100 Subject: [PATCH] Started to update the exam to work with Zustand for the history review --- src/interfaces/exam.ts | 8 +++++ src/pages/api/exam/[module]/[id].ts | 31 +++++++++++++++++++ .../exam/{[module].ts => [module]/index.ts} | 0 src/pages/exam/index.tsx | 31 ++++++++++++++----- src/stores/examStore.ts | 21 ++++++++++++- src/utils/stats.ts | 5 +++ 6 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/pages/api/exam/[module]/[id].ts rename src/pages/api/exam/{[module].ts => [module]/index.ts} (100%) diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 9bfdf047..ea4102f4 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -69,6 +69,10 @@ export interface WritingExercise { info: string; //* The information about the task, like the amount of time they should spend on it prompt: string; //* The context given to the user containing what they should write about wordCounter: WordCounter; //* The minimum or maximum amount of words that should be written + userSolutions: { + id: string; + solution: string; + }[]; } export interface SpeakingExercise { @@ -77,6 +81,10 @@ export interface SpeakingExercise { title: string; text: string; prompts: string[]; + userSolutions: { + id: string; + solution: string; + }[]; } export interface FillBlanksExercise { diff --git a/src/pages/api/exam/[module]/[id].ts b/src/pages/api/exam/[module]/[id].ts new file mode 100644 index 00000000..c1d5110e --- /dev/null +++ b/src/pages/api/exam/[module]/[id].ts @@ -0,0 +1,31 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type {NextApiRequest, NextApiResponse} from "next"; +import {app} from "@/firebase"; +import {getFirestore, doc, getDoc} from "firebase/firestore"; +import {withIronSessionApiRoute} from "iron-session/next"; +import {sessionOptions} from "@/lib/session"; + +const db = getFirestore(app); + +export default withIronSessionApiRoute(handler, sessionOptions); + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {module, id} = req.query as {module: string; id: string}; + + const docRef = doc(db, module, id); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + res.status(200).json({ + id: docSnap.id, + ...docSnap.data(), + }); + } else { + res.status(404).json(undefined); + } +} diff --git a/src/pages/api/exam/[module].ts b/src/pages/api/exam/[module]/index.ts similarity index 100% rename from src/pages/api/exam/[module].ts rename to src/pages/api/exam/[module]/index.ts diff --git a/src/pages/exam/index.tsx b/src/pages/exam/index.tsx index 28e3459f..855d233b 100644 --- a/src/pages/exam/index.tsx +++ b/src/pages/exam/index.tsx @@ -18,6 +18,7 @@ import {Stat, User} from "@/interfaces/user"; import Speaking from "@/exams/Speaking"; import {v4 as uuidv4} from "uuid"; import useUser from "@/hooks/useUser"; +import useExamStore, {ExamState} from "@/stores/examStore"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -42,30 +43,44 @@ export default function Page() { const [userSolutions, setUserSolutions] = useState([]); const [selectedModules, setSelectedModules] = useState([]); const [hasBeenUploaded, setHasBeenUploaded] = useState(false); - const [showSolutions, setShowSolutions] = useState(false); const [moduleIndex, setModuleIndex] = useState(0); const [sessionId, setSessionId] = useState(""); const [exam, setExam] = useState(); const [timer, setTimer] = useState(-1); + const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]); + const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]); + const {user} = useUser({redirectTo: "/login"}); useEffect(() => setSessionId(uuidv4()), []); useEffect(() => { (async () => { - if (selectedModules.length > 0 && moduleIndex < selectedModules.length) { - const nextExam = await getExam(selectedModules[moduleIndex]); + if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { + const nextExam = exams[moduleIndex]; setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModules, moduleIndex]); + }, [selectedModules, moduleIndex, exams]); + + useEffect(() => { + async () => { + if (selectedModules.length > 0) { + const examPromises = selectedModules.map(getExam); + Promise.all(examPromises).then((values) => { + if (values.every((x) => !!x)) { + } + }); + } + }; + }, [selectedModules, setExams]); useEffect(() => { (async () => { if (selectedModules.length > 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded) { - const stats: Stat[] = userSolutions.map((solution) => ({ + const newStats: Stat[] = userSolutions.map((solution) => ({ ...solution, session: sessionId, exam: solution.exam!, @@ -75,7 +90,7 @@ export default function Page() { })); axios - .post<{ok: boolean}>("/api/stats", stats) + .post<{ok: boolean}>("/api/stats", newStats) .then((response) => setHasBeenUploaded(response.data.ok)) .catch(() => setHasBeenUploaded(false)); } @@ -116,7 +131,9 @@ export default function Page() { }; const updateExamWithUserSolutions = (exam: Exam): Exam => { - const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions})); + const exercises = exam.exercises.map((x) => + Object.assign(x, !x.userSolutions ? {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions} : x.userSolutions), + ); return Object.assign(exam, exercises); }; diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index 8cd2ee09..4383c860 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -1,6 +1,25 @@ import {Module} from "@/interfaces"; +import {Exam} from "@/interfaces/exam"; +import {Stat} from "@/interfaces/user"; +import {getExamsBySession} from "@/utils/stats"; import {create} from "zustand"; -const useExamStore = create((set) => ({})); +export interface ExamState { + exams: Exam[]; + stats: Stat[]; + showSolutions: boolean; + setStats: (stats: Stat[]) => void; + setExams: (exams: Exam[]) => void; + setShowSolutions: (showSolutions: boolean) => void; +} + +const useExamStore = create((set) => ({ + exams: [], + stats: [], + showSolutions: false, + setStats: (stats: Stat[]) => set(() => ({stats})), + setExams: (exams: Exam[]) => set(() => ({exams})), + setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})), +})); export default useExamStore; diff --git a/src/utils/stats.ts b/src/utils/stats.ts index dca0dcbf..3f8b445c 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -91,5 +91,10 @@ export const formatExerciseAverageScoreStats = (stats: Stat[]): {label: string; }); }; +export const getExamsBySession = (stats: Stat[], session: string) => { + const grouped = groupBySession(stats); + return grouped[session].map((exam) => exam.exam); +}; + export const groupBySession = (stats: Stat[]) => groupBy(stats, "session"); export const groupByDate = (stats: Stat[]) => groupBy(stats, "date");