diff --git a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx index 19bb40b7..852128d5 100644 --- a/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx +++ b/src/components/ExamEditor/SectionRenderer/SectionContext/listening.tsx @@ -73,7 +73,7 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => { "h-5 w-5", `text-ielts-${currentModule}` )} /> - Conversation + {(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"} } > diff --git a/src/components/High/AssignmentCard.tsx b/src/components/High/AssignmentCard.tsx index 6720c0e1..49812254 100644 --- a/src/components/High/AssignmentCard.tsx +++ b/src/components/High/AssignmentCard.tsx @@ -1,6 +1,7 @@ import { Session } from "@/hooks/useSessions"; import { Assignment } from "@/interfaces/results"; import { User } from "@/interfaces/user"; +import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments"; import { sortByModuleName } from "@/utils/moduleUtils"; import clsx from "clsx"; import moment from "moment"; @@ -44,7 +45,16 @@ export default function AssignmentCard({ user, assignment, session, startAssignm ))} - {!assignment.results.map((r) => r.user).includes(user.id) && ( + {futureAssignmentFilter(assignment) && ( + + )} + {activeAssignmentFilter(assignment) && !assignment.results.map((r) => r.user).includes(user.id) && ( <>
- +
)} )} {assignment.results.map((r) => r.user).includes(user.id) && ( diff --git a/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx b/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx index ba0d67c6..11673641 100644 --- a/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx +++ b/src/components/Medium/ModuleTitle/MCQuestionGrid.tsx @@ -3,105 +3,96 @@ import Modal from "@/components/Modal"; import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { BsFillGrid3X3GapFill } from "react-icons/bs"; interface Props { - showSolutions: boolean; - runOnClick: ((index: number) => void) | undefined; + exam: LevelExam + showSolutions: boolean; + runOnClick: ((index: number) => void) | undefined; } -const MCQuestionGrid: React.FC = ({showSolutions, runOnClick}) => { - const [isOpen, setIsOpen] = useState(false); +const MCQuestionGrid: React.FC = ({ exam, showSolutions, runOnClick }) => { + const [isOpen, setIsOpen] = useState(false); - const { + const { userSolutions, partIndex: sectionIndex, - exerciseIndex, - exam + exerciseIndex, } = useExamStore((state) => state); - const isMultipleChoiceLevelExercise = () => { - if (exam?.module === 'level' && typeof sectionIndex === "number" && sectionIndex > -1) { - const currentExercise = (exam as LevelExam).parts[sectionIndex].exercises[exerciseIndex]; - return currentExercise && currentExercise.type === 'multipleChoice'; - } - return false; - }; + const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex]) + const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions]) + const answeredQuestions = useMemo(() => new Set(userSolution.solutions.map(sol => sol.question.toString())), [userSolution.solutions]) + const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions]) + const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1), + [currentExercise.questions.length, exerciseOffset]); - if (!isMultipleChoiceLevelExercise() && !userSolutions) return null; + const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => { + const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { + if (foundMap) return foundMap; + return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null; + }, null as ShuffleMap | null); + const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution; - const currentExercise = (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise; - const userSolution = userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!; - const answeredQuestions = new Set(userSolution.solutions.map(sol => sol.question.toString())); - const exerciseOffset = Number(currentExercise.questions[0].id); - const lastExercise = exerciseOffset + (currentExercise.questions.length - 1); + if (!userSolutions) return ""; - const getQuestionColor = (questionId: string, solution: string, userQuestionSolution: string | undefined) => { - const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => { - if (foundMap) return foundMap; - return userSolution.shuffleMaps?.find(map => map.questionID.toString() === questionId.toString()) || null; - }, null as ShuffleMap | null); - const newSolution = questionShuffleMap ? questionShuffleMap?.map[solution] : solution; + if (!userQuestionSolution) { + return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700"; + } - if (!userSolutions) return ""; + return userQuestionSolution === newSolution ? + "!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" : + "!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark"; + } - if (!userQuestionSolution) { - return "!bg-mti-gray-davy !border--mti-gray-davy !text-mti-gray-davy !text-white hover:!bg-gray-700"; - } + return ( + <> + + setIsOpen(false)} + className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all" + > + <> +

{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}

+
+ {currentExercise.questions.map((_, index) => { + const questionNumber = exerciseOffset + index; + const isAnswered = answeredQuestions.has(questionNumber.toString()); + const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution; - return userQuestionSolution === newSolution ? - "!bg-mti-purple-light !text-mti-purple-light !text-white hover:!bg-mti-purple-dark" : - "!bg-mti-rose-light !border-mti-rose-light !text-mti-rose-light !text-white hover:!bg-mti-rose-dark"; - } - - return ( - <> - - setIsOpen(false)} - className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all" - > - <> -

{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}

-
- {currentExercise.questions.map((_, index) => { - const questionNumber = exerciseOffset + index; - const isAnswered = answeredQuestions.has(questionNumber.toString()); - const solution = currentExercise.questions.find((x) => x.id.toString() == questionNumber.toString())!.solution; - - const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option; - return ( - - ); - })} -
-

- Click a question number to jump to that question -

- -
- - ); + const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option; + return ( + + ); + })} +
+

+ Click a question number to jump to that question +

+ +
+ + ); } export default MCQuestionGrid; diff --git a/src/components/Medium/ModuleTitle/index.tsx b/src/components/Medium/ModuleTitle/index.tsx index f529b6f9..c22ed521 100644 --- a/src/components/Medium/ModuleTitle/index.tsx +++ b/src/components/Medium/ModuleTitle/index.tsx @@ -1,11 +1,11 @@ import { Module } from "@/interfaces"; import { moduleLabels } from "@/utils/moduleUtils"; import clsx from "clsx"; -import { ReactNode, useState } from "react"; +import { ReactNode, useMemo, useState } from "react"; import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen } from "react-icons/bs"; import ProgressBar from "../../Low/ProgressBar"; import Timer from "../Timer"; -import { Exercise } from "@/interfaces/exam"; +import { Exercise, LevelExam } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import React from "react"; import MCQuestionGrid from "./MCQuestionGrid"; @@ -38,9 +38,7 @@ export default function ModuleTitle({ showSolutions = false, runOnClick = undefined }: Props) { - const { - exam - } = useExamStore((state) => state); + const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state); const moduleIcon: { [key in Module]: ReactNode } = { reading: , @@ -50,6 +48,14 @@ export default function ModuleTitle({ level: , }; + const showGrid = useMemo(() => + exam?.module === "level" + && partIndex > -1 + && exam.parts[partIndex].exercises[examExerciseIndex].type === "multipleChoice" + && !!userSolutions, + [exam, examExerciseIndex, partIndex, userSolutions] + ) + return ( <> {showTimer && } @@ -67,7 +73,7 @@ export default function ModuleTitle({ return (
{partInstructions.split("\\n").map((line, lineIndex) => ( - not correct')}}> + not correct') }}> ))}
); @@ -87,9 +93,9 @@ export default function ModuleTitle({ - {exam?.module === "level" && } + {showGrid && } ); -} \ No newline at end of file +} diff --git a/src/dashboards/AssignmentCreator.tsx b/src/dashboards/AssignmentCreator.tsx index c3bdd792..89c58598 100644 --- a/src/dashboards/AssignmentCreator.tsx +++ b/src/dashboards/AssignmentCreator.tsx @@ -1,27 +1,27 @@ import Input from "@/components/Low/Input"; import Modal from "@/components/Modal"; -import {Module} from "@/interfaces"; +import { Module } from "@/interfaces"; import clsx from "clsx"; -import {useEffect, useMemo, useState} from "react"; -import {BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; -import {generate} from "random-words"; -import {capitalize} from "lodash"; +import { useEffect, useMemo, useState } from "react"; +import { BsBook, BsCheckCircle, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs"; +import { generate } from "random-words"; +import { capitalize } from "lodash"; import useUsers from "@/hooks/useUsers"; -import {Group, User} from "@/interfaces/user"; +import { Group, User } from "@/interfaces/user"; import ProgressBar from "@/components/Low/ProgressBar"; -import {calculateAverageLevel} from "@/utils/score"; +import { calculateAverageLevel } from "@/utils/score"; import Button from "@/components/Low/Button"; import ReactDatePicker from "react-datepicker"; import moment from "moment"; import axios from "axios"; -import {getExam} from "@/utils/exams"; -import {toast} from "react-toastify"; -import {Assignment} from "@/interfaces/results"; +import { getExam } from "@/utils/exams"; +import { toast } from "react-toastify"; +import { Assignment } from "@/interfaces/results"; import Checkbox from "@/components/Low/Checkbox"; -import {InstructorGender, Variant} from "@/interfaces/exam"; +import { InstructorGender, Variant } from "@/interfaces/exam"; import Select from "@/components/Low/Select"; import useExams from "@/hooks/useExams"; -import {useListSearch} from "@/hooks/useListSearch"; +import { useListSearch } from "@/hooks/useListSearch"; interface Props { isCreating: boolean; @@ -34,7 +34,7 @@ interface Props { const SIZE = 12; -export default function AssignmentCreator({isCreating, assignment, user, groups, users, cancelCreation}: Props) { +export default function AssignmentCreator({ isCreating, assignment, user, groups, users, cancelCreation }: Props) { const [studentsPage, setStudentsPage] = useState(0); const [teachersPage, setTeachersPage] = useState(0); @@ -43,14 +43,14 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, const [teachers, setTeachers] = useState(!!assignment ? assignment.teachers || [] : [...(user.type === "teacher" ? [user.id] : [])]); const [name, setName] = useState( assignment?.name || - generate({ - minLength: 6, - maxLength: 8, - min: 2, - max: 3, - join: " ", - formatter: capitalize, - }), + generate({ + minLength: 6, + maxLength: 8, + min: 2, + max: 3, + join: " ", + formatter: capitalize, + }), ); const [isLoading, setIsLoading] = useState(false); const [startDate, setStartDate] = useState(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "hour").toDate()); @@ -65,18 +65,17 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, const [released, setReleased] = useState(assignment?.released || false); const [autoStart, setAutostart] = useState(assignment?.autoStart || false); - const [autoStartDate, setAutoStartDate] = useState(assignment ? moment(assignment.autoStartDate).toDate() : new Date()); const [useRandomExams, setUseRandomExams] = useState(true); - const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); + const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]); - const {exams} = useExams(); + const { exams } = useExams(); const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]); const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]); - const {rows: filteredStudentsRows, renderSearch: renderStudentSearch, text: studentText} = useListSearch([["name"], ["email"]], userStudents); - const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText} = useListSearch([["name"], ["email"]], userTeachers); + const { rows: filteredStudentsRows, renderSearch: renderStudentSearch, text: studentText } = useListSearch([["name"], ["email"]], userStudents); + const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch, text: teacherText } = useListSearch([["name"], ["email"]], userTeachers); useEffect(() => setStudentsPage(0), [studentText]); const studentRows = useMemo( @@ -131,7 +130,6 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, instructorGender, released, autoStart, - autoStartDate, }) .then(() => { toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`); @@ -306,24 +304,6 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, onChange={(date) => setEndDate(date)} /> - {autoStart && ( -
- - moment(date).isSameOrAfter(new Date())} - dateFormat="dd/MM/yyyy HH:mm" - selected={autoStartDate} - showTimeSelect - onChange={(date) => setAutoStartDate(date)} - /> -
- )} {selectedModules.includes("speaking") && ( @@ -337,9 +317,9 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)} disabled={!selectedModules.includes("speaking") || !!assignment} options={[ - {value: "male", label: "Male"}, - {value: "female", label: "Female"}, - {value: "varied", label: "Varied"}, + { value: "male", label: "Male" }, + { value: "female", label: "Female" }, + { value: "varied", label: "Varied" }, ]} /> @@ -362,12 +342,12 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, }} onChange={(value) => value - ? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), {id: value.value!, module}]) + ? setExamIDs((prev) => [...prev.filter((x) => x.module !== module), { id: value.value!, module }]) : setExamIDs((prev) => prev.filter((x) => x.module !== module)) } options={exams .filter((x) => !x.isDiagnostic && x.module === module) - .map((x) => ({value: x.id, label: x.id}))} + .map((x) => ({ value: x.id, label: x.id }))} /> ))} @@ -394,7 +374,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "transition duration-300 ease-in-out", users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) && - "!bg-mti-purple-light !text-white", + "!bg-mti-purple-light !text-white", )}> {g.name} @@ -475,7 +455,7 @@ export default function AssignmentCreator({isCreating, assignment, user, groups, "bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light", "transition duration-300 ease-in-out", users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) && - "!bg-mti-purple-light !text-white", + "!bg-mti-purple-light !text-white", )}> {g.name} diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx index 9d89242d..8c3f9a3b 100644 --- a/src/exams/Finish.tsx +++ b/src/exams/Finish.tsx @@ -1,14 +1,14 @@ import Button from "@/components/Low/Button"; import ModuleTitle from "@/components/Medium/ModuleTitle"; -import {moduleResultText} from "@/constants/ielts"; -import {Module} from "@/interfaces"; -import {User} from "@/interfaces/user"; +import { moduleResultText } from "@/constants/ielts"; +import { Module } from "@/interfaces"; +import { User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import {calculateBandScore, getGradingLabel} from "@/utils/score"; +import { calculateBandScore, getGradingLabel } from "@/utils/score"; import clsx from "clsx"; import Link from "next/link"; -import {useRouter} from "next/router"; -import {Fragment, useEffect, useState} from "react"; +import { useRouter } from "next/router"; +import { Fragment, useEffect, useState } from "react"; import { BsArrowCounterclockwise, BsBan, @@ -21,14 +21,14 @@ import { BsPen, BsShareFill, } from "react-icons/bs"; -import {LevelScore} from "@/constants/ielts"; -import {getLevelScore} from "@/utils/score"; -import {capitalize} from "lodash"; +import { LevelScore } from "@/constants/ielts"; +import { getLevelScore } from "@/utils/score"; +import { capitalize } from "lodash"; import Modal from "@/components/Modal"; -import {UserSolution} from "@/interfaces/exam"; +import { UserSolution } from "@/interfaces/exam"; import ai_usage from "@/utils/ai.detection"; import useGradingSystem from "@/hooks/useGrading"; -import {Assignment} from "@/interfaces/results"; +import { Assignment } from "@/interfaces/results"; interface Score { module: Module; @@ -49,20 +49,23 @@ interface Props { isLoading: boolean; assignment?: Assignment; onViewResults: (moduleIndex?: number) => void; + destination?: string } -export default function Finish({user, scores, modules, information, solutions, isLoading, assignment, onViewResults}: Props) { +export default function Finish({ user, scores, modules, information, solutions, isLoading, assignment, onViewResults, destination }: Props) { const [selectedModule, setSelectedModule] = useState(modules[0]); const [selectedScore, setSelectedScore] = useState(scores.find((x) => x.module === modules[0])!); const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false); const aiUsage = Math.round(ai_usage(solutions) * 100); const exams = useExamStore((state) => state.exams); - const {gradingSystem} = useGradingSystem(); + const { gradingSystem } = useGradingSystem(); + + const router = useRouter() useEffect(() => setSelectedScore(scores.find((x) => x.module === selectedModule)!), [scores, selectedModule]); - const moduleColors: {[key in Module]: {progress: string; inner: string}} = { + const moduleColors: { [key in Module]: { progress: string; inner: string } } = { reading: { progress: "text-ielts-reading", inner: "bg-ielts-reading-light", @@ -286,10 +289,8 @@ export default function Finish({user, scores, modules, information, solutions, i
@@ -325,7 +326,7 @@ export default function Finish({user, scores, modules, information, solutions, i )}
- + diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index 3038b617..baef3ca1 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -18,62 +18,64 @@ import { Tab } from "@headlessui/react"; import Modal from "@/components/Modal"; import { typeCheckWordsMC } from "@/utils/type.check"; import SectionNavbar from "../Navigation/SectionNavbar"; +import AudioPlayer from "@/components/Low/AudioPlayer"; interface Props { - exam: LevelExam; - showSolutions?: boolean; - onFinish: (userSolutions: UserSolution[]) => void; - preview?: boolean; - partDividers?: boolean; + exam: LevelExam; + showSolutions?: boolean; + onFinish: (userSolutions: UserSolution[]) => void; + preview?: boolean; + partDividers?: boolean; } export default function Level({ exam, showSolutions = false, onFinish, preview = false }: Props) { - const levelBgColor = "bg-ielts-level-light"; + const levelBgColor = "bg-ielts-level-light"; - const examState = useExamStore((state) => state); - const persistentExamState = usePersistentExamStore((state) => state); + const examState = useExamStore((state) => state); + const persistentExamState = usePersistentExamStore((state) => state); - const { - userSolutions, - hasExamEnded, - partIndex, - exerciseIndex, - questionIndex, - shuffles, - currentSolution, - setBgColor, - setUserSolutions, - setHasExamEnded, - setPartIndex, - setExerciseIndex, - setQuestionIndex, - setShuffles, - setCurrentSolution - } = !preview ? examState : persistentExamState; + const { + userSolutions, + hasExamEnded, + partIndex, + exerciseIndex, + questionIndex, + shuffles, + currentSolution, + setBgColor, + setUserSolutions, + setHasExamEnded, + setPartIndex, + setExerciseIndex, + setQuestionIndex, + setShuffles, + setCurrentSolution + } = !preview ? examState : persistentExamState; - // In case client want to switch back - const textRenderDisabled = true; + // In case client want to switch back + const textRenderDisabled = true; - const [showSubmissionModal, setShowSubmissionModal] = useState(false); - const [showQuestionsModal, setShowQuestionsModal] = useState(false); - const [continueAnyways, setContinueAnyways] = useState(false); - const [textRender, setTextRender] = useState(false); - const [changedPrompt, setChangedPrompt] = useState(false); - const [nextExerciseCalled, setNextExerciseCalled] = useState(false); - const [currentSolutionSet, setCurrentSolutionSet] = useState(false); + const [timesListened, setTimesListened] = useState(0); + const [showSubmissionModal, setShowSubmissionModal] = useState(false); + const [showQuestionsModal, setShowQuestionsModal] = useState(false); + const [continueAnyways, setContinueAnyways] = useState(false); + const [textRender, setTextRender] = useState(false); + const [changedPrompt, setChangedPrompt] = useState(false); + const [nextExerciseCalled, setNextExerciseCalled] = useState(false); + const [currentSolutionSet, setCurrentSolutionSet] = useState(false); - const [seenParts, setSeenParts] = useState>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0])); + const [seenParts, setSeenParts] = useState>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0])); - const [questionModalKwargs, setQuestionModalKwargs] = useState<{ - type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; - }>({ - type: "blankQuestions", - onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } - }); + const [questionModalKwargs, setQuestionModalKwargs] = useState<{ + type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; + }>({ + type: "blankQuestions", + onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } + }); - const [currentExercise, setCurrentExercise] = useState(undefined); - const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); - const [startNow, setStartNow] = useState(!showSolutions); + const [currentExercise, setCurrentExercise] = useState(undefined); + const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); + const [startNow, setStartNow] = useState(!showSolutions); useEffect(() => { @@ -84,445 +86,483 @@ export default function Level({ exam, showSolutions = false, onFinish, preview = // eslint-disable-next-line react-hooks/exhaustive-deps }, [exerciseIndex]); - useEffect(() => { - if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) { - setCurrentExercise(exam.parts[0].exercises[0]); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentExercise, partIndex, exerciseIndex]); + useEffect(() => { + if (currentExercise === undefined && partIndex === 0 && exerciseIndex === 0) { + setCurrentExercise(exam.parts[0].exercises[0]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentExercise, partIndex, exerciseIndex]); - 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)); - const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); - const [contextWordLines, setContextWordLines] = useState(undefined); - const [totalLines, setTotalLines] = useState(0); + const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); + const [contextWordLines, setContextWordLines] = useState(undefined); + const [totalLines, setTotalLines] = useState(0); - 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 (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!] : [] }]); - setCurrentSolutionSet(true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise]) + useEffect(() => { + 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!] : [] }]); + setCurrentSolutionSet(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentSolution, exam.id, exam.shuffle, shuffles, currentExercise]) - useEffect(() => { - if (typeof currentSolution !== "undefined") { - setCurrentSolution(undefined); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentSolution]); + useEffect(() => { + if (typeof currentSolution !== "undefined") { + setCurrentSolution(undefined); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentSolution]); - useEffect(() => { - if (showSolutions) { - const solutionShuffles = userSolutions.map(solution => ({ - exerciseID: solution.exercise, - shuffles: solution.shuffleMaps || [] - })); - setShuffles(solutionShuffles); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useEffect(() => { + if (showSolutions) { + const solutionShuffles = userSolutions.map(solution => ({ + exerciseID: solution.exercise, + shuffles: solution.shuffleMaps || [] + })); + setShuffles(solutionShuffles); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const getExercise = () => { - let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; - exercise = { - ...exercise, - userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [], - }; - exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); - return exercise; - }; + const getExercise = () => { + let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; + exercise = { + ...exercise, + userSolutions: userSolutions.find((x) => x.exercise == exercise.id)?.solutions || [], + }; + exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); + return exercise; + }; - useEffect(() => { - setCurrentExercise(getExercise()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [partIndex, exerciseIndex, questionIndex]); + useEffect(() => { + setCurrentExercise(getExercise()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [partIndex, exerciseIndex, questionIndex]); - const next = () => { - setNextExerciseCalled(true); - } + const next = () => { + setNextExerciseCalled(true); + } - const nextExercise = () => { - scrollToTop(); + const nextExercise = () => { + scrollToTop(); - if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { - setExerciseIndex(exerciseIndex + 1); - setCurrentSolutionSet(false); - return; - } + if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { + setExerciseIndex(exerciseIndex + 1); + setCurrentSolutionSet(false); + return; + } - if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { - modalKwargs(); - setShowQuestionsModal(true); - return; - } + if (partIndex + 1 === exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions && !continueAnyways) { + modalKwargs(); + setShowQuestionsModal(true); + return; + } - if (partIndex + 1 < exam.parts.length && !hasExamEnded) { - if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) { - modalKwargs(); - setShowQuestionsModal(true); - return; - } + if (partIndex + 1 < exam.parts.length && !hasExamEnded) { + if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) { + modalKwargs(); + setShowQuestionsModal(true); + return; + } - if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) { - setShowPartDivider(true); - setBgColor(levelBgColor); - } + if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) { + setShowPartDivider(true); + setBgColor(levelBgColor); + } - setSeenParts(prev => new Set(prev).add(partIndex + 1)); + setSeenParts(prev => new Set(prev).add(partIndex + 1)); - if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) { - setTextRender(true); - } - setPartIndex(partIndex + 1); - setExerciseIndex(0); - setQuestionIndex(0); - setCurrentSolutionSet(false); - return; - } + if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) { + setTextRender(true); + } - if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) { - modalKwargs(); - setShowQuestionsModal(true); - } + setTimesListened(0); + setPartIndex(partIndex + 1); + setExerciseIndex(0); + setQuestionIndex(0); + setCurrentSolutionSet(false); + return; + } - setHasExamEnded(false); - setCurrentSolutionSet(false); - if (typeof showSolutionsSave !== "undefined") { - onFinish(showSolutionsSave); - } else { - onFinish(userSolutions); - } - } + if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) { + modalKwargs(); + setShowQuestionsModal(true); + } - useEffect(() => { - if (nextExerciseCalled && currentSolutionSet) { - nextExercise(); - setNextExerciseCalled(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nextExerciseCalled, currentSolutionSet]) + setHasExamEnded(false); + setCurrentSolutionSet(false); + if (typeof showSolutionsSave !== "undefined") { + onFinish(showSolutionsSave); + } else { + onFinish(userSolutions); + } + } - const previousExercise = (solution?: UserSolution) => { - scrollToTop(); + useEffect(() => { + if (nextExerciseCalled && currentSolutionSet) { + nextExercise(); + setNextExerciseCalled(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nextExerciseCalled, currentSolutionSet]) - if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) { - setTextRender(true); - return; - } + const previousExercise = (solution?: UserSolution) => { + scrollToTop(); - if (questionIndex == 0) { - setPartIndex(partIndex - 1); - if (!seenParts.has(partIndex - 1)) { - setBgColor(levelBgColor); - setShowPartDivider(true); - setQuestionIndex(0); - setSeenParts(prev => new Set(prev).add(partIndex - 1)); - return; - } + if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) { + setTextRender(true); + return; + } - const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; - const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; - setExerciseIndex(lastExerciseIndex); + if (questionIndex == 0) { + setPartIndex(partIndex - 1); + if (!seenParts.has(partIndex - 1)) { + setBgColor(levelBgColor); + setShowPartDivider(true); + setQuestionIndex(0); + setSeenParts(prev => new Set(prev).add(partIndex - 1)); + return; + } - if (lastExercise.type === "multipleChoice") { - setQuestionIndex(lastExercise.questions.length - 1) - } else { - setQuestionIndex(0) - } - return; - } + const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; + const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; + setExerciseIndex(lastExerciseIndex); - setExerciseIndex(exerciseIndex - 1); - if (exerciseIndex - 1 === -1) { - setPartIndex(partIndex - 1); - const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; - const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; - if (previousExercise.type === "multipleChoice") { - setQuestionIndex(previousExercise.questions.length - 1) - } - } + if (lastExercise.type === "multipleChoice") { + setQuestionIndex(lastExercise.questions.length - 1) + } else { + setQuestionIndex(0) + } + return; + } - }; + setExerciseIndex(exerciseIndex - 1); + if (exerciseIndex - 1 === -1) { + setPartIndex(partIndex - 1); + const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; + const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; + if (previousExercise.type === "multipleChoice") { + setQuestionIndex(previousExercise.questions.length - 1) + } + } - const calculateExerciseIndex = () => { - return exam.parts.reduce((acc, curr, index) => { - if (index < partIndex) { - return acc + countExercises(curr.exercises) - } - return acc; - }, 0) + (questionIndex + 1); - }; + }; - const renderText = () => ( - <> -
- <> -
- {textRender && !textRenderDisabled ? ( - <> -

- Please read the following excerpt attentively, you will then be asked questions about the text you've read. -

- You will be allowed to read the text while doing the exercises - - ) : ( -

- Answer the questions on the right based on what you've read. -

- )} -
- {exam.parts[partIndex].context && - } -
- -
- {textRender && !textRenderDisabled && ( -
- + const calculateExerciseIndex = () => { + return exam.parts.reduce((acc, curr, index) => { + if (index < partIndex) { + return acc + countExercises(curr.exercises) + } + return acc; + }, 0) + (questionIndex + 1); + }; - -
- )} - - ); + const renderAudioPlayer = () => ( +
+ {exam?.parts[partIndex]?.audio?.source ? ( + <> +
+

Please listen to the following audio attentively.

+ + {(() => { + const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; + return audioRepeatTimes && audioRepeatTimes > 0 + ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` + : "You may listen to the audio as many times as you would like."; + })()} + +
+
+ setTimesListened((prev) => prev + 1)} + disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null && + timesListened === exam.parts[partIndex]?.audio?.repeatableTimes} + disablePause + /> +
+ + ) : ( + This section will be displayed the audio once it has been generated. + )} - const partLabel = () => { - const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : ''; - if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) - return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` +
+ ); - if (currentExercise?.type === "multipleChoice") { - return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` - } + const renderText = () => ( + <> +
+ <> +
+ {textRender && !textRenderDisabled ? ( + <> +

+ Please read the following excerpt attentively, you will then be asked questions about the text you've read. +

+ You will be allowed to read the text while doing the exercises + + ) : ( +

+ Answer the questions on the right based on what you've read. +

+ )} +
+ {(exam.parts[partIndex].context || exam.parts[partIndex].text) && + } +
+ +
+ {textRender && !textRenderDisabled && ( +
+ - if (typeof exam.parts[partIndex].context === "string") { - const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise; - return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}` - } - } + +
+ )} + + ); - const answeredEveryQuestion = (partIndex: number) => { - return exam.parts[partIndex].exercises.every((exercise) => { - const userSolution = userSolutions.find(x => x.exercise === exercise.id); - switch (exercise.type) { - case 'multipleChoice': - return userSolution?.solutions.length === exercise.questions.length; - case 'fillBlanks': - return userSolution?.solutions.length === exercise.words.length; - case 'writeBlanks': - return userSolution?.solutions.length === exercise.solutions.length; - case 'matchSentences': - return userSolution?.solutions.length === exercise.sentences.length; - case 'trueFalse': - return userSolution?.solutions.length === exercise.questions.length; - } - return false; - }); - } + const partLabel = () => { + const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : ''; + if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) + return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` - useEffect(() => { - const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; + if (currentExercise?.type === "multipleChoice") { + return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` + } - const findMatch = (index: number) => { - if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) { - const match = currentExercise!.questions[index].prompt.match(regex); - if (match) { - return { match: match[1], originalLine: match[2] } - } - } - return; - } + if (typeof exam.parts[partIndex].context === "string") { + const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise; + return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}` + } + } - // if the client for some whatever random reason decides - // to add more questions update this - const numberOfQuestions = 2; + const answeredEveryQuestion = (partIndex: number) => { + return exam.parts[partIndex].exercises.every((exercise) => { + const userSolution = userSolutions.find(x => x.exercise === exercise.id); + switch (exercise.type) { + case 'multipleChoice': + return userSolution?.solutions.length === exercise.questions.length; + case 'fillBlanks': + return userSolution?.solutions.length === exercise.words.length; + case 'writeBlanks': + return userSolution?.solutions.length === exercise.solutions.length; + case 'matchSentences': + return userSolution?.solutions.length === exercise.sentences.length; + case 'trueFalse': + return userSolution?.solutions.length === exercise.questions.length; + } + return false; + }); + } - if (exam.parts[partIndex].context) { - const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => { - const result = findMatch(questionIndex + i); - if (!!result) { - acc.push(result); - } - return acc; - }, []); + useEffect(() => { + const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; - if (hits.length > 0) { - setContextWords(hits) - } - } + const findMatch = (index: number) => { + if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) { + const match = currentExercise!.questions[index].prompt.match(regex); + if (match) { + return { match: match[1], originalLine: match[2] } + } + } + return; + } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentExercise, questionIndex, totalLines]); + // if the client for some whatever random reason decides + // to add more questions update this + const numberOfQuestions = 2; - useEffect(() => { - if ( - exerciseIndex !== -1 && currentExercise && - currentExercise.type === "multipleChoice" && - exam.parts[partIndex].context && contextWordLines - ) { - if (contextWordLines.length > 0) { - contextWordLines.forEach((n, i) => { - if (contextWords && contextWords[i] && n !== -1) { - const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace( - `in line ${contextWords[i].originalLine}`, - `in line ${n}` - ); - currentExercise!.questions[questionIndex + i].prompt = updatedPrompt; - } - }) - setChangedPrompt(true); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [contextWordLines]); + if (exam.parts[partIndex].context) { + const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => { + const result = findMatch(questionIndex + i); + if (!!result) { + acc.push(result); + } + return acc; + }, []); + + if (hits.length > 0) { + setContextWords(hits) + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentExercise, questionIndex, totalLines]); + + useEffect(() => { + if ( + exerciseIndex !== -1 && currentExercise && + currentExercise.type === "multipleChoice" && + exam.parts[partIndex].context && contextWordLines + ) { + if (contextWordLines.length > 0) { + contextWordLines.forEach((n, i) => { + if (contextWords && contextWords[i] && n !== -1) { + const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace( + `in line ${contextWords[i].originalLine}`, + `in line ${n}` + ); + currentExercise!.questions[questionIndex + i].prompt = updatedPrompt; + } + }) + setChangedPrompt(true); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contextWordLines]); - useEffect(() => { - if (continueAnyways) { - setContinueAnyways(false); - nextExercise(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [continueAnyways]); + useEffect(() => { + if (continueAnyways) { + setContinueAnyways(false); + nextExercise(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [continueAnyways]); - const modalKwargs = () => { - const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { - type: "blankQuestions", - unanswered: false, - onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } - }; + const modalKwargs = () => { + const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { + type: "blankQuestions", + unanswered: false, + onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } + }; - if (partIndex === exam.parts.length - 1) { - kwargs.type = "submit" - kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex)); - kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; - } - setQuestionModalKwargs(kwargs); - } + if (partIndex === exam.parts.length - 1) { + kwargs.type = "submit" + kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex)); + kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; + } + setQuestionModalKwargs(kwargs); + } - const mcNavKwargs = { - userSolutions: userSolutions, - exam: exam, - partIndex: partIndex, - showSolutions: showSolutions, - setExerciseIndex: setExerciseIndex, - setPartIndex: setPartIndex, - runOnClick: setQuestionIndex - } + const mcNavKwargs = { + userSolutions: userSolutions, + exam: exam, + partIndex: partIndex, + showSolutions: showSolutions, + setExerciseIndex: setExerciseIndex, + setPartIndex: setPartIndex, + runOnClick: setQuestionIndex + } - const memoizedRender = useMemo(() => { - setChangedPrompt(false); - return ( - <> - {textRender && !textRenderDisabled ? - renderText() : - <> - {exam.parts[partIndex]?.context && renderText()} - {(showSolutions) ? - currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) : - currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise) - } - - } - ) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textRender, currentExercise, changedPrompt]); + const memoizedRender = useMemo(() => { + setChangedPrompt(false); + return ( + <> + {textRender && !textRenderDisabled ? + renderText() : + <> + {exam.parts[partIndex]?.context && renderText()} + {exam.parts[partIndex]?.audio && renderAudioPlayer()} + {(showSolutions) ? + currentExercise && renderSolution(currentExercise, nextExercise, previousExercise) : + currentExercise && renderExercise(currentExercise, exam.id, next, previousExercise) + } + + } + + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [textRender, currentExercise, changedPrompt]); - return ( - <> -
- { }} - title={"Confirm Submission"} - > - <> -

Are you sure you want to proceed with the submission?

-
- - -
- -
- - { - !(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) && - - } - {(showPartDivider || startNow) ? - { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} - /> : ( - <> - {exam.parts[0].intro && exam.parts.length !== 1 && ( - { - setExerciseIndex(0); - setQuestionIndex(0); - if (!seenParts.has(index)) { - setShowPartDivider(true); - setBgColor(levelBgColor); - setSeenParts(prev => new Set(prev).add(index)); - } - } - } /> - )} - x.exercises))} - disableTimer={showSolutions} - showTimer={false} - {...mcNavKwargs} - /> -
- {memoizedRender} -
- - )} -
- - ); + return ( + <> +
+ { }} + title={"Confirm Submission"} + > + <> +

Are you sure you want to proceed with the submission?

+
+ + +
+ +
+ + { + !(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) && + + } + {(showPartDivider || startNow) ? + { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); }} + /> : ( + <> + {exam.parts[0].intro && ( + { + setExerciseIndex(0); + setQuestionIndex(0); + if (!seenParts.has(index)) { + setShowPartDivider(true); + setBgColor(levelBgColor); + setSeenParts(prev => new Set(prev).add(index)); + } + } + } /> + )} + x.exercises))} + disableTimer={showSolutions} + showTimer={false} + {...mcNavKwargs} + /> +
+ {memoizedRender} +
+ + )} +
+ + ); } diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts index cd9ff373..58832a4f 100644 --- a/src/interfaces/results.ts +++ b/src/interfaces/results.ts @@ -1,8 +1,8 @@ -import {Module} from "@/interfaces"; -import {InstructorGender} from "./exam"; -import {Stat} from "./user"; +import { Module } from "@/interfaces"; +import { InstructorGender } from "./exam"; +import { Stat } from "./user"; -export type UserResults = {[key in Module]: ModuleResult}; +export type UserResults = { [key in Module]: ModuleResult }; interface ModuleResult { exams: string[]; @@ -22,7 +22,7 @@ export interface Assignment { assigner: string; assignees: string[]; results: AssignmentResult[]; - exams: {id: string; module: Module; assignee: string}[]; + exams: { id: string; module: Module; assignee: string }[]; instructorGender?: InstructorGender; startDate: Date; endDate: Date; @@ -32,9 +32,8 @@ export interface Assignment { // unless start is active, the assignment is not visible to the assignees // start date now works as a limit time to start the exam start?: boolean; - autoStartDate?: Date; autoStart?: boolean; entity?: string; } -export type AssignmentWithCorporateId = Assignment & {corporateId: string}; +export type AssignmentWithCorporateId = Assignment & { corporateId: string }; diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 79069e30..480310ac 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,15 +12,15 @@ import Selection from "@/exams/Selection"; import Speaking from "@/exams/Speaking"; import Writing from "@/exams/Writing"; import useUser from "@/hooks/useUser"; -import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam"; -import {Stat, User} from "@/interfaces/user"; +import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam"; +import { Stat, User } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; -import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; -import {defaultExamUserSolutions, getExam} from "@/utils/exams"; +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 { 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"; import clsx from "clsx"; @@ -31,10 +31,11 @@ import { mapBy } from "@/utils"; interface Props { page: "exams" | "exercises"; user: User; + destination?: string hideSidebar?: boolean } -export default function ExamPage({page, user, hideSidebar = false}: Props) { +export default function ExamPage({ page, user, destination = "/exam", hideSidebar = false }: Props) { const [variant, setVariant] = useState("full"); const [avoidRepeated, setAvoidRepeated] = useState(false); const [hasBeenUploaded, setHasBeenUploaded] = useState(false); @@ -49,18 +50,18 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { const assignment = useExamStore((state) => state.assignment); const initialTimeSpent = useExamStore((state) => state.timeSpent); - 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 {inactivity, setInactivity} = useExamStore((state) => state); - const {bgColor, setBgColor} = useExamStore((state) => state); + 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 { inactivity, setInactivity } = useExamStore((state) => state); + const { bgColor, setBgColor } = useExamStore((state) => state); const setShuffleMaps = useExamStore((state) => state.setShuffles); const router = useRouter(); @@ -262,11 +263,11 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { date: new Date().getTime(), isDisabled: solution.isDisabled, shuffleMaps: solution.shuffleMaps, - ...(assignment ? {assignment: assignment.id} : {}), + ...(assignment ? { assignment: assignment.id } : {}), })); axios - .post<{ok: boolean}>("/api/stats", newStats) + .post<{ ok: boolean }>("/api/stats", newStats) .then((response) => setHasBeenUploaded(response.data.ok)) .catch(() => setHasBeenUploaded(false)); } @@ -329,7 +330,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { ), }), ); - return Object.assign(exam, {parts}); + return Object.assign(exam, { parts }); } const exercises = exam.exercises.map((x) => @@ -337,7 +338,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions, }), ); - return Object.assign(exam, {exercises}); + return Object.assign(exam, { exercises }); }; const onFinish = async (solutions: UserSolution[]) => { @@ -392,7 +393,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { correct: number; }[] => { const scores: { - [key in Module]: {total: number; missing: number; correct: number}; + [key in Module]: { total: number; missing: number; correct: number }; } = { reading: { total: 0, @@ -434,7 +435,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { return Object.keys(scores) .filter((x) => scores[x as Module].total > 0) - .map((x) => ({module: x as Module, ...scores[x as Module]})); + .map((x) => ({ module: x as Module, ...scores[x as Module] })); }; const renderScreen = () => { @@ -465,6 +466,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) { timeSpent, inactivity: totalInactivity, }} + destination={destination} onViewResults={(index?: number) => { if (exams[0].module === "level") { const levelExam = exams[0] as LevelExam; diff --git a/src/pages/assignments/creator/[id].tsx b/src/pages/assignments/creator/[id].tsx index 4f3e8bca..0f8dcdf1 100644 --- a/src/pages/assignments/creator/[id].tsx +++ b/src/pages/assignments/creator/[id].tsx @@ -6,43 +6,43 @@ import ProgressBar from "@/components/Low/ProgressBar"; import Select from "@/components/Low/Select"; import Separator from "@/components/Low/Separator"; import useExams from "@/hooks/useExams"; -import {useListSearch} from "@/hooks/useListSearch"; +import { useListSearch } from "@/hooks/useListSearch"; import usePagination from "@/hooks/usePagination"; -import {Module} from "@/interfaces"; -import {EntityWithRoles} from "@/interfaces/entity"; -import {InstructorGender, Variant} from "@/interfaces/exam"; -import {Assignment} from "@/interfaces/results"; -import {Group, User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {mapBy, redirect, serialize} from "@/utils"; +import { Module } from "@/interfaces"; +import { EntityWithRoles } from "@/interfaces/entity"; +import { InstructorGender, Variant } from "@/interfaces/exam"; +import { Assignment } from "@/interfaces/results"; +import { Group, User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; -import {getAssignment} from "@/utils/assignments.be"; -import {getEntitiesWithRoles} from "@/utils/entities.be"; -import {getGroups, getGroupsByEntities} from "@/utils/groups.be"; -import {checkAccess, doesEntityAllow, findAllowedEntities} from "@/utils/permissions"; -import {calculateAverageLevel} from "@/utils/score"; -import {getEntitiesUsers, getUsers} from "@/utils/users.be"; +import { getAssignment } from "@/utils/assignments.be"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; +import { getGroups, getGroupsByEntities } from "@/utils/groups.be"; +import { checkAccess, doesEntityAllow, findAllowedEntities } from "@/utils/permissions"; +import { calculateAverageLevel } from "@/utils/score"; +import { getEntitiesUsers, getUsers } from "@/utils/users.be"; import axios from "axios"; import clsx from "clsx"; -import {withIronSessionSsr} from "iron-session/next"; -import {capitalize} from "lodash"; +import { withIronSessionSsr } from "iron-session/next"; +import { capitalize } from "lodash"; import moment from "moment"; import Head from "next/head"; import Link from "next/link"; -import {useRouter} from "next/router"; -import {generate} from "random-words"; -import {useEffect, useMemo, useState} from "react"; +import { useRouter } from "next/router"; +import { generate } from "random-words"; +import { useEffect, useMemo, useState } from "react"; import ReactDatePicker from "react-datepicker"; -import {BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs"; -import {toast} from "react-toastify"; +import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs"; +import { toast } from "react-toastify"; -export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { +export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => { const user = await requestUser(req, res) if (!user) return redirect("/login") res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59"); - const {id} = params as {id: string}; + const { id } = params as { id: string }; const entityIDS = mapBy(user.entities, "id") || []; const assignment = await getAssignment(id); @@ -59,7 +59,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id'))); const groups = await (checkAccess(user, ["developer", "admin"]) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id'))); - return {props: serialize({user, users, entities: allowedEntities, assignment, groups})}; + return { props: serialize({ user, users, entities: allowedEntities, assignment, groups }) }; }, sessionOptions); interface Props { @@ -72,7 +72,7 @@ interface Props { const SIZE = 9; -export default function AssignmentsPage({assignment, user, users, entities, groups}: Props) { +export default function AssignmentsPage({ assignment, user, users, entities, groups }: Props) { const [selectedModules, setSelectedModules] = useState(assignment.exams.map((e) => e.module)); const [assignees, setAssignees] = useState(assignment.assignees); const [teachers, setTeachers] = useState(assignment.teachers || []); @@ -90,12 +90,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou const [released, setReleased] = useState(assignment.released || false); const [autoStart, setAutostart] = useState(assignment.autoStart || false); - const [autoStartDate, setAutoStartDate] = useState(moment(assignment.autoStartDate).toDate()); const [useRandomExams, setUseRandomExams] = useState(true); - const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]); + const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]); - const {exams} = useExams(); + const { exams } = useExams(); const router = useRouter(); const classrooms = useMemo(() => groups.filter((e) => e.entity === entity), [entity, groups]); @@ -103,11 +102,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]); const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]); - const {rows: filteredStudentsRows, renderSearch: renderStudentSearch} = useListSearch([["name"], ["email"]], userStudents); - const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch} = useListSearch([["name"], ["email"]], userTeachers); + const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents); + const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers); - const {items: studentRows, renderMinimal: renderStudentPagination} = usePagination(filteredStudentsRows, SIZE); - const {items: teacherRows, renderMinimal: renderTeacherPagination} = usePagination(filteredTeachersRows, SIZE); + const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE); + const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE); useEffect(() => { setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module))); @@ -148,7 +147,6 @@ export default function AssignmentsPage({assignment, user, users, entities, grou instructorGender, released, autoStart, - autoStartDate, }) .then(() => { toast.success(`The assignment "${name}" has been updated successfully!`); @@ -316,9 +314,9 @@ export default function AssignmentsPage({assignment, user, users, entities, grou setName(e)} defaultValue={name} label="Assignment Name" required /> setName(e)} defaultValue={name} label="Assignment Name" required />