From 01e55f970d124275ff6e72be066551c11c84f3e9 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 28 Jan 2024 18:10:56 +0000 Subject: [PATCH] Solved a bug where if two evaluations where too fast, they would overwrite each other --- src/exams/Finish.tsx | 515 ++++++++++++++----------- src/pages/(exam)/ExamPage.tsx | 683 ++++++++++++++++++++-------------- 2 files changed, 691 insertions(+), 507 deletions(-) diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index d3c76218..9be2f239 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -1,244 +1,319 @@ import Button from "@/components/Low/Button"; import ModuleTitle from "@/components/Medium/ModuleTitle"; -import {moduleResultText} from "@/constants/ielts"; -import {Module} from "@/interfaces"; -import {User} from "@/interfaces/user"; +import { moduleResultText } from "@/constants/ielts"; +import { Module } from "@/interfaces"; +import { User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import {calculateBandScore} from "@/utils/score"; +import { calculateBandScore } from "@/utils/score"; import clsx from "clsx"; import Link from "next/link"; -import {useRouter} from "next/router"; -import {Fragment, useEffect, useState} from "react"; -import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs"; -import {LevelScore} from "@/constants/ielts"; -import {getLevelScore} from "@/utils/score"; +import { useRouter } from "next/router"; +import { Fragment, useEffect, useState } from "react"; +import { + BsArrowCounterclockwise, + BsBook, + BsClipboard, + BsEyeFill, + BsHeadphones, + BsMegaphone, + BsPen, + BsShareFill, +} from "react-icons/bs"; +import { LevelScore } from "@/constants/ielts"; +import { getLevelScore } from "@/utils/score"; interface Score { - module: Module; - correct: number; - total: number; - missing: number; + module: Module; + correct: number; + total: number; + missing: number; } interface Props { - user: User; - modules: Module[]; - scores: Score[]; - isLoading: boolean; - onViewResults: () => void; + user: User; + modules: Module[]; + scores: Score[]; + isLoading: boolean; + onViewResults: () => void; } -export default function Finish({user, scores, modules, isLoading, onViewResults}: Props) { - const [selectedModule, setSelectedModule] = useState(modules[0]); - const [selectedScore, setSelectedScore] = useState(scores.find((x) => x.module === modules[0])!); +export default function Finish({ + user, + scores, + modules, + isLoading, + onViewResults, +}: Props) { + const [selectedModule, setSelectedModule] = useState(modules[0]); + const [selectedScore, setSelectedScore] = useState( + scores.find((x) => x.module === modules[0])!, + ); - const exams = useExamStore((state) => state.exams); + const exams = useExamStore((state) => state.exams); - useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]); + useEffect( + () => setSelectedScore(scores.find((x) => x.module === selectedModule)!), + [scores, selectedModule], + ); + useEffect(() => console.log(scores), [scores]); - const moduleColors: {[key in Module]: {progress: string; inner: string}} = { - reading: { - progress: "text-ielts-reading", - inner: "bg-ielts-reading-light", - }, - listening: { - progress: "text-ielts-listening", - inner: "bg-ielts-listening-light", - }, - writing: { - progress: "text-ielts-writing", - inner: "bg-ielts-writing-light", - }, - speaking: { - progress: "text-ielts-speaking", - inner: "bg-ielts-speaking-light", - }, - level: { - progress: "text-ielts-level", - inner: "bg-ielts-level-light", - }, - }; + const moduleColors: { [key in Module]: { progress: string; inner: string } } = + { + reading: { + progress: "text-ielts-reading", + inner: "bg-ielts-reading-light", + }, + listening: { + progress: "text-ielts-listening", + inner: "bg-ielts-listening-light", + }, + writing: { + progress: "text-ielts-writing", + inner: "bg-ielts-writing-light", + }, + speaking: { + progress: "text-ielts-speaking", + inner: "bg-ielts-speaking-light", + }, + level: { + progress: "text-ielts-level", + inner: "bg-ielts-level-light", + }, + }; - const getTotalExercises = () => { - const exam = exams.find((x) => x.module === selectedModule)!; - if (exam.module === "reading" || exam.module === "listening") { - return exam.parts.flatMap((x) => x.exercises).length; - } + const getTotalExercises = () => { + const exam = exams.find((x) => x.module === selectedModule)!; + if (exam.module === "reading" || exam.module === "listening") { + return exam.parts.flatMap((x) => x.exercises).length; + } - return exam.exercises.length; - }; + return exam.exercises.length; + }; - const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus); + const bandScore: number = calculateBandScore( + selectedScore.correct, + selectedScore.total, + selectedModule, + user.focus, + ); - const showLevel = (level: number) => { - if (selectedModule === "level") { - const [levelStr, grade] = getLevelScore(level); - return ( -
- {levelStr} - {grade} -
- ); - } + const showLevel = (level: number) => { + if (selectedModule === "level") { + const [levelStr, grade] = getLevelScore(level); + return ( +
+ {levelStr} + {grade} +
+ ); + } - return {level}; - }; + return {level}; + }; - return ( - <> -
- x.module === selectedModule)!.minTimer} - disableTimer - /> -
- {modules.includes("reading") && ( -
setSelectedModule("reading")} - className={clsx( - "flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-reading hover:text-white", - selectedModule === "reading" ? "bg-ielts-reading text-white" : "bg-mti-gray-smoke text-ielts-reading", - )}> - - Reading -
- )} - {modules.includes("listening") && ( -
setSelectedModule("listening")} - className={clsx( - "flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-listening hover:text-white", - selectedModule === "listening" ? "bg-ielts-listening text-white" : "bg-mti-gray-smoke text-ielts-listening", - )}> - - Listening -
- )} - {modules.includes("writing") && ( -
setSelectedModule("writing")} - className={clsx( - "flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-writing hover:text-white", - selectedModule === "writing" ? "bg-ielts-writing text-white" : "bg-mti-gray-smoke text-ielts-writing", - )}> - - Writing -
- )} - {modules.includes("speaking") && ( -
setSelectedModule("speaking")} - className={clsx( - "flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-speaking hover:text-white", - selectedModule === "speaking" ? "bg-ielts-speaking text-white" : "bg-mti-gray-smoke text-ielts-speaking", - )}> - - Speaking -
- )} - {modules.includes("level") && ( -
setSelectedModule("level")} - className={clsx( - "flex gap-2 items-center rounded-xl p-4 cursor-pointer hover:shadow-lg transition duration-300 ease-in-out hover:bg-ielts-level hover:text-white", - selectedModule === "level" ? "bg-ielts-level text-white" : "bg-mti-gray-smoke text-ielts-level", - )}> - - Level -
- )} -
- {isLoading && ( -
- - - Evaluating your answers, please be patient... -
- You can also check it later on your records page! -
-
- )} - {!isLoading && ( -
- {moduleResultText(selectedModule, bandScore)} -
-
-
- Level - {showLevel(bandScore)} -
-
-
-
-
-
- - {(((selectedScore.total - selectedScore.missing) / selectedScore.total) * 100).toFixed(0)}% - - Completion -
-
-
-
-
- {selectedScore.correct.toString().padStart(2, "0")} - Correct -
-
-
-
-
- - {(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")} - - Wrong -
-
-
-
-
- )} -
+ return ( + <> +
+ x.module === selectedModule)!.minTimer} + disableTimer + /> +
+ {modules.includes("reading") && ( +
setSelectedModule("reading")} + className={clsx( + "hover:bg-ielts-reading flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg", + selectedModule === "reading" + ? "bg-ielts-reading text-white" + : "bg-mti-gray-smoke text-ielts-reading", + )} + > + + Reading +
+ )} + {modules.includes("listening") && ( +
setSelectedModule("listening")} + className={clsx( + "hover:bg-ielts-listening flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg", + selectedModule === "listening" + ? "bg-ielts-listening text-white" + : "bg-mti-gray-smoke text-ielts-listening", + )} + > + + Listening +
+ )} + {modules.includes("writing") && ( +
setSelectedModule("writing")} + className={clsx( + "hover:bg-ielts-writing flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg", + selectedModule === "writing" + ? "bg-ielts-writing text-white" + : "bg-mti-gray-smoke text-ielts-writing", + )} + > + + Writing +
+ )} + {modules.includes("speaking") && ( +
setSelectedModule("speaking")} + className={clsx( + "hover:bg-ielts-speaking flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg", + selectedModule === "speaking" + ? "bg-ielts-speaking text-white" + : "bg-mti-gray-smoke text-ielts-speaking", + )} + > + + Speaking +
+ )} + {modules.includes("level") && ( +
setSelectedModule("level")} + className={clsx( + "hover:bg-ielts-level flex cursor-pointer items-center gap-2 rounded-xl p-4 transition duration-300 ease-in-out hover:text-white hover:shadow-lg", + selectedModule === "level" + ? "bg-ielts-level text-white" + : "bg-mti-gray-smoke text-ielts-level", + )} + > + + Level +
+ )} +
+ {isLoading && ( +
+ + + Evaluating your answers, please be patient... +
+ You can also check it later on your records page! +
+
+ )} + {!isLoading && ( +
+ + {moduleResultText(selectedModule, bandScore)} + +
+
+
+ Level + {showLevel(bandScore)} +
+
+
+
+
+
+ + {( + ((selectedScore.total - selectedScore.missing) / + selectedScore.total) * + 100 + ).toFixed(0)} + % + + Completion +
+
+
+
+
+ + {selectedScore.correct.toString().padStart(2, "0")} + + Correct +
+
+
+
+
+ + {(selectedScore.total - selectedScore.correct) + .toString() + .padStart(2, "0")} + + Wrong +
+
+
+
+
+ )} +
- {!isLoading && ( -
-
-
- - Play Again -
-
- - Review Answers -
-
+ {!isLoading && ( +
+
+
+ + Play Again +
+
+ + Review Answers +
+
- - - -
- )} - - ); + + + +
+ )} + + ); } diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 27f15e18..c5c64aac 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -1,332 +1,441 @@ /* eslint-disable @next/next/no-img-element */ -import Head from "next/head"; -import {useEffect, useState} from "react"; -import {Module} from "@/interfaces"; +import { Module } from "@/interfaces"; +import { useEffect, useState } from "react"; -import Selection from "@/exams/Selection"; -import Reading from "@/exams/Reading"; -import {Exam, InteractiveSpeakingExercise, SpeakingExercise, UserSolution, Variant, WritingExercise} from "@/interfaces/exam"; -import Listening from "@/exams/Listening"; -import Writing from "@/exams/Writing"; -import {ToastContainer, toast} from "react-toastify"; -import Finish from "@/exams/Finish"; -import axios from "axios"; -import {Stat} from "@/interfaces/user"; -import Speaking from "@/exams/Speaking"; -import {v4 as uuidv4} from "uuid"; -import useUser from "@/hooks/useUser"; -import useExamStore from "@/stores/examStore"; -import Layout from "@/components/High/Layout"; import AbandonPopup from "@/components/AbandonPopup"; -import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; -import {useRouter} from "next/router"; -import {getExam} from "@/utils/exams"; -import {capitalize} from "lodash"; +import Layout from "@/components/High/Layout"; +import Finish from "@/exams/Finish"; import Level from "@/exams/Level"; +import Listening from "@/exams/Listening"; +import Reading from "@/exams/Reading"; +import Selection from "@/exams/Selection"; +import Speaking from "@/exams/Speaking"; +import Writing from "@/exams/Writing"; +import useUser from "@/hooks/useUser"; +import { Exam, UserSolution, Variant } from "@/interfaces/exam"; +import { Stat } from "@/interfaces/user"; +import useExamStore from "@/stores/examStore"; +import { + evaluateSpeakingAnswer, + evaluateWritingAnswer, +} from "@/utils/evaluation"; +import { getExam } from "@/utils/exams"; +import axios from "axios"; +import { useRouter } from "next/router"; +import { toast, ToastContainer } from "react-toastify"; +import { v4 as uuidv4 } from "uuid"; interface Props { - page: "exams" | "exercises"; + page: "exams" | "exercises"; } -export default function ExamPage({page}: Props) { - const [hasBeenUploaded, setHasBeenUploaded] = useState(false); - const [moduleIndex, setModuleIndex] = useState(0); - const [sessionId, setSessionId] = useState(""); - const [exam, setExam] = useState(); - const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); - const [showAbandonPopup, setShowAbandonPopup] = useState(false); - const [avoidRepeated, setAvoidRepeated] = useState(false); - const [timeSpent, setTimeSpent] = useState(0); - const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState([]); - const [variant, setVariant] = useState("full"); +export default function ExamPage({ page }: Props) { + const [hasBeenUploaded, setHasBeenUploaded] = useState(false); + const [moduleIndex, setModuleIndex] = useState(0); + const [sessionId, setSessionId] = useState(""); + const [exam, setExam] = useState(); + const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); + const [showAbandonPopup, setShowAbandonPopup] = useState(false); + const [avoidRepeated, setAvoidRepeated] = useState(false); + const [timeSpent, setTimeSpent] = useState(0); + const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState< + string[] + >([]); + const [variant, setVariant] = useState("full"); - const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]); - const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]); - const [showSolutions, setShowSolutions] = useExamStore((state) => [state.showSolutions, state.setShowSolutions]); - const [selectedModules, setSelectedModules] = useExamStore((state) => [state.selectedModules, state.setSelectedModules]); - const assignment = useExamStore((state) => state.assignment); + const [exams, setExams] = useExamStore((state) => [ + state.exams, + state.setExams, + ]); + const [userSolutions, setUserSolutions] = useExamStore((state) => [ + state.userSolutions, + state.setUserSolutions, + ]); + const [showSolutions, setShowSolutions] = useExamStore((state) => [ + state.showSolutions, + state.setShowSolutions, + ]); + const [selectedModules, setSelectedModules] = useExamStore((state) => [ + state.selectedModules, + state.setSelectedModules, + ]); + const assignment = useExamStore((state) => state.assignment); - const {user} = useUser({redirectTo: "/login"}); - const router = useRouter(); + const { user } = useUser({ redirectTo: "/login" }); + const router = useRouter(); - useEffect(() => setSessionId(uuidv4()), []); - useEffect(() => { - if (user?.type === "developer") console.log(exam); - }, [exam, user]); + useEffect(() => setSessionId(uuidv4()), []); + useEffect(() => { + if (user?.type === "developer") console.log(exam); + }, [exam, user]); - useEffect(() => { - selectedModules.length > 0 && timeSpent === 0 && !showSolutions; - if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) { - const timerInterval = setInterval(() => { - setTimeSpent((prev) => prev + 1); - }, 1000); + useEffect(() => { + selectedModules.length > 0 && timeSpent === 0 && !showSolutions; + if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) { + const timerInterval = setInterval(() => { + setTimeSpent((prev) => prev + 1); + }, 1000); - return () => { - clearInterval(timerInterval); - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModules.length]); + return () => { + clearInterval(timerInterval); + }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedModules.length]); - useEffect(() => { - if (showSolutions) setModuleIndex(-1); - }, [showSolutions]); + useEffect(() => { + if (showSolutions) setModuleIndex(-1); + }, [showSolutions]); - useEffect(() => { - (async () => { - 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, exams]); + useEffect(() => { + (async () => { + 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, exams]); - useEffect(() => { - (async () => { - if (selectedModules.length > 0 && exams.length === 0) { - const examPromises = selectedModules.map((module) => getExam(module, avoidRepeated, variant)); - Promise.all(examPromises).then((values) => { - if (values.every((x) => !!x)) { - setExams(values.map((x) => x!)); - } else { - toast.error("Something went wrong, please try again"); - setTimeout(router.reload, 500); - } - }); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModules, setExams, exams]); + useEffect(() => { + (async () => { + if (selectedModules.length > 0 && exams.length === 0) { + const examPromises = selectedModules.map((module) => + getExam(module, avoidRepeated, variant), + ); + Promise.all(examPromises).then((values) => { + if (values.every((x) => !!x)) { + setExams(values.map((x) => x!)); + } else { + toast.error("Something went wrong, please try again"); + setTimeout(router.reload, 500); + } + }); + } + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedModules, setExams, exams]); - useEffect(() => { - if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) { - const newStats: Stat[] = userSolutions.map((solution) => ({ - ...solution, - id: solution.id || uuidv4(), - timeSpent, - session: sessionId, - exam: solution.exam!, - module: solution.module!, - user: user?.id || "", - date: new Date().getTime(), - ...(assignment ? {assignment: assignment.id} : {}), - })); + useEffect(() => { + if ( + selectedModules.length > 0 && + exams.length !== 0 && + moduleIndex >= selectedModules.length && + !hasBeenUploaded && + !showSolutions + ) { + const newStats: Stat[] = userSolutions.map((solution) => ({ + ...solution, + id: solution.id || uuidv4(), + timeSpent, + session: sessionId, + exam: solution.exam!, + module: solution.module!, + user: user?.id || "", + date: new Date().getTime(), + ...(assignment ? { assignment: assignment.id } : {}), + })); - axios - .post<{ok: boolean}>("/api/stats", newStats) - .then((response) => setHasBeenUploaded(response.data.ok)) - .catch(() => setHasBeenUploaded(false)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedModules, moduleIndex, hasBeenUploaded]); + axios + .post<{ ok: boolean }>("/api/stats", newStats) + .then((response) => setHasBeenUploaded(response.data.ok)) + .catch(() => setHasBeenUploaded(false)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedModules, moduleIndex, hasBeenUploaded]); - useEffect(() => { - if (statsAwaitingEvaluation.length === 0) return setIsEvaluationLoading(false); - return setIsEvaluationLoading(true); - }, [statsAwaitingEvaluation]); + useEffect(() => { + setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0); + }, [statsAwaitingEvaluation]); - useEffect(() => { - if (statsAwaitingEvaluation.length > 0) { - statsAwaitingEvaluation.forEach(checkIfStatHasBeenEvaluated); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [statsAwaitingEvaluation]); + useEffect(() => { + if (statsAwaitingEvaluation.length > 0) { + checkIfStatsHaveBeenEvaluated(statsAwaitingEvaluation); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [statsAwaitingEvaluation]); - const checkIfStatHasBeenEvaluated = (id: string) => { - setTimeout(async () => { - const statRequest = await axios.get(`/api/stats/${id}`); - const stat = statRequest.data; - if (stat.solutions.every((x) => x.evaluation !== null)) { - const userSolution: UserSolution = { - id, - exercise: stat.exercise, - score: stat.score, - solutions: stat.solutions, - type: stat.type, - exam: stat.exam, - module: stat.module, - }; + const checkIfStatsHaveBeenEvaluated = (ids: string[]) => { + setTimeout(async () => { + const awaitedStats = await Promise.all( + ids.map(async (id) => (await axios.get(`/api/stats/${id}`)).data), + ); + const solutionsEvaluated = awaitedStats.every((stat) => + stat.solutions.every((x) => x.evaluation !== null), + ); + if (solutionsEvaluated) { + const statsUserSolutions: UserSolution[] = awaitedStats.map((stat) => ({ + id: stat.id, + exercise: stat.exercise, + score: stat.score, + solutions: stat.solutions, + type: stat.type, + exam: stat.exam, + module: stat.module, + })); - setUserSolutions(userSolutions.map((x) => (x.exercise === userSolution.exercise ? userSolution : x))); - return setStatsAwaitingEvaluation((prev) => prev.filter((x) => x !== id)); - } + const updatedUserSolutions = userSolutions.map((x) => { + const respectiveSolution = statsUserSolutions.find( + (y) => y.exercise === x.exercise, + ); + return respectiveSolution ? respectiveSolution : x; + }); - return checkIfStatHasBeenEvaluated(id); - }, 5 * 1000); - }; + setUserSolutions(updatedUserSolutions); + return setStatsAwaitingEvaluation((prev) => + prev.filter((x) => !ids.includes(x)), + ); + } - const updateExamWithUserSolutions = (exam: Exam): Exam => { - if (exam.module === "reading" || exam.module === "listening") { - const parts = exam.parts.map((p) => - Object.assign(p, { - exercises: p.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions})), - }), - ); - return Object.assign(exam, {parts}); - } + return checkIfStatsHaveBeenEvaluated(ids); + }, 5 * 1000); + }; - const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions})); - return Object.assign(exam, {exercises}); - }; + const updateExamWithUserSolutions = (exam: Exam): Exam => { + if (exam.module === "reading" || exam.module === "listening") { + const parts = exam.parts.map((p) => + Object.assign(p, { + exercises: p.exercises.map((x) => + Object.assign(x, { + userSolutions: userSolutions.find((y) => x.id === y.exercise) + ?.solutions, + }), + ), + }), + ); + return Object.assign(exam, { parts }); + } - const onFinish = (solutions: UserSolution[]) => { - const solutionIds = solutions.map((x) => x.exercise); - const solutionExams = solutions.map((x) => x.exam); + const exercises = exam.exercises.map((x) => + Object.assign(x, { + userSolutions: userSolutions.find((y) => x.id === y.exercise) + ?.solutions, + }), + ); + return Object.assign(exam, { exercises }); + }; - if (exam && !solutionExams.includes(exam.id)) return; + const onFinish = (solutions: UserSolution[]) => { + const solutionIds = solutions.map((x) => x.exercise); + const solutionExams = solutions.map((x) => x.exam); - if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) { - setHasBeenUploaded(true); - setIsEvaluationLoading(true); + if (exam && !solutionExams.includes(exam.id)) return; - Promise.all( - exam.exercises.map(async (exercise) => { - const evaluationID = uuidv4(); - if (exercise.type === "writing") - return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID); + if ( + exam && + (exam.module === "writing" || exam.module === "speaking") && + solutions.length > 0 && + !showSolutions + ) { + setHasBeenUploaded(true); + setIsEvaluationLoading(true); - if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") - return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID); - }), - ) - .then((responses) => { - setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]); - setUserSolutions([...userSolutions, ...responses.filter((x) => !!x)] as any); - }) - .finally(() => { - setHasBeenUploaded(false); - }); - } + Promise.all( + exam.exercises.map(async (exercise) => { + const evaluationID = uuidv4(); + if (exercise.type === "writing") + return await evaluateWritingAnswer( + exercise, + solutions.find((x) => x.exercise === exercise.id)!, + evaluationID, + ); - axios.get("/api/stats/update"); + if ( + exercise.type === "interactiveSpeaking" || + exercise.type === "speaking" + ) + return await evaluateSpeakingAnswer( + exercise, + solutions.find((x) => x.exercise === exercise.id)!, + evaluationID, + ); + }), + ) + .then((responses) => { + setStatsAwaitingEvaluation((prev) => [ + ...prev, + ...responses.filter((x) => !!x).map((r) => (r as any).id), + ]); + setUserSolutions([ + ...userSolutions, + ...responses.filter((x) => !!x), + ] as any); + }) + .finally(() => { + setHasBeenUploaded(false); + }); + } - setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); - setModuleIndex((prev) => prev + 1); - }; + axios.get("/api/stats/update"); - const aggregateScoresByModule = (answers: UserSolution[]): {module: Module; total: number; missing: number; correct: number}[] => { - const scores: {[key in Module]: {total: number; missing: number; correct: number}} = { - reading: { - total: 0, - correct: 0, - missing: 0, - }, - listening: { - total: 0, - correct: 0, - missing: 0, - }, - writing: { - total: 0, - correct: 0, - missing: 0, - }, - speaking: { - total: 0, - correct: 0, - missing: 0, - }, - level: { - total: 0, - correct: 0, - missing: 0, - }, - }; + setUserSolutions([ + ...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), + ...solutions, + ]); + setModuleIndex((prev) => prev + 1); + }; - answers.forEach((x) => { - scores[x.module!] = { - total: scores[x.module!].total + x.score.total, - correct: scores[x.module!].correct + x.score.correct, - missing: scores[x.module!].missing + x.score.missing, - }; - }); + const aggregateScoresByModule = ( + answers: UserSolution[], + ): { module: Module; total: number; missing: number; correct: number }[] => { + const scores: { + [key in Module]: { total: number; missing: number; correct: number }; + } = { + reading: { + total: 0, + correct: 0, + missing: 0, + }, + listening: { + total: 0, + correct: 0, + missing: 0, + }, + writing: { + total: 0, + correct: 0, + missing: 0, + }, + speaking: { + total: 0, + correct: 0, + missing: 0, + }, + level: { + total: 0, + correct: 0, + missing: 0, + }, + }; - return Object.keys(scores) - .filter((x) => scores[x as Module].total > 0) - .map((x) => ({module: x as Module, ...scores[x as Module]})); - }; + answers.forEach((x) => { + scores[x.module!] = { + total: scores[x.module!].total + x.score.total, + correct: scores[x.module!].correct + x.score.correct, + missing: scores[x.module!].missing + x.score.missing, + }; + }); - const renderScreen = () => { - if (selectedModules.length === 0) { - return ( - { - setModuleIndex(0); - setAvoidRepeated(avoid); - setSelectedModules(modules); - setVariant(variant); - }} - /> - ); - } + return Object.keys(scores) + .filter((x) => scores[x as Module].total > 0) + .map((x) => ({ module: x as Module, ...scores[x as Module] })); + }; - if (moduleIndex >= selectedModules.length || moduleIndex === -1) { - return ( - { - setShowSolutions(true); - setModuleIndex(0); - setExam(exams[0]); - }} - scores={aggregateScoresByModule(userSolutions)} - /> - ); - } + const renderScreen = () => { + if (selectedModules.length === 0) { + return ( + { + setModuleIndex(0); + setAvoidRepeated(avoid); + setSelectedModules(modules); + setVariant(variant); + }} + /> + ); + } - if (exam && exam.module === "reading") { - return ; - } + if (moduleIndex >= selectedModules.length || moduleIndex === -1) { + return ( + { + setShowSolutions(true); + setModuleIndex(0); + setExam(exams[0]); + }} + scores={aggregateScoresByModule(userSolutions)} + /> + ); + } - if (exam && exam.module === "listening") { - return ; - } + if (exam && exam.module === "reading") { + return ( + + ); + } - if (exam && exam.module === "writing") { - return ; - } + if (exam && exam.module === "listening") { + return ( + + ); + } - if (exam && exam.module === "speaking") { - return ; - } + if (exam && exam.module === "writing") { + return ( + + ); + } - if (exam && exam.module === "level") { - return ; - } + if (exam && exam.module === "speaking") { + return ( + + ); + } - return <>Loading...; - }; + if (exam && exam.module === "level") { + return ( + + ); + } - return ( - <> - - {user && ( - setShowAbandonPopup(true)}> - <> - {renderScreen()} - {!showSolutions && moduleIndex < selectedModules.length && ( - router.reload()} - onCancel={() => setShowAbandonPopup(false)} - /> - )} - - - )} - - ); + return <>Loading...; + }; + + return ( + <> + + {user && ( + setShowAbandonPopup(true)} + > + <> + {renderScreen()} + {!showSolutions && moduleIndex < selectedModules.length && ( + router.reload()} + onCancel={() => setShowAbandonPopup(false)} + /> + )} + + + )} + + ); }