/* eslint-disable @next/next/no-img-element */ import {Module} from "@/interfaces"; import {useEffect, 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 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 {defaultExamUserSolutions, 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"; import useSessions from "@/hooks/useSessions"; import ShortUniqueId from "short-unique-id"; interface Props { page: "exams" | "exercises"; } export default function ExamPage({page}: Props) { const [variant, setVariant] = useState("full"); const [avoidRepeated, setAvoidRepeated] = useState(false); const [hasBeenUploaded, setHasBeenUploaded] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState([]); const [timeSpent, setTimeSpent] = useState(0); const resetStore = useExamStore((state) => state.reset); const assignment = useExamStore((state) => state.assignment); const initialTimeSpent = useExamStore((state) => state.timeSpent); const examStore = useExamStore; const {exam, setExam} = useExamStore((state) => state); const {exams, setExams} = useExamStore((state) => state); const {sessionId, setSessionId} = useExamStore((state) => state); const {partIndex, setPartIndex} = useExamStore((state) => state); const {moduleIndex, setModuleIndex} = useExamStore((state) => state); const {questionIndex, setQuestionIndex} = useExamStore((state) => state); const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state); const {userSolutions, setUserSolutions} = useExamStore((state) => state); const {showSolutions, setShowSolutions} = useExamStore((state) => state); const {selectedModules, setSelectedModules} = useExamStore((state) => state); const {user} = useUser({redirectTo: "/login"}); const router = useRouter(); const reset = () => { resetStore(); setVariant("full"); setAvoidRepeated(false); setHasBeenUploaded(false); setShowAbandonPopup(false); setIsEvaluationLoading(false); setStatsAwaitingEvaluation([]); setTimeSpent(0); }; // eslint-disable-next-line react-hooks/exhaustive-deps const saveSession = async () => { console.log("Saving your session..."); await axios.post("/api/sessions", { id: sessionId, sessionId, date: new Date().toISOString(), userSolutions, moduleIndex, selectedModules, assignment, timeSpent, exams, exam, partIndex, exerciseIndex, questionIndex, user: user?.id, }); }; useEffect(() => setTimeSpent((prev) => prev + initialTimeSpent), [initialTimeSpent]); useEffect(() => { if (userSolutions.length === 0 && exams.length > 0) { const defaultSolutions = exams.map(defaultExamUserSolutions).flat(); setUserSolutions(defaultSolutions); } }, [exams, setUserSolutions, userSolutions]); useEffect(() => { if ( sessionId.length > 0 && userSolutions.length > 0 && selectedModules.length > 0 && exams.length > 0 && !!exam && timeSpent > 0 && !showSolutions && moduleIndex < selectedModules.length ) saveSession(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex, questionIndex]); useEffect(() => { if (timeSpent % 20 === 0 && timeSpent > 0 && moduleIndex < selectedModules.length && !showSolutions) saveSession(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [timeSpent]); useEffect(() => { if (selectedModules.length > 0 && sessionId.length === 0) { const shortUID = new ShortUniqueId(); setSessionId(shortUID.randomUUID(8)); } }, [setSessionId, selectedModules, sessionId]); useEffect(() => { if (user?.type === "developer") console.log(exam); }, [exam, user]); useEffect(() => { 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]); useEffect(() => { if (showSolutions) setModuleIndex(-1); }, [setModuleIndex, showSolutions]); useEffect(() => { (async () => { if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { const nextExam = exams[moduleIndex]; if (partIndex === -1 && nextExam.module !== "listening") setPartIndex(0); if (exerciseIndex === -1 && !["reading", "listening"].includes(nextExam.module)) setExerciseIndex(0); 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, user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined, ), ); 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(), isDisabled: solution.isDisabled, ...(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]); useEffect(() => { setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0); }, [statsAwaitingEvaluation]); useEffect(() => { if (statsAwaitingEvaluation.length > 0) { checkIfStatsHaveBeenEvaluated(statsAwaitingEvaluation); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [statsAwaitingEvaluation]); const checkIfStatsHaveBeenEvaluated = (ids: string[]) => { setTimeout(async () => { try { 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, })); const updatedUserSolutions = userSolutions.map((x) => { const respectiveSolution = statsUserSolutions.find((y) => y.exercise === x.exercise); return respectiveSolution ? respectiveSolution : x; }); setUserSolutions(updatedUserSolutions); return setStatsAwaitingEvaluation((prev) => prev.filter((x) => !ids.includes(x))); } return checkIfStatsHaveBeenEvaluated(ids); } catch { return checkIfStatsHaveBeenEvaluated(ids); } }, 5 * 1000); }; 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 exercises = exam.exercises.map((x) => Object.assign(x, { userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions, }), ); return Object.assign(exam, {exercises}); }; const onFinish = async (solutions: UserSolution[]) => { const solutionIds = solutions.map((x) => x.exercise); const solutionExams = solutions.map((x) => x.exam); if (exam && !solutionExams.includes(exam.id)) return; if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) { setHasBeenUploaded(true); setIsEvaluationLoading(true); 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 (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID); }), ) .then((responses) => { examStore.setState((prev) => { return { userSolutions: [ ...prev.userSolutions.filter( (x) => !responses .filter((r) => !!r) .map((r) => r!.id) .includes(x.id), ), ...responses.filter((x) => !!x), ] as any, }; }); setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]); }) .finally(() => { setHasBeenUploaded(false); }); } axios.get("/api/stats/update"); setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); setModuleIndex(moduleIndex + 1); setPartIndex(-1); setExerciseIndex(-1); setQuestionIndex(0); }; const aggregateScoresByModule = (): {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.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 renderScreen = () => { if (selectedModules.length === 0) { return ( { setModuleIndex(0); setAvoidRepeated(avoid); setSelectedModules(modules); setVariant(variant); }} /> ); } if (moduleIndex >= selectedModules.length || moduleIndex === -1) { return ( { setShowSolutions(true); setModuleIndex(0); setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0); setPartIndex(exams[0].module === "listening" ? -1 : 0); setExam(exams[0]); }} scores={aggregateScoresByModule()} /> ); } if (exam && exam.module === "reading") { return ; } if (exam && exam.module === "listening") { return ; } if (exam && exam.module === "writing") { return ; } if (exam && exam.module === "speaking") { return ; } if (exam && exam.module === "level") { return ; } return <>Loading...; }; return ( <> {user && ( setShowAbandonPopup(true)}> <> {renderScreen()} {!showSolutions && moduleIndex < selectedModules.length && ( { reset(); }} onCancel={() => setShowAbandonPopup(false)} /> )} )} ); }