Merge remote-tracking branch 'origin/develop' into feature/ExamGenRework
This commit is contained in:
@@ -73,7 +73,7 @@ const ListeningContext: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
"h-5 w-5",
|
||||
`text-ielts-${currentModule}`
|
||||
)} />
|
||||
<span className="font-medium text-gray-900">Conversation</span>
|
||||
<span className="font-medium text-gray-900">{(sectionId === 1 || sectionId === 3) ? "Conversation" : "Monologue"}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
||||
))}
|
||||
</div>
|
||||
{!assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
{futureAssignmentFilter(assignment) && (
|
||||
<Button
|
||||
color="rose"
|
||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||
disabled
|
||||
variant="outline">
|
||||
Not yet started
|
||||
</Button>
|
||||
)}
|
||||
{activeAssignmentFilter(assignment) && !assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<>
|
||||
<div
|
||||
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||
@@ -73,22 +83,22 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
||||
className={clsx(
|
||||
"-md:hidden h-full w-full max-w-[50%] cursor-pointer"
|
||||
)}>
|
||||
<Button
|
||||
className={clsx("w-full h-full !rounded-xl")}
|
||||
onClick={() => resumeAssignment(session)}
|
||||
color="green"
|
||||
variant="outline">
|
||||
Resume
|
||||
</Button>
|
||||
<Button
|
||||
className={clsx("w-full h-full !rounded-xl")}
|
||||
onClick={() => resumeAssignment(session)}
|
||||
color="green"
|
||||
variant="outline">
|
||||
Resume
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
||||
<Button
|
||||
onClick={() => router.push("/record")}
|
||||
color="green"
|
||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||
disabled
|
||||
variant="outline">
|
||||
Submitted
|
||||
</Button>
|
||||
|
||||
@@ -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<Props> = ({showSolutions, runOnClick}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const MCQuestionGrid: React.FC<Props> = ({ 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 (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg">
|
||||
<BsFillGrid3X3GapFill size={24} />
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all"
|
||||
>
|
||||
<>
|
||||
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3>
|
||||
<div className="grid grid-cols-5 gap-3 px-4 py-2">
|
||||
{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 (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setIsOpen(true)} padding="p-2" className="rounded-lg">
|
||||
<BsFillGrid3X3GapFill size={24} />
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white shadow-xl transition-all"
|
||||
>
|
||||
<>
|
||||
<h3 className="text-xl font-semibold mb-4 text-center">{`Part ${sectionIndex + 1} (Questions ${exerciseOffset} - ${lastExercise})`}</h3>
|
||||
<div className="grid grid-cols-5 gap-3 px-4 py-2">
|
||||
{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 (
|
||||
<Button
|
||||
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
|
||||
key={index}
|
||||
className={clsx(
|
||||
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
|
||||
(showSolutions ?
|
||||
getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
|
||||
(isAnswered ?
|
||||
"bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
|
||||
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
||||
)
|
||||
)
|
||||
)}
|
||||
onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
|
||||
>
|
||||
{questionNumber}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-600 text-center">
|
||||
Click a question number to jump to that question
|
||||
</p>
|
||||
</>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
const userQuestionSolution = currentExercise.userSolutions?.find((x) => x.question.toString() == questionNumber.toString())?.option;
|
||||
return (
|
||||
<Button
|
||||
variant={showSolutions ? "solid" : (isAnswered ? "solid" : "outline")}
|
||||
key={index}
|
||||
className={clsx(
|
||||
"w-12 h-12 flex items-center justify-center rounded-lg text-sm font-bold transition-all duration-200 ease-in-out",
|
||||
(showSolutions ?
|
||||
getQuestionColor(questionNumber.toString(), solution, userQuestionSolution) :
|
||||
(isAnswered ?
|
||||
"bg-mti-purple-light border-mti-purple-light text-white hover:bg-mti-purple-dark hover:border-mti-purple-dark" :
|
||||
"bg-white border-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
||||
)
|
||||
)
|
||||
)}
|
||||
onClick={() => { if (typeof runOnClick !== "undefined") { runOnClick(index); } setIsOpen(false); }}
|
||||
>
|
||||
{questionNumber}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-600 text-center">
|
||||
Click a question number to jump to that question
|
||||
</p>
|
||||
</>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MCQuestionGrid;
|
||||
|
||||
@@ -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: <BsBook className="text-ielts-reading w-6 h-6" />,
|
||||
@@ -50,6 +48,14 @@ export default function ModuleTitle({
|
||||
level: <BsClipboard className="text-ielts-level w-6 h-6" />,
|
||||
};
|
||||
|
||||
const showGrid = useMemo(() =>
|
||||
exam?.module === "level"
|
||||
&& partIndex > -1
|
||||
&& exam.parts[partIndex].exercises[examExerciseIndex].type === "multipleChoice"
|
||||
&& !!userSolutions,
|
||||
[exam, examExerciseIndex, partIndex, userSolutions]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTimer && <Timer minTimer={minTimer} disableTimer={disableTimer} />}
|
||||
@@ -67,7 +73,7 @@ export default function ModuleTitle({
|
||||
return (
|
||||
<div key={index} className="text-2xl font-semibold flex flex-col gap-2">
|
||||
{partInstructions.split("\\n").map((line, lineIndex) => (
|
||||
<span key={lineIndex} dangerouslySetInnerHTML={{__html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></span>
|
||||
<span key={lineIndex} dangerouslySetInnerHTML={{ __html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>') }}></span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -87,9 +93,9 @@ export default function ModuleTitle({
|
||||
</div>
|
||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||
</div>
|
||||
{exam?.module === "level" && <MCQuestionGrid showSolutions={showSolutions} runOnClick={runOnClick}/>}
|
||||
{showGrid && <MCQuestionGrid exam={exam as LevelExam} showSolutions={showSolutions} runOnClick={runOnClick} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user