Corrected the behaviour of the exam after the timer has ended

This commit is contained in:
Tiago Ribeiro
2024-11-20 10:26:59 +00:00
parent e6d77af53f
commit 0eed8e4612
7 changed files with 53 additions and 52 deletions

View File

@@ -1,9 +1,9 @@
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import {motion} from "framer-motion"; import { motion } from "framer-motion";
import TimerEndedModal from "../TimerEndedModal"; import TimerEndedModal from "../TimerEndedModal";
import clsx from "clsx"; import clsx from "clsx";
import {BsStopwatch} from "react-icons/bs"; import { BsStopwatch } from "react-icons/bs";
interface Props { interface Props {
minTimer: number; minTimer: number;
@@ -11,13 +11,13 @@ interface Props {
standalone?: boolean; standalone?: boolean;
} }
const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) => { const Timer: React.FC<Props> = ({ minTimer, disableTimer, standalone = false }) => {
const [timer, setTimer] = useState(minTimer * 60); const [timer, setTimer] = useState(minTimer * 60);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [warningMode, setWarningMode] = useState(false); const [warningMode, setWarningMode] = useState(false);
const setHasExamEnded = useExamStore((state) => state.setHasExamEnded); const setHasExamEnded = useExamStore((state) => state.setHasExamEnded);
const {timeSpent} = useExamStore((state) => state); const { timeSpent } = useExamStore((state) => state);
useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]); useEffect(() => setTimer((prev) => prev - timeSpent), [timeSpent]);
@@ -54,9 +54,9 @@ const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) =>
standalone ? "top-10" : "top-4", standalone ? "top-10" : "top-4",
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt", warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
)} )}
initial={{scale: warningMode && !disableTimer ? 0.8 : 1}} initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
animate={{scale: warningMode && !disableTimer ? 1.1 : 1}} animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}> transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
<BsStopwatch className="w-6 h-6" /> <BsStopwatch className="w-6 h-6" />
<span className="text-lg font-bold w-12"> <span className="text-lg font-bold w-12">
{timer > 0 && ( {timer > 0 && (

View File

@@ -78,7 +78,7 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
const [startNow, setStartNow] = useState<boolean>(!showSolutions); const [startNow, setStartNow] = useState<boolean>(!showSolutions);
useEffect(() => { useEffect(() => {
if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(partIndex)) { if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(partIndex)) {
setShowPartDivider(true); setShowPartDivider(true);
setBgColor(levelBgColor); setBgColor(levelBgColor);
@@ -101,6 +101,11 @@ export default function Level({ exam, showSolutions = false, onFinish, preview =
const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined) const [showSolutionsSave, setShowSolutionsSave] = useState(showSolutions ? userSolutions.filter((x) => x.module === "level") : undefined)
useEffect(() => {
if (hasExamEnded) onFinish(userSolutions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasExamEnded]);
useEffect(() => { useEffect(() => {
if (typeof currentSolution !== "undefined") { if (typeof currentSolution !== "undefined") {
setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); setUserSolutions([...userSolutions.filter((x) => x.exercise !== currentSolution.exercise), { ...currentSolution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]);

View File

@@ -145,10 +145,9 @@ export default function Listening({ exam, showSolutions = false, preview = false
}, []); }, []);
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded) onFinish(userSolutions)
setExerciseIndex(exerciseIndex + 1); // eslint-disable-next-line react-hooks/exhaustive-deps
} }, [hasExamEnded]);
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const confirmFinishModule = (keepGoing?: boolean) => { const confirmFinishModule = (keepGoing?: boolean) => {
if (!keepGoing) { if (!keepGoing) {

View File

@@ -173,10 +173,9 @@ export default function Reading({ exam, showSolutions = false, preview = false,
}, []); }, []);
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded) onFinish(userSolutions)
setExerciseIndex(exerciseIndex + 1); // eslint-disable-next-line react-hooks/exhaustive-deps
} }, [hasExamEnded]);
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const confirmFinishModule = (keepGoing?: boolean) => { const confirmFinishModule = (keepGoing?: boolean) => {
if (!keepGoing) { if (!keepGoing) {

View File

@@ -53,10 +53,9 @@ export default function Speaking({ exam, showSolutions = false, onFinish, previe
}, [exerciseIndex]); }, [exerciseIndex]);
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded) onFinish(userSolutions)
setExerciseIndex(exerciseIndex + 1); // eslint-disable-next-line react-hooks/exhaustive-deps
} }, [hasExamEnded]);
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));

View File

@@ -34,10 +34,9 @@ export default function Writing({ exam, showSolutions = false, preview = false,
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.exercises[0].intro === "string" && exam.exercises[0].intro !== ""); const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.exercises[0].intro === "string" && exam.exercises[0].intro !== "");
useEffect(() => { useEffect(() => {
if (hasExamEnded && exerciseIndex === -1) { if (hasExamEnded) onFinish(userSolutions)
setExerciseIndex(exerciseIndex + 1); // eslint-disable-next-line react-hooks/exhaustive-deps
} }, [hasExamEnded]);
}, [hasExamEnded, exerciseIndex, setExerciseIndex]);
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
@@ -100,7 +99,7 @@ export default function Writing({ exam, showSolutions = false, preview = false,
defaultTitle="Writing exam" defaultTitle="Writing exam"
section={exam.exercises[exerciseIndex]} section={exam.exercises[exerciseIndex]}
sectionIndex={exerciseIndex} sectionIndex={exerciseIndex}
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex))}} onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
/> : ( /> : (
<div className="flex flex-col h-full w-full gap-8 items-center"> <div className="flex flex-col h-full w-full gap-8 items-center">
<ModuleTitle <ModuleTitle

View File

@@ -1,30 +1,30 @@
import {useMemo, useState} from "react"; import { useMemo, useState } from "react";
import {PERMISSIONS} from "@/constants/userPermissions"; import { PERMISSIONS } from "@/constants/userPermissions";
import useExams from "@/hooks/useExams"; import useExams from "@/hooks/useExams";
import useUsers from "@/hooks/useUsers"; import useUsers from "@/hooks/useUsers";
import {Module} from "@/interfaces"; import { Module } from "@/interfaces";
import {Exam} from "@/interfaces/exam"; import { Exam } from "@/interfaces/exam";
import {Type, User} from "@/interfaces/user"; import { Type, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore"; import useExamStore from "@/stores/examStore";
import {getExamById} from "@/utils/exams"; import { getExamById } from "@/utils/exams";
import {countExercises} from "@/utils/moduleUtils"; import { countExercises } from "@/utils/moduleUtils";
import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import axios from "axios"; import axios from "axios";
import clsx from "clsx"; import clsx from "clsx";
import {capitalize, uniq} from "lodash"; import { capitalize, uniq } from "lodash";
import {useRouter} from "next/router"; import { useRouter } from "next/router";
import {BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs"; import { BsBan, BsBanFill, BsCheck, BsCircle, BsPencil, BsStop, BsTrash, BsUpload, BsX } from "react-icons/bs";
import {toast} from "react-toastify"; import { toast } from "react-toastify";
import {useListSearch} from "@/hooks/useListSearch"; import { useListSearch } from "@/hooks/useListSearch";
import Modal from "@/components/Modal"; import Modal from "@/components/Modal";
import {checkAccess} from "@/utils/permissions"; import { checkAccess } from "@/utils/permissions";
import useGroups from "@/hooks/useGroups"; import useGroups from "@/hooks/useGroups";
import Button from "@/components/Low/Button"; import Button from "@/components/Low/Button";
import { EntityWithRoles } from "@/interfaces/entity"; import { EntityWithRoles } from "@/interfaces/entity";
const searchFields = [["module"], ["id"], ["createdBy"]]; const searchFields = [["module"], ["id"], ["createdBy"]];
const CLASSES: {[key in Module]: string} = { const CLASSES: { [key in Module]: string } = {
reading: "text-ielts-reading", reading: "text-ielts-reading",
listening: "text-ielts-listening", listening: "text-ielts-listening",
speaking: "text-ielts-speaking", speaking: "text-ielts-speaking",
@@ -34,7 +34,7 @@ const CLASSES: {[key in Module]: string} = {
const columnHelper = createColumnHelper<Exam>(); const columnHelper = createColumnHelper<Exam>();
const ExamOwnerSelector = ({options, exam, onSave}: {options: User[]; exam: Exam; onSave: (owners: string[]) => void}) => { const ExamOwnerSelector = ({ options, exam, onSave }: { options: User[]; exam: Exam; onSave: (owners: string[]) => void }) => {
const [owners, setOwners] = useState(exam.owners || []); const [owners, setOwners] = useState(exam.owners || []);
return ( return (
@@ -57,12 +57,12 @@ const ExamOwnerSelector = ({options, exam, onSave}: {options: User[]; exam: Exam
); );
}; };
export default function ExamList({user, entities}: {user: User; entities: EntityWithRoles[];}) { export default function ExamList({ user, entities }: { user: User; entities: EntityWithRoles[]; }) {
const [selectedExam, setSelectedExam] = useState<Exam>(); const [selectedExam, setSelectedExam] = useState<Exam>();
const {exams, reload} = useExams(); const { exams, reload } = useExams();
const {users} = useUsers(); const { users } = useUsers();
const {groups} = useGroups({admin: user?.id, userType: user?.type}); const { groups } = useGroups({ admin: user?.id, userType: user?.type });
const filteredExams = useMemo(() => exams.filter((e) => { const filteredExams = useMemo(() => exams.filter((e) => {
if (!e.private) return true if (!e.private) return true
@@ -90,7 +90,7 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
}); });
}, [filteredExams, users]); }, [filteredExams, users]);
const {rows: filteredRows, renderSearch} = useListSearch<Exam>(searchFields, parsedExams); const { rows: filteredRows, renderSearch } = useListSearch<Exam>(searchFields, parsedExams);
const setExams = useExamStore((state) => state.setExams); const setExams = useExamStore((state) => state.setExams);
const setSelectedModules = useExamStore((state) => state.setSelectedModules); const setSelectedModules = useExamStore((state) => state.setSelectedModules);
@@ -110,14 +110,14 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
setExams([exam]); setExams([exam]);
setSelectedModules([module]); setSelectedModules([module]);
router.push("/exercises"); router.push("/exam");
}; };
const privatizeExam = async (exam: Exam) => { const privatizeExam = async (exam: Exam) => {
if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return; if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
axios axios
.patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private}) .patch(`/api/exam/${exam.module}/${exam.id}`, { private: !exam.private })
.then(() => toast.success(`Updated the "${exam.id}" exam`)) .then(() => toast.success(`Updated the "${exam.id}" exam`))
.catch((reason) => { .catch((reason) => {
if (reason.response.status === 404) { if (reason.response.status === 404) {
@@ -227,7 +227,7 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
{ {
header: "", header: "",
id: "actions", id: "actions",
cell: ({row}: {row: {original: Exam}}) => { cell: ({ row }: { row: { original: Exam } }) => {
return ( return (
<div className="flex gap-4"> <div className="flex gap-4">
{(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && ( {(row.original.owners?.includes(user.id) || checkAccess(user, ["admin", "developer"])) && (
@@ -273,7 +273,7 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
{renderSearch()} {renderSearch()}
<Modal isOpen={!!selectedExam} title={`Edit Exam Owners - ${selectedExam?.id}`} onClose={() => setSelectedExam(undefined)}> <Modal isOpen={!!selectedExam} title={`Edit Exam Owners - ${selectedExam?.id}`} onClose={() => setSelectedExam(undefined)}>
{!!selectedExam ? ( {!!selectedExam ? (
<ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, {owners})} /> <ExamOwnerSelector options={filteredCorporates} exam={selectedExam} onSave={(owners) => updateExam(selectedExam, { owners })} />
) : ( ) : (
<div /> <div />
)} )}
@@ -304,4 +304,4 @@ export default function ExamList({user, entities}: {user: User; entities: Entity
</table> </table>
</div> </div>
); );
} }