From 8baa25c445f1baa2535db5a0e358159234f90844 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Mon, 5 Feb 2024 17:59:46 +0000 Subject: [PATCH] Added a Scroll To Top function --- src/components/Exercises/MultipleChoice.tsx | 6 + src/exams/Listening.tsx | 4 + src/exams/Reading.tsx | 4 + src/exams/Speaking.tsx | 4 + src/exams/Writing.tsx | 4 + src/pages/(exam)/ExamPage.tsx | 673 +++++++++----------- src/stores/examStore.ts | 42 +- src/styles/globals.css | 10 +- 8 files changed, 354 insertions(+), 393 deletions(-) diff --git a/src/components/Exercises/MultipleChoice.tsx b/src/components/Exercises/MultipleChoice.tsx index b16063c5..f2847209 100644 --- a/src/components/Exercises/MultipleChoice.tsx +++ b/src/components/Exercises/MultipleChoice.tsx @@ -63,6 +63,8 @@ export default function MultipleChoice({ const hasExamEnded = useExamStore((state) => state.hasExamEnded); + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + useEffect(() => { if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -93,6 +95,8 @@ export default function MultipleChoice({ } else { setQuestionIndex((prev) => prev + 1); } + + scrollToTop(); }; const back = () => { @@ -101,6 +105,8 @@ export default function MultipleChoice({ } else { setQuestionIndex((prev) => prev - 1); } + + scrollToTop(); }; return ( diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index 89d8ca9b..6d344812 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -32,6 +32,8 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { setExerciseIndex((prev) => prev + 1); @@ -52,6 +54,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props }; const nextExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } @@ -92,6 +95,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props }; const previousExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 7d0edff9..573d2c3c 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -93,6 +93,8 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + useEffect(() => { const listener = (e: KeyboardEvent) => { if (e.key === "F3" || ((e.ctrlKey || e.metaKey) && e.key === "f")) { @@ -127,6 +129,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) }; const nextExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } @@ -167,6 +170,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) }; const previousExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } diff --git a/src/exams/Speaking.tsx b/src/exams/Speaking.tsx index 80cdffde..d3e3564c 100644 --- a/src/exams/Speaking.tsx +++ b/src/exams/Speaking.tsx @@ -26,11 +26,14 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props) const [userSolutions, setUserSolutions] = useState(exam.exercises.map((x) => defaultUserSolutions(x, exam))); const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + useEffect(() => { setCurrentQuestionIndex(0); }, [questionIndex]); const nextExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } @@ -55,6 +58,7 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props) }; const previousExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index e317117a..c2a6a692 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -24,7 +24,10 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props) const [hasExamEnded, setHasExamEnded] = useExamStore((state) => [state.hasExamEnded, state.setHasExamEnded]); + const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); + const nextExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } @@ -48,6 +51,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props) }; const previousExercise = (solution?: UserSolution) => { + scrollToTop(); if (solution) { setUserSolutions((prev) => [...prev.filter((x) => x.exercise !== solution.exercise), solution]); } diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index c5c64aac..cfe36e8b 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -1,6 +1,6 @@ /* eslint-disable @next/next/no-img-element */ -import { Module } from "@/interfaces"; -import { useEffect, useState } from "react"; +import {Module} from "@/interfaces"; +import {useEffect, useState} from "react"; import AbandonPopup from "@/components/AbandonPopup"; import Layout from "@/components/High/Layout"; @@ -12,430 +12,333 @@ 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 {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 {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"; +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< - string[] - >([]); - const [variant, setVariant] = useState("full"); +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 [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 {assignment} = useExamStore((state) => state); + const {exam, setExam} = useExamStore((state) => state); + const {exams, setExams} = useExamStore((state) => state); + const {timeSpent, setTimeSpent} = useExamStore((state) => state); + const {sessionId, setSessionId} = useExamStore((state) => state); + const {moduleIndex, setModuleIndex} = 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 {user} = useUser({redirectTo: "/login"}); + const router = useRouter(); - useEffect(() => setSessionId(uuidv4()), []); - useEffect(() => { - if (user?.type === "developer") console.log(exam); - }, [exam, user]); + useEffect(() => setSessionId(uuidv4()), [setSessionId]); + 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(timeSpent + 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); + }, [setModuleIndex, 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(() => { - setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0); - }, [statsAwaitingEvaluation]); + useEffect(() => { + setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0); + }, [statsAwaitingEvaluation]); - useEffect(() => { - if (statsAwaitingEvaluation.length > 0) { - checkIfStatsHaveBeenEvaluated(statsAwaitingEvaluation); - } - // 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 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, - })); + 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, + })); - const updatedUserSolutions = userSolutions.map((x) => { - const respectiveSolution = statsUserSolutions.find( - (y) => y.exercise === x.exercise, - ); - return respectiveSolution ? respectiveSolution : x; - }); + 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)), - ); - } + setUserSolutions(updatedUserSolutions); + return setStatsAwaitingEvaluation((prev) => prev.filter((x) => !ids.includes(x))); + } - return checkIfStatsHaveBeenEvaluated(ids); - }, 5 * 1000); - }; + 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 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 exercises = exam.exercises.map((x) => + Object.assign(x, { + userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions, + }), + ); + return Object.assign(exam, {exercises}); + }; - const onFinish = (solutions: UserSolution[]) => { - const solutionIds = solutions.map((x) => x.exercise); - const solutionExams = solutions.map((x) => x.exam); + const onFinish = (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 && !solutionExams.includes(exam.id)) return; - if ( - exam && - (exam.module === "writing" || exam.module === "speaking") && - solutions.length > 0 && - !showSolutions - ) { - setHasBeenUploaded(true); - setIsEvaluationLoading(true); + 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, - ); + 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) => { - setStatsAwaitingEvaluation((prev) => [ - ...prev, - ...responses.filter((x) => !!x).map((r) => (r as any).id), - ]); - setUserSolutions([ - ...userSolutions, - ...responses.filter((x) => !!x), - ] as any); - }) - .finally(() => { - setHasBeenUploaded(false); - }); - } + 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); + }); + } - axios.get("/api/stats/update"); + axios.get("/api/stats/update"); - setUserSolutions([ - ...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), - ...solutions, - ]); - setModuleIndex((prev) => prev + 1); - }; + setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); + setModuleIndex(moduleIndex + 1); + }; - 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, - }, - }; + 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, + }, + }; - 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, - }; - }); + 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, + }; + }); - return Object.keys(scores) - .filter((x) => scores[x as Module].total > 0) - .map((x) => ({ module: x as Module, ...scores[x as Module] })); - }; + 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); - }} - /> - ); - } + 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); - setExam(exams[0]); - }} - scores={aggregateScoresByModule(userSolutions)} - /> - ); - } + if (moduleIndex >= selectedModules.length || moduleIndex === -1) { + return ( + { + setShowSolutions(true); + setModuleIndex(0); + setExam(exams[0]); + }} + scores={aggregateScoresByModule(userSolutions)} + /> + ); + } - if (exam && exam.module === "reading") { - return ( - - ); - } + if (exam && exam.module === "reading") { + return ; + } - if (exam && exam.module === "listening") { - return ( - - ); - } + if (exam && exam.module === "listening") { + return ; + } - if (exam && exam.module === "writing") { - return ( - - ); - } + if (exam && exam.module === "writing") { + return ; + } - if (exam && exam.module === "speaking") { - return ( - - ); - } + if (exam && exam.module === "speaking") { + return ; + } - if (exam && exam.module === "level") { - return ( - - ); - } + if (exam && exam.module === "level") { + return ; + } - return <>Loading...; - }; + return <>Loading...; + }; - return ( - <> - - {user && ( - setShowAbandonPopup(true)} - > - <> - {renderScreen()} - {!showSolutions && moduleIndex < selectedModules.length && ( - router.reload()} - onCancel={() => setShowAbandonPopup(false)} - /> - )} - - - )} - - ); + return ( + <> + + {user && ( + setShowAbandonPopup(true)}> + <> + {renderScreen()} + {!showSolutions && moduleIndex < selectedModules.length && ( + router.reload()} + onCancel={() => setShowAbandonPopup(false)} + /> + )} + + + )} + + ); } diff --git a/src/stores/examStore.ts b/src/stores/examStore.ts index 7561d155..1102c01b 100644 --- a/src/stores/examStore.ts +++ b/src/stores/examStore.ts @@ -5,17 +5,35 @@ import {create} from "zustand"; export interface ExamState { exams: Exam[]; - userSolutions: UserSolution[]; - showSolutions: boolean; - hasExamEnded: boolean; - selectedModules: Module[]; - assignment?: Assignment; - setHasExamEnded: (hasExamEnded: boolean) => void; - setUserSolutions: (userSolutions: UserSolution[]) => void; setExams: (exams: Exam[]) => void; + + userSolutions: UserSolution[]; + setUserSolutions: (userSolutions: UserSolution[]) => void; + + showSolutions: boolean; setShowSolutions: (showSolutions: boolean) => void; + + hasExamEnded: boolean; + setHasExamEnded: (hasExamEnded: boolean) => void; + + selectedModules: Module[]; setSelectedModules: (modules: Module[]) => void; + + assignment?: Assignment; setAssignment: (assignment: Assignment) => void; + + timeSpent: number; + setTimeSpent: (timeSpent: number) => void; + + sessionId: string; + setSessionId: (sessionId: string) => void; + + moduleIndex: number; + setModuleIndex: (moduleIndex: number) => void; + + exam?: Exam; + setExam: (exam?: Exam) => void; + reset: () => void; } @@ -26,16 +44,26 @@ export const initialState = { selectedModules: [], hasExamEnded: false, assignment: undefined, + timeSpent: 0, + sessionId: "", + exam: undefined, + moduleIndex: 0, }; const useExamStore = create((set) => ({ ...initialState, + setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})), setExams: (exams: Exam[]) => set(() => ({exams})), setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})), setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})), setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})), setAssignment: (assignment: Assignment) => set(() => ({assignment})), + setTimeSpent: (timeSpent) => set(() => ({timeSpent})), + setSessionId: (sessionId: string) => set(() => ({sessionId})), + setExam: (exam?: Exam) => set(() => ({exam})), + setModuleIndex: (moduleIndex: number) => set(() => ({moduleIndex})), + reset: () => set(() => initialState), })); diff --git a/src/styles/globals.css b/src/styles/globals.css index 1e1987bd..427186bb 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -31,7 +31,15 @@ margin: 0; } -html, +html { + min-height: 100vh !important; + height: 100%; + max-width: 100vw; + overflow-x: hidden; + overflow-y: auto; + font-family: "Open Sans", system-ui, -apple-system, "Helvetica Neue", sans-serif; +} + body { min-height: 100vh !important; height: 100%;