/* eslint-disable @next/next/no-img-element */ import { Module } from "@/interfaces"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import AbandonPopup from "@/components/AbandonPopup"; 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 { Exam, LevelExam, UserSolution, Variant, WritingExam } from "@/interfaces/exam"; import { User } from "@/interfaces/user"; 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 ShortUniqueId from "short-unique-id"; import { ExamProps } from "@/exams/types"; import useExamStore from "@/stores/exam"; import useEvaluationPolling from "@/hooks/useEvaluationPolling"; interface Props { page: "exams" | "exercises"; user: User; destination?: string hideSidebar?: boolean } export default function ExamPage({ page, user, destination = "/", hideSidebar = false }: Props) { const router = useRouter(); const [variant, setVariant] = useState("full"); const [avoidRepeated, setAvoidRepeated] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [pendingExercises, setPendingExercises] = useState([]); const { exam, setExam, exams, sessionId, setSessionId, setPartIndex, moduleIndex, setModuleIndex, setQuestionIndex, setExerciseIndex, userSolutions, setUserSolutions, showSolutions, setShowSolutions, selectedModules, setSelectedModules, setUser, inactivity, timeSpent, assignment, bgColor, flags, dispatch, reset: resetStore, saveStats, saveSession, setFlags, setShuffles, evaluated, } = useExamStore(); const [isFetchingExams, setIsFetchingExams] = useState(false); const [isExamLoaded, setIsExamLoaded] = useState(moduleIndex < selectedModules.length); useEffect(() => { setIsExamLoaded(moduleIndex < selectedModules.length); }, [showSolutions, moduleIndex, selectedModules]); useEffect(() => { if (!showSolutions && sessionId.length === 0 && user?.id) { const shortUID = new ShortUniqueId(); setUser(user.id); setSessionId(shortUID.randomUUID(8)); } }, [setSessionId, isExamLoaded, sessionId, showSolutions, setUser, user?.id]); useEffect(() => { if (user?.type === "developer") console.log(exam); }, [exam, user]); useEffect(() => { (async () => { if (selectedModules.length > 0 && exams.length === 0) { setIsFetchingExams(true); const examPromises = selectedModules.map((module) => getExam( module, avoidRepeated, variant, user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined, ), ); Promise.all(examPromises).then((values) => { setIsFetchingExams(false); if (values.every((x) => !!x)) { dispatch({ type: 'INIT_EXAM', payload: { exams: values.map((x) => x!), modules: selectedModules } }) } else { toast.error("Something went wrong, please try again"); setTimeout(router.reload, 500); } }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, exams]); const reset = () => { resetStore(); setVariant("full"); setAvoidRepeated(false); setShowAbandonPopup(false); }; useEffect(() => { if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) { if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 && !showSolutions) { const exercisesToEvaluate = exam.exercises .map(exercise => exercise.id); setPendingExercises(exercisesToEvaluate); (async () => { await Promise.all( exam.exercises.map(async (exercise, index) => { if (exercise.type === "writing") await evaluateWritingAnswer(user.id, sessionId, exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!, exercise.attachment?.url); if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") { await evaluateSpeakingAnswer( user.id, sessionId, exercise, userSolutions.find((x) => x.exercise === exercise.id)!, index + 1, ); } }), ) })(); } } }, [exam, showSolutions, userSolutions, sessionId, user?.id, flags]); useEvaluationPolling({ pendingExercises, setPendingExercises }); useEffect(() => { if (flags.finalizeExam && moduleIndex !== -1) { setModuleIndex(-1); } }, [flags.finalizeExam, moduleIndex, setModuleIndex]); useEffect(() => { if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) { (async () => { if (evaluated.length !== 0) { setUserSolutions( userSolutions.map(solution => { const evaluatedSolution = evaluated.find(e => e.exercise === solution.exercise); if (evaluatedSolution) { return { ...solution, ...evaluatedSolution }; } return solution; }) ); } await saveStats(); await axios.get("/api/stats/update"); setShowSolutions(true); setFlags({ finalizeExam: false }); dispatch({ type: "UPDATE_EXAMS" }) })(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions, flags]); const aggregateScoresByModule = (isPractice?: boolean): { 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, }, }; userSolutions.filter(x => isPractice ? x.isPractice : !x.isPractice).forEach((x) => { const examModule = x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); scores[examModule!] = { total: scores[examModule!].total + x.score.total, correct: scores[examModule!].correct + x.score.correct, missing: scores[examModule!].missing + x.score.missing, }; }); return Object.keys(scores) .filter((x) => scores[x as Module].total > 0) .map((x) => ({ module: x as Module, ...scores[x as Module] })); }; const ModuleExamMap: Record>> = { "reading": Reading as React.ComponentType>, "listening": Listening as React.ComponentType>, "writing": Writing as React.ComponentType>, "speaking": Speaking as React.ComponentType>, "level": Level as React.ComponentType>, } const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined; const onAbandon = async () => { await saveSession(); reset(); }; return ( <> {user && ( setShowAbandonPopup(true)}> <> {/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/} {selectedModules.length === 0 && { setModuleIndex(0); setAvoidRepeated(avoid); setSelectedModules(modules); setVariant(variant); }} />} {isFetchingExams && (
Loading Exam ...
)} {(moduleIndex === -1 && selectedModules.length !== 0) && { if (exams[0].module === "level") { const levelExam = exams[0] as LevelExam; const allExercises = levelExam.parts.flatMap((part) => part.exercises); const exerciseOrderMap = new Map(allExercises.map((ex, index) => [ex.id, index])); const orderedSolutions = userSolutions.slice().sort((a, b) => { const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity; const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity; return indexA - indexB; }); setUserSolutions(orderedSolutions); } else { setUserSolutions(userSolutions); } setShuffles([]); if (index === undefined) { setFlags({ reviewAll: true }); setModuleIndex(0); setExam(exams[0]); } else { setModuleIndex(index); setExam(exams[index]); } setShowSolutions(true); setQuestionIndex(0); setExerciseIndex(0); setPartIndex(0); }} scores={aggregateScoresByModule()} practiceScores={aggregateScoresByModule(true)} />} {/* Exam is on going, display it and the abandon modal */} {isExamLoaded && moduleIndex !== -1 && ( <> {exam && CurrentExam && } {!showSolutions && setShowAbandonPopup(false)} /> } )}
)} ); }