Corrected the behaviour of the exam after the timer has ended
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
@@ -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!] : [] }]);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user