ENCOA-222 & ENCOA-223
ENCOA-222: Added an option for non-assignment exams to view the transcript of a Listening audio; ENCOA-223: Updated the Listening exam to show all of the exercises/questions of each part on a single page;
This commit is contained in:
@@ -20,8 +20,9 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
preview,
|
preview,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const examState = useExamStore((state) => state);
|
const examState = useExamStore((state) => state);
|
||||||
const persistentExamState = usePersistentExamStore((state) => state);
|
const persistentExamState = usePersistentExamStore((state) => state);
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||||
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles;
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const excludeWordMCType = (x: any) => {
|
const excludeWordMCType = (x: any) => {
|
||||||
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
return typeof x === "string" ? x : (x as { letter: string; word: string });
|
||||||
};
|
};
|
||||||
@@ -55,16 +56,16 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
setOpenDropdownId(null);
|
setOpenDropdownId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
@@ -105,18 +106,18 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
const id = match.replaceAll(/[\{\}]/g, "");
|
const id = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = answers.find((x) => x.id === id);
|
const userSolution = answers.find((x) => x.id === id);
|
||||||
const styles = clsx(
|
const styles = clsx(
|
||||||
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit inline-block",
|
"rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center w-fit inline-block",
|
||||||
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
!userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight",
|
||||||
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight",
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentSelection = words.find((x) => {
|
const currentSelection = words.find((x) => {
|
||||||
if (typeof x !== "string" && "id" in x) {
|
if (typeof x !== "string" && "id" in x) {
|
||||||
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
return (x as FillBlanksMCOption).id.toString() == id.toString();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}) as FillBlanksMCOption;
|
}) as FillBlanksMCOption;
|
||||||
|
|
||||||
return variant === "mc" ? (
|
return variant === "mc" ? (
|
||||||
<MCDropdown
|
<MCDropdown
|
||||||
id={id}
|
id={id}
|
||||||
@@ -126,7 +127,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
className="inline-block py-2 px-1 align-middle"
|
className="inline-block py-2 px-1 align-middle"
|
||||||
width={220}
|
width={220}
|
||||||
isOpen={openDropdownId === id}
|
isOpen={openDropdownId === id}
|
||||||
onToggle={()=> setOpenDropdownId(prevId => prevId === id ? null : id)}
|
onToggle={() => setOpenDropdownId(prevId => prevId === id ? null : id)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@@ -141,7 +142,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
},
|
},
|
||||||
[variant, words, answers, openDropdownId],
|
[variant, words, answers, openDropdownId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoizedLines = useMemo(() => {
|
const memoizedLines = useMemo(() => {
|
||||||
return text.split("\\n").map((line, index) => (
|
return text.split("\\n").map((line, index) => (
|
||||||
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
<p key={index} className={clsx(variant === "mc" && "whitespace-pre-wrap")}>
|
||||||
@@ -163,29 +164,33 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers]);
|
}, [answers]);
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
|
Previous Page
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => {
|
||||||
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
|
}}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next Page
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
|
||||||
Previous Page
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => {
|
|
||||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
|
||||||
}}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next Page
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
{variant !== "mc" && (
|
{variant !== "mc" && (
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
@@ -224,25 +229,8 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps })}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
|
||||||
Previous Page
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
{!disableProgressButtons && progressButtons()}
|
||||||
color="purple"
|
|
||||||
onClick={() => {
|
|
||||||
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
|
||||||
}}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next Page
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import {MatchSentenceExerciseOption, MatchSentenceExerciseSentence, MatchSentencesExercise} from "@/interfaces/exam";
|
import { MatchSentenceExerciseOption, MatchSentenceExerciseSentence, MatchSentencesExercise } from "@/interfaces/exam";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import LineTo from "react-lineto";
|
import LineTo from "react-lineto";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import Xarrow from "react-xarrows";
|
import Xarrow from "react-xarrows";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {DndContext, DragEndEvent, useDraggable, useDroppable} from "@dnd-kit/core";
|
import { DndContext, DragEndEvent, useDraggable, useDroppable } from "@dnd-kit/core";
|
||||||
|
|
||||||
function DroppableQuestionArea({question, answer}: {question: MatchSentenceExerciseSentence; answer?: string}) {
|
function DroppableQuestionArea({ question, answer }: { question: MatchSentenceExerciseSentence; answer?: string }) {
|
||||||
const {isOver, setNodeRef} = useDroppable({id: `droppable_sentence_${question.id}`});
|
const { isOver, setNodeRef } = useDroppable({ id: `droppable_sentence_${question.id}` });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-4" ref={setNodeRef}>
|
<div className="grid grid-cols-3 gap-4" ref={setNodeRef}>
|
||||||
@@ -35,16 +35,16 @@ function DroppableQuestionArea({question, answer}: {question: MatchSentenceExerc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) {
|
function DraggableOptionArea({ option }: { option: MatchSentenceExerciseOption }) {
|
||||||
const {attributes, listeners, setNodeRef, transform} = useDraggable({
|
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||||
id: `draggable_option_${option.id}`,
|
id: `draggable_option_${option.id}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = transform
|
const style = transform
|
||||||
? {
|
? {
|
||||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
zIndex: 99,
|
zIndex: 99,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,15 +63,25 @@ function DraggableOptionArea({option}: {option: MatchSentenceExerciseOption}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MatchSentences({id, options, type, prompt, sentences, userSolutions, onNext, onBack}: MatchSentencesExercise & CommonProps) {
|
export default function MatchSentences({
|
||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
id,
|
||||||
|
options,
|
||||||
|
type,
|
||||||
|
prompt,
|
||||||
|
sentences,
|
||||||
|
userSolutions,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
|
}: MatchSentencesExercise & CommonProps) {
|
||||||
|
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -80,7 +90,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
const optionID = event.active.id.toString().replace("draggable_option_", "");
|
const optionID = event.active.id.toString().replace("draggable_option_", "");
|
||||||
const sentenceID = event.over.id.toString().replace("droppable_sentence_", "");
|
const sentenceID = event.over.id.toString().replace("droppable_sentence_", "");
|
||||||
|
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), {question: sentenceID, option: optionID}]);
|
setAnswers((prev) => [...prev.filter((x) => x.question.toString() !== sentenceID), { question: sentenceID, option: optionID }]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,34 +101,43 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - answers.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length;
|
const missing = total - answers.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@@ -151,22 +170,7 @@ export default function MatchSentences({id, options, type, prompt, sentences, us
|
|||||||
</DndContext>
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap} from "@/interfaces/exam";
|
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
id,
|
id,
|
||||||
@@ -72,10 +72,19 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, prompt, type, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({
|
||||||
const [answers, setAnswers] = useState<{question: string; option: string}[]>(userSolutions);
|
id,
|
||||||
|
prompt,
|
||||||
|
type,
|
||||||
|
questions,
|
||||||
|
userSolutions,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
|
}: MultipleChoiceExercise & CommonProps) {
|
||||||
|
const [answers, setAnswers] = useState<{ question: string; option: string }[]>(userSolutions || []);
|
||||||
|
|
||||||
const {questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution} = useExamStore(
|
const { questionIndex, exerciseIndex, exam, shuffles, hasExamEnded, partIndex, setQuestionIndex, setCurrentSolution } = useExamStore(
|
||||||
(state) => state,
|
(state) => state,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,16 +93,16 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
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));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
const onSelectOption = (option: string, question: MultipleChoiceQuestion) => {
|
const onSelectOption = (option: string, question: MultipleChoiceQuestion) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), {option, question: question.id}]);
|
setAnswers((prev) => [...prev.filter((x) => x.question !== question.id), { option, question: question.id }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -127,12 +136,17 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
return isSolutionCorrect || false;
|
return isSolutionCorrect || false;
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length;
|
const missing = total - answers!.filter((x) => questions.find((y) => x.question.toString() === y.id.toString())).length;
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (questionIndex + 1 >= questions.length - 1) {
|
if (questionIndex + 1 >= questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
onNext({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 2);
|
setQuestionIndex(questionIndex + 2);
|
||||||
}
|
}
|
||||||
@@ -141,7 +155,7 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps});
|
onBack({ exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps });
|
||||||
} else {
|
} else {
|
||||||
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
|
if (exam?.module === "level" && typeof exam.parts[0].intro !== "undefined" && questionIndex === 0) return;
|
||||||
setQuestionIndex(questionIndex - 2);
|
setQuestionIndex(questionIndex - 2);
|
||||||
@@ -150,72 +164,74 @@ export default function MultipleChoice({id, prompt, type, questions, userSolutio
|
|||||||
scrollToTop();
|
scrollToTop();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const progressButtons = () => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex justify-between w-full gap-8">
|
||||||
<div className="flex justify-between w-full gap-8">
|
<Button
|
||||||
<Button
|
color="purple"
|
||||||
color="purple"
|
variant="outline"
|
||||||
variant="outline"
|
onClick={back}
|
||||||
onClick={back}
|
className="max-w-[200px] w-full"
|
||||||
className="max-w-[200px] w-full"
|
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
Back
|
||||||
Back
|
</Button>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
{exam &&
|
{exam &&
|
||||||
exam.module === "level" &&
|
exam.module === "level" &&
|
||||||
partIndex === exam.parts.length - 1 &&
|
partIndex === exam.parts.length - 1 &&
|
||||||
exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
|
exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
|
||||||
questionIndex + 1 >= questions.length - 1
|
questionIndex + 1 >= questions.length - 1
|
||||||
? "Submit"
|
? "Submit"
|
||||||
: "Next"}
|
: "Next"}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderAllQuestions = () =>
|
||||||
|
questions.map(question => (
|
||||||
|
<div
|
||||||
|
key={question.id} className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
|
<Question
|
||||||
|
{...question}
|
||||||
|
userSolution={answers.find((x) => question.id === x.question)?.option}
|
||||||
|
onSelectOption={(option) => onSelectOption(option, question)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 mb-20">
|
const renderTwoQuestions = () => (
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
<>
|
||||||
{/*<span className="text-xl font-semibold mb-2">{"Select the appropriate option."}</span>*/}
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
{questionIndex < questions.length && (
|
{questionIndex < questions.length && (
|
||||||
<Question
|
<Question
|
||||||
{...questions[questionIndex]}
|
{...questions[questionIndex]}
|
||||||
userSolution={answers.find((x) => questions[questionIndex].id === x.question)?.option}
|
userSolution={answers.find((x) => questions[questionIndex].id === x.question)?.option}
|
||||||
onSelectOption={(option) => onSelectOption(option, questions[questionIndex])}
|
onSelectOption={(option) => onSelectOption(option, questions[questionIndex])}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{questionIndex + 1 < questions.length && (
|
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
|
||||||
<Question
|
|
||||||
{...questions[questionIndex + 1]}
|
|
||||||
userSolution={answers.find((x) => questions[questionIndex + 1].id === x.question)?.option}
|
|
||||||
onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{questionIndex + 1 < questions.length && (
|
||||||
<Button
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
color="purple"
|
<Question
|
||||||
variant="outline"
|
{...questions[questionIndex + 1]}
|
||||||
onClick={back}
|
userSolution={answers.find((x) => questions[questionIndex + 1].id === x.question)?.option}
|
||||||
className="max-w-[200px] w-full"
|
onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])}
|
||||||
disabled={exam && exam.module === "level" && partIndex === 0 && questionIndex === 0}>
|
/>
|
||||||
Back
|
</div>
|
||||||
</Button>
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
return (
|
||||||
{exam &&
|
<div className="flex flex-col gap-4">
|
||||||
exam.module === "level" &&
|
{!disableProgressButtons && progressButtons()}
|
||||||
partIndex === exam.parts.length - 1 &&
|
|
||||||
exerciseIndex === exam.parts[partIndex].exercises.length - 1 &&
|
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
|
||||||
questionIndex + 1 >= questions.length - 1
|
{disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
|
||||||
? "Submit"
|
|
||||||
: "Next"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!disableProgressButtons && progressButtons()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import {TrueFalseExercise} from "@/interfaces/exam";
|
import { TrueFalseExercise } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import clsx from "clsx";
|
||||||
import {CommonProps} from ".";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
|
|
||||||
export default function TrueFalse({id, type, prompt, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
export default function TrueFalse({
|
||||||
const [answers, setAnswers] = useState<{id: string; solution: "true" | "false" | "not_given"}[]>(userSolutions);
|
id,
|
||||||
|
type,
|
||||||
|
prompt,
|
||||||
|
questions,
|
||||||
|
userSolutions,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
|
}: TrueFalseExercise & CommonProps) {
|
||||||
|
const [answers, setAnswers] = useState<{ id: string; solution: "true" | "false" | "not_given" }[]>(userSolutions);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
const setCurrentSolution = useExamStore((state) => state.setCurrentSolution);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -26,11 +36,11 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - answers.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
@@ -41,29 +51,38 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), {id: questionId, solution}]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== questionId), { id: questionId, solution }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@@ -123,22 +142,7 @@ export default function TrueFalse({id, type, prompt, questions, userSolutions, o
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import {WriteBlanksExercise} from "@/interfaces/exam";
|
import { WriteBlanksExercise } from "@/interfaces/exam";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ function Blank({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const words = userInput.split(" ");
|
const words = userInput.split(" ");
|
||||||
if (words.length > maxWords) {
|
if (words.length > maxWords) {
|
||||||
toast.warning(`You have reached your word limit of ${maxWords} words!`, {toastId: "word-limit"});
|
toast.warning(`You have reached your word limit of ${maxWords} words!`, { toastId: "word-limit" });
|
||||||
setUserInput(words.join(" ").trim());
|
setUserInput(words.join(" ").trim());
|
||||||
}
|
}
|
||||||
}, [maxWords, userInput]);
|
}, [maxWords, userInput]);
|
||||||
@@ -46,13 +46,24 @@ function Blank({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WriteBlanks({id, prompt, type, maxWords, solutions, userSolutions, text, onNext, onBack}: WriteBlanksExercise & CommonProps) {
|
export default function WriteBlanks({
|
||||||
const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions);
|
id,
|
||||||
|
prompt,
|
||||||
|
type,
|
||||||
|
maxWords,
|
||||||
|
solutions,
|
||||||
|
userSolutions,
|
||||||
|
text,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
|
}: WriteBlanksExercise & CommonProps) {
|
||||||
|
const [answers, setAnswers] = useState<{ id: string; solution: string }[]>(userSolutions);
|
||||||
|
|
||||||
const {hasExamEnded, setCurrentSolution} = useExamStore((state) => state);
|
const { hasExamEnded, setCurrentSolution } = useExamStore((state) => state);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type});
|
if (hasExamEnded) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hasExamEnded]);
|
}, [hasExamEnded]);
|
||||||
|
|
||||||
@@ -67,14 +78,19 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - answers.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
const missing = total - answers.filter((x) => solutions.find((y) => x.id === y.id)).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type});
|
setCurrentSolution({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, setAnswers]);
|
}, [answers, setAnswers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableProgressButtons) onNext({ exercise: id, solutions: answers, score: calculateScore(), type });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [answers, disableProgressButtons])
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-base leading-5">
|
<span className="text-base leading-5">
|
||||||
@@ -82,7 +98,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
const id = match.replaceAll(/[\{\}]/g, "");
|
const id = match.replaceAll(/[\{\}]/g, "");
|
||||||
const userSolution = answers.find((x) => x.id === id);
|
const userSolution = answers.find((x) => x.id === id);
|
||||||
const setUserSolution = (solution: string) => {
|
const setUserSolution = (solution: string) => {
|
||||||
setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution}]);
|
setAnswers((prev) => [...prev.filter((x) => x.id !== id), { id, solution }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Blank userSolution={userSolution?.solution} maxWords={maxWords} id={id} setUserSolution={setUserSolution} />;
|
return <Blank userSolution={userSolution?.solution} maxWords={maxWords} id={id} setUserSolution={setUserSolution} />;
|
||||||
@@ -91,26 +107,30 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: answers, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<span key={index}>
|
<span key={index}>
|
||||||
@@ -129,22 +149,7 @@ export default function WriteBlanks({id, prompt, type, maxWords, solutions, user
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: answers, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ import Speaking from "./Speaking";
|
|||||||
import TrueFalse from "./TrueFalse";
|
import TrueFalse from "./TrueFalse";
|
||||||
import InteractiveSpeaking from "./InteractiveSpeaking";
|
import InteractiveSpeaking from "./InteractiveSpeaking";
|
||||||
|
|
||||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), { ssr: false });
|
||||||
|
|
||||||
export interface CommonProps {
|
export interface CommonProps {
|
||||||
examID?: string;
|
examID?: string;
|
||||||
onNext: (userSolutions: UserSolution) => void;
|
onNext: (userSolutions: UserSolution) => void;
|
||||||
onBack: (userSolutions: UserSolution) => void;
|
onBack: (userSolutions: UserSolution) => void;
|
||||||
enableNavigation?: boolean;
|
enableNavigation?: boolean;
|
||||||
|
disableProgressButtons?: boolean
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,21 +36,22 @@ export const renderExercise = (
|
|||||||
onNext: (userSolutions: UserSolution) => void,
|
onNext: (userSolutions: UserSolution) => void,
|
||||||
onBack: (userSolutions: UserSolution) => void,
|
onBack: (userSolutions: UserSolution) => void,
|
||||||
enableNavigation?: boolean,
|
enableNavigation?: boolean,
|
||||||
|
disableProgressButtons?: boolean,
|
||||||
preview?: boolean,
|
preview?: boolean,
|
||||||
) => {
|
) => {
|
||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case "fillBlanks":
|
case "fillBlanks":
|
||||||
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
return <FillBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "trueFalse":
|
case "trueFalse":
|
||||||
return <TrueFalse key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
return <TrueFalse disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "matchSentences":
|
case "matchSentences":
|
||||||
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview}/>;
|
return <MatchSentences disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "multipleChoice":
|
case "multipleChoice":
|
||||||
return <MultipleChoice key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
return <MultipleChoice disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "writeBlanks":
|
case "writeBlanks":
|
||||||
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
return <WriteBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "writing":
|
case "writing":
|
||||||
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} examID={examID} enableNavigation={enableNavigation} preview={preview}/>;
|
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} examID={examID} enableNavigation={enableNavigation} preview={preview} />;
|
||||||
case "speaking":
|
case "speaking":
|
||||||
return <Speaking key={exercise.id} {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
return <Speaking key={exercise.id} {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} examID={examID} preview={preview} />;
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { sortByModuleName } from "@/utils/moduleUtils";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useMemo } from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import ModuleBadge from "../ModuleBadge";
|
import ModuleBadge from "../ModuleBadge";
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ interface Props {
|
|||||||
export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) {
|
export default function AssignmentCard({ user, assignment, session, startAssignment, resumeAssignment }: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const hasBeenSubmitted = useMemo(() => assignment.results.map((r) => r.user).includes(user.id), [assignment.results, user.id])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -45,7 +48,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
<ModuleBadge className="scale-110 w-full" key={module} module={module} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{futureAssignmentFilter(assignment) && (
|
{futureAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
||||||
<Button
|
<Button
|
||||||
color="rose"
|
color="rose"
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
@@ -54,7 +57,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
Not yet started
|
Not yet started
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{activeAssignmentFilter(assignment) && !assignment.results.map((r) => r.user).includes(user.id) && (
|
{activeAssignmentFilter(assignment) && !hasBeenSubmitted && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
className="tooltip flex h-full w-full items-center justify-end pl-8 md:hidden"
|
||||||
@@ -94,7 +97,7 @@ export default function AssignmentCard({ user, assignment, session, startAssignm
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{assignment.results.map((r) => r.user).includes(user.id) && (
|
{hasBeenSubmitted && (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
color="green"
|
||||||
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
className="-md:hidden h-full w-full max-w-[50%] !rounded-xl"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface Props {
|
|||||||
showSolutions?: boolean;
|
showSolutions?: boolean;
|
||||||
currentExercise?: Exercise;
|
currentExercise?: Exercise;
|
||||||
runOnClick?: ((questionIndex: number) => void) | undefined;
|
runOnClick?: ((questionIndex: number) => void) | undefined;
|
||||||
|
indexLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModuleTitle({
|
export default function ModuleTitle({
|
||||||
@@ -36,7 +37,8 @@ export default function ModuleTitle({
|
|||||||
partLabel,
|
partLabel,
|
||||||
showTimer = true,
|
showTimer = true,
|
||||||
showSolutions = false,
|
showSolutions = false,
|
||||||
runOnClick = undefined
|
runOnClick = undefined,
|
||||||
|
indexLabel = "Question"
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state);
|
const { exam, partIndex, exerciseIndex: examExerciseIndex, userSolutions } = useExamStore((state) => state);
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ export default function ModuleTitle({
|
|||||||
{examLabel ? examLabel : (module === "level" ? "Placement Test" : `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`)}
|
{examLabel ? examLabel : (module === "level" ? "Placement Test" : `${moduleLabels[module]} exam${label ? ` - ${label}` : ''}`)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-semibold self-end">
|
<span className="text-sm font-semibold self-end">
|
||||||
Question {exerciseIndex}/{totalExercises}
|
{indexLabel} {exerciseIndex}/{totalExercises}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
<ProgressBar color={module} label="" percentage={(exerciseIndex * 100) / totalExercises} className="h-2 w-full" />
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {FillBlanksExercise, FillBlanksMCOption, ShuffleMap} from "@/interfaces/exam";
|
import { FillBlanksExercise, FillBlanksMCOption, ShuffleMap } from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {Fragment} from "react";
|
import { Fragment } from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import { typeCheckWordsMC } from "@/utils/type.check";
|
import { typeCheckWordsMC } from "@/utils/type.check";
|
||||||
|
|
||||||
export default function FillBlanksSolutions({id, type, prompt, solutions, words, text, onNext, onBack}: FillBlanksExercise & CommonProps) {
|
export default function FillBlanksSolutions({ id, type, prompt, solutions, words, text, onNext, onBack, disableProgressButtons = false }: FillBlanksExercise & CommonProps) {
|
||||||
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
const storeUserSolutions = useExamStore((state) => state.userSolutions);
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
|
||||||
|
|
||||||
const correctUserSolutions = storeUserSolutions.find((solution) => solution.exercise === id)?.solutions;
|
const correctUserSolutions = storeUserSolutions.find((solution) => solution.exercise === id)?.solutions;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
|
|||||||
return false;
|
return false;
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - correctUserSolutions!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
@@ -81,20 +81,20 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
|
|||||||
typeof w === "string"
|
typeof w === "string"
|
||||||
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
? w.toLowerCase() === userSolution.solution.toLowerCase()
|
||||||
: "letter" in w
|
: "letter" in w
|
||||||
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
? w.letter.toLowerCase() === userSolution.solution.toLowerCase()
|
||||||
: "options" in w
|
: "options" in w
|
||||||
? w.id === userSolution.questionId
|
? w.id === userSolution.questionId
|
||||||
: false,
|
: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userSolutionText =
|
const userSolutionText =
|
||||||
typeof userSolutionWord === "string"
|
typeof userSolutionWord === "string"
|
||||||
? userSolutionWord
|
? userSolutionWord
|
||||||
: userSolutionWord && "letter" in userSolutionWord
|
: userSolutionWord && "letter" in userSolutionWord
|
||||||
? userSolutionWord.word
|
? userSolutionWord.word
|
||||||
: userSolutionWord && "options" in userSolutionWord
|
: userSolutionWord && "options" in userSolutionWord
|
||||||
? userSolution.solution
|
? userSolution.solution
|
||||||
: userSolution.solution;
|
: userSolution.solution;
|
||||||
|
|
||||||
let correct;
|
let correct;
|
||||||
let solutionText;
|
let solutionText;
|
||||||
@@ -149,27 +149,31 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||||
{correctUserSolutions &&
|
{correctUserSolutions &&
|
||||||
text.split("\\n").map((line, index) => (
|
text.split("\\n").map((line, index) => (
|
||||||
@@ -195,23 +199,7 @@ export default function FillBlanksSolutions({id, type, prompt, solutions, words,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: correctUserSolutions!, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {MatchSentenceExerciseSentence, MatchSentencesExercise} from "@/interfaces/exam";
|
import { MatchSentenceExerciseSentence, MatchSentencesExercise } from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import LineTo from "react-lineto";
|
import LineTo from "react-lineto";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import {Fragment} from "react";
|
import { Fragment } from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import Xarrow from "react-xarrows";
|
import Xarrow from "react-xarrows";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
@@ -15,7 +15,7 @@ function QuestionSolutionArea({
|
|||||||
userSolution,
|
userSolution,
|
||||||
}: {
|
}: {
|
||||||
question: MatchSentenceExerciseSentence;
|
question: MatchSentenceExerciseSentence;
|
||||||
userSolution?: {question: string; option: string};
|
userSolution?: { question: string; option: string };
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
@@ -26,8 +26,8 @@ function QuestionSolutionArea({
|
|||||||
!userSolution
|
!userSolution
|
||||||
? "bg-mti-gray-davy"
|
? "bg-mti-gray-davy"
|
||||||
: userSolution.option.toString() === question.solution.toString()
|
: userSolution.option.toString() === question.solution.toString()
|
||||||
? "bg-mti-purple"
|
? "bg-mti-purple"
|
||||||
: "bg-mti-rose",
|
: "bg-mti-rose",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
)}>
|
)}>
|
||||||
{question.id}
|
{question.id}
|
||||||
@@ -40,8 +40,8 @@ function QuestionSolutionArea({
|
|||||||
!userSolution
|
!userSolution
|
||||||
? "border-mti-gray-davy"
|
? "border-mti-gray-davy"
|
||||||
: userSolution.option.toString() === question.solution.toString()
|
: userSolution.option.toString() === question.solution.toString()
|
||||||
? "border-mti-purple"
|
? "border-mti-purple"
|
||||||
: "border-mti-rose",
|
: "border-mti-rose",
|
||||||
)}>
|
)}>
|
||||||
<span className="line-through">
|
<span className="line-through">
|
||||||
{userSolution && userSolution?.option.toString() !== question.solution.toString() && `Paragraph ${userSolution.option}`}
|
{userSolution && userSolution?.option.toString() !== question.solution.toString() && `Paragraph ${userSolution.option}`}
|
||||||
@@ -61,8 +61,9 @@ export default function MatchSentencesSolutions({
|
|||||||
userSolutions,
|
userSolutions,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
}: MatchSentencesExercise & CommonProps) {
|
}: MatchSentencesExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = sentences.length;
|
const total = sentences.length;
|
||||||
@@ -71,30 +72,34 @@ export default function MatchSentencesSolutions({
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - userSolutions.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length;
|
const missing = total - userSolutions.filter((x) => sentences.find((y) => y.id.toString() === x.question.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@@ -128,23 +133,7 @@ export default function MatchSentencesSolutions({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
import {MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap} from "@/interfaces/exam";
|
import { MultipleChoiceExercise, MultipleChoiceQuestion, ShuffleMap } from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import {v4} from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
function Question({
|
function Question({
|
||||||
id,
|
id,
|
||||||
@@ -14,8 +14,8 @@ function Question({
|
|||||||
solution,
|
solution,
|
||||||
options,
|
options,
|
||||||
userSolution,
|
userSolution,
|
||||||
}: MultipleChoiceQuestion & {userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean}) {
|
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) {
|
||||||
const {userSolutions} = useExamStore((state) => state);
|
const { userSolutions } = useExamStore((state) => state);
|
||||||
|
|
||||||
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
|
||||||
if (foundMap) return foundMap;
|
if (foundMap) return foundMap;
|
||||||
@@ -89,8 +89,8 @@ function Question({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MultipleChoice({id, type, prompt, questions, userSolutions, onNext, onBack}: MultipleChoiceExercise & CommonProps) {
|
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: MultipleChoiceExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
|
||||||
|
|
||||||
const stats = useExamStore((state) => state.userSolutions);
|
const stats = useExamStore((state) => state.userSolutions);
|
||||||
|
|
||||||
@@ -107,12 +107,12 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
}
|
}
|
||||||
}).length;
|
}).length;
|
||||||
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
const missing = total - userSolutions.filter((x) => questions.find((y) => y.id.toString() === x.question.toString())).length;
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
if (questionIndex + 1 >= questions.length - 1) {
|
if (questionIndex + 1 >= questions.length - 1) {
|
||||||
onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex + 2);
|
setQuestionIndex(questionIndex + 2);
|
||||||
}
|
}
|
||||||
@@ -120,50 +120,68 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
if (questionIndex === 0) {
|
if (questionIndex === 0) {
|
||||||
onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type});
|
onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type });
|
||||||
} else {
|
} else {
|
||||||
setQuestionIndex(questionIndex - 2);
|
setQuestionIndex(questionIndex - 2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const progressButtons = () => (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex justify-between w-full gap-8">
|
||||||
<div className="flex justify-between w-full gap-8">
|
<Button
|
||||||
<Button
|
color="purple"
|
||||||
color="purple"
|
variant="outline"
|
||||||
variant="outline"
|
onClick={back}
|
||||||
onClick={back}
|
className="max-w-[200px] w-full"
|
||||||
className="max-w-[200px] w-full"
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
Back
|
||||||
Back
|
</Button>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderAllQuestions = () =>
|
||||||
|
questions.map(question => (
|
||||||
|
<div
|
||||||
|
key={question.id} className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
|
<Question
|
||||||
|
{...question}
|
||||||
|
userSolution={userSolutions.find((x) => question.id === x.question)?.option}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
const renderTwoQuestions = () => (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
|
{questionIndex < questions.length && (
|
||||||
|
<Question
|
||||||
|
{...questions[questionIndex]}
|
||||||
|
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 w-full h-full mb-20 mt-4">
|
{questionIndex + 1 < questions.length && (
|
||||||
<div className="flex flex-col gap-4 mt-2">
|
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
<Question
|
||||||
{/*<span className="text-xl font-semibold">{prompt}</span>*/}
|
{...questions[questionIndex + 1]}
|
||||||
{userSolutions && questionIndex < questions.length && (
|
userSolution={userSolutions.find((x) => questions[questionIndex + 1].id === x.question)?.option}
|
||||||
<Question
|
/>
|
||||||
{...questions[questionIndex]}
|
|
||||||
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{userSolutions && questionIndex + 1 < questions.length && (
|
|
||||||
<div className="flex flex-col gap-8 h-fit w-full bg-mti-gray-smoke rounded-xl px-16 py-8">
|
|
||||||
<Question
|
|
||||||
{...questions[questionIndex + 1]}
|
|
||||||
userSolution={userSolutions.find((x) => questions[questionIndex + 1].id === x.question)?.option}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{!disableProgressButtons && progressButtons()}
|
||||||
|
|
||||||
|
<div className={clsx("flex flex-col gap-4 mt-4", !disableProgressButtons && "mb-20")}>
|
||||||
|
{disableProgressButtons ? renderAllQuestions() : renderTwoQuestions()}
|
||||||
|
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
@@ -181,20 +199,7 @@ export default function MultipleChoice({id, type, prompt, questions, userSolutio
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={back}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={next} className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {FillBlanksExercise, TrueFalseExercise} from "@/interfaces/exam";
|
import { FillBlanksExercise, TrueFalseExercise } from "@/interfaces/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {Fragment} from "react";
|
import { Fragment } from "react";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
type Solution = "true" | "false" | "not_given";
|
type Solution = "true" | "false" | "not_given";
|
||||||
|
|
||||||
export default function TrueFalseSolution({prompt, type, id, questions, userSolutions, onNext, onBack}: TrueFalseExercise & CommonProps) {
|
export default function TrueFalseSolution({ prompt, type, id, questions, userSolutions, onNext, onBack, disableProgressButtons = false }: TrueFalseExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = questions.length || 0;
|
const total = questions.length || 0;
|
||||||
@@ -18,7 +18,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - userSolutions.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - userSolutions.filter((x) => questions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getButtonColor = (buttonSolution: Solution, solution: Solution, userSolution: Solution | undefined) => {
|
const getButtonColor = (buttonSolution: Solution, solution: Solution, userSolution: Solution | undefined) => {
|
||||||
@@ -39,27 +39,31 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
return "gray";
|
return "gray";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@@ -137,23 +141,7 @@ export default function TrueFalseSolution({prompt, type, id, questions, userSolu
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {errorButtonStyle, infoButtonStyle} from "@/constants/buttonStyles";
|
import { errorButtonStyle, infoButtonStyle } from "@/constants/buttonStyles";
|
||||||
import {WriteBlanksExercise} from "@/interfaces/exam";
|
import { WriteBlanksExercise } from "@/interfaces/exam";
|
||||||
import {mdiArrowLeft, mdiArrowRight} from "@mdi/js";
|
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
|
||||||
import Icon from "@mdi/react";
|
import Icon from "@mdi/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
import reactStringReplace from "react-string-replace";
|
||||||
import {CommonProps} from ".";
|
import { CommonProps } from ".";
|
||||||
import {toast} from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
|
||||||
@@ -71,8 +71,9 @@ export default function WriteBlanksSolutions({
|
|||||||
text,
|
text,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
|
disableProgressButtons = false
|
||||||
}: WriteBlanksExercise & CommonProps) {
|
}: WriteBlanksExercise & CommonProps) {
|
||||||
const {questionIndex, setQuestionIndex, partIndex, exam} = useExamStore((state) => state);
|
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
|
||||||
|
|
||||||
const calculateScore = () => {
|
const calculateScore = () => {
|
||||||
const total = text.match(/({{\d+}})/g)?.length || 0;
|
const total = text.match(/({{\d+}})/g)?.length || 0;
|
||||||
@@ -85,7 +86,7 @@ export default function WriteBlanksSolutions({
|
|||||||
).length;
|
).length;
|
||||||
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
const missing = total - userSolutions.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length;
|
||||||
|
|
||||||
return {total, correct, missing};
|
return { total, correct, missing };
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLines = (line: string) => {
|
const renderLines = (line: string) => {
|
||||||
@@ -104,27 +105,31 @@ export default function WriteBlanksSolutions({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onBack({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] w-full"
|
||||||
|
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={() => onNext({ exercise: id, solutions: userSolutions, score: calculateScore(), type })}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex justify-between w-full gap-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", !disableProgressButtons && "mb-20")}>
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 mt-4 h-full w-full mb-20">
|
|
||||||
<span className="text-sm w-full leading-6">
|
<span className="text-sm w-full leading-6">
|
||||||
{prompt.split("\\n").map((line, index) => (
|
{prompt.split("\\n").map((line, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@@ -158,23 +163,7 @@ export default function WriteBlanksSolutions({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
{!disableProgressButtons && progressButtons()}
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onBack({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] w-full"
|
|
||||||
disabled={exam && typeof partIndex !== "undefined" && exam.module === "level" && questionIndex === 0 && partIndex === 0}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
onClick={() => onNext({exercise: id, solutions: userSolutions, score: calculateScore(), type})}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,25 +19,27 @@ import TrueFalseSolution from "./TrueFalse";
|
|||||||
import WriteBlanks from "./WriteBlanks";
|
import WriteBlanks from "./WriteBlanks";
|
||||||
import Writing from "./Writing";
|
import Writing from "./Writing";
|
||||||
|
|
||||||
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), {ssr: false});
|
const MatchSentences = dynamic(() => import("@/components/Solutions/MatchSentences"), { ssr: false });
|
||||||
|
|
||||||
export interface CommonProps {
|
export interface CommonProps {
|
||||||
onNext: (userSolutions: UserSolution) => void;
|
onNext: (userSolutions: UserSolution) => void;
|
||||||
onBack: (userSolutions: UserSolution) => void;
|
onBack: (userSolutions: UserSolution) => void;
|
||||||
|
disableProgressButtons?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void) => {
|
export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: () => void, updateIndex?: (internalIndex: number) => void,
|
||||||
|
disableProgressButtons?: boolean) => {
|
||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case "fillBlanks":
|
case "fillBlanks":
|
||||||
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <FillBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "trueFalse":
|
case "trueFalse":
|
||||||
return <TrueFalseSolution key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
|
return <TrueFalseSolution disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "matchSentences":
|
case "matchSentences":
|
||||||
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
return <MatchSentences disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "multipleChoice":
|
case "multipleChoice":
|
||||||
return <MultipleChoice key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} />;
|
return <MultipleChoice disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as MultipleChoiceExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "writeBlanks":
|
case "writeBlanks":
|
||||||
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <WriteBlanks disableProgressButtons={disableProgressButtons} key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "writing":
|
case "writing":
|
||||||
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
||||||
case "speaking":
|
case "speaking":
|
||||||
|
|||||||
@@ -326,11 +326,9 @@ export default function Finish({ user, scores, modules, information, solutions,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={destination || "/"} className="w-full max-w-[200px] self-end">
|
<Button onClick={() => destination === "/exam" ? router.reload() : router.push(destination || "/")} color="purple" className="w-full max-w-[200px] self-end">
|
||||||
<Button color="purple" className="w-full max-w-[200px] self-end">
|
Dashboard
|
||||||
Dashboard
|
</Button>
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ListeningExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam";
|
import { ListeningExam, MultipleChoiceExercise, Script, UserSolution } from "@/interfaces/exam";
|
||||||
import { useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { renderExercise } from "@/components/Exercises";
|
import { renderExercise } from "@/components/Exercises";
|
||||||
import { renderSolution } from "@/components/Solutions";
|
import { renderSolution } from "@/components/Solutions";
|
||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
@@ -9,6 +9,9 @@ import BlankQuestionsModal from "@/components/QuestionsModal";
|
|||||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
import { countExercises } from "@/utils/moduleUtils";
|
||||||
import PartDivider from "./Navigation/SectionDivider";
|
import PartDivider from "./Navigation/SectionDivider";
|
||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { capitalize } from "lodash";
|
||||||
|
import { mapBy } from "@/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ListeningExam;
|
exam: ListeningExam;
|
||||||
@@ -17,17 +20,76 @@ interface Props {
|
|||||||
onFinish: (userSolutions: UserSolution[]) => void;
|
onFinish: (userSolutions: UserSolution[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ScriptModal({ isOpen, script, onClose }: { isOpen: boolean; script: Script; onClose: () => void }) {
|
||||||
|
return (
|
||||||
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0">
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95">
|
||||||
|
<Dialog.Panel className="w-full relative max-w-4xl transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||||
|
<div className="mt-2 overflow-auto mb-28">
|
||||||
|
<p className="text-sm">
|
||||||
|
{typeof script === "string" && script.split("\\n").map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{typeof script === "object" && script.map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
<b>{line.name} ({capitalize(line.gender)})</b>: {line.text}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-8 right-8 max-w-[200px] self-end w-full">
|
||||||
|
<Button color="purple" variant="outline" className="max-w-[200px] self-end w-full" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const INSTRUCTIONS_AUDIO_SRC =
|
const INSTRUCTIONS_AUDIO_SRC =
|
||||||
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82";
|
"https://firebasestorage.googleapis.com/v0/b/storied-phalanx-349916.appspot.com/o/generic_listening_intro_v2.mp3?alt=media&token=16769f5f-1e9b-4a72-86a9-45a6f0fa9f82";
|
||||||
|
|
||||||
export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) {
|
export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) {
|
||||||
const listeningBgColor = "bg-ielts-listening-light";
|
const listeningBgColor = "bg-ielts-listening-light";
|
||||||
|
|
||||||
|
const [showTextModal, setShowTextModal] = useState(false);
|
||||||
const [timesListened, setTimesListened] = useState(0);
|
const [timesListened, setTimesListened] = useState(0);
|
||||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||||
|
|
||||||
|
|
||||||
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : []));
|
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : []));
|
||||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== "");
|
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== "");
|
||||||
|
|
||||||
@@ -99,74 +161,35 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
};
|
};
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
if (solution)
|
||||||
if (solution) {
|
setUserSolutions([
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
|
...userSolutions.filter((x) => x.exercise !== solution.exercise),
|
||||||
}
|
{ ...solution, module: "listening", exam: exam.id }
|
||||||
if (storeQuestionIndex > 0) {
|
]);
|
||||||
const exercise = getExercise();
|
};
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), { id: exercise.id, amount: storeQuestionIndex }]);
|
|
||||||
}
|
|
||||||
setStoreQuestionIndex(0);
|
|
||||||
|
|
||||||
if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) {
|
const previousExercise = (solution?: UserSolution) => { };
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const nextPart = () => {
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||||
setPartIndex(partIndex + 1);
|
setPartIndex(partIndex + 1);
|
||||||
setTimesListened(0);
|
setExerciseIndex(0);
|
||||||
setExerciseIndex(showSolutions ? 0 : -1);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!showSolutions && !hasExamEnded) {
|
||||||
solution &&
|
const exercises = partIndex < exam.parts.length ? exam.parts[partIndex].exercises : []
|
||||||
![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every(
|
const exerciseIDs = mapBy(exercises, 'id')
|
||||||
(x) => x === 0,
|
|
||||||
) &&
|
const hasMissing = userSolutions.filter(x => exerciseIDs.includes(x.exercise)).map(x => x.score.missing).some(x => x > 0)
|
||||||
!showSolutions &&
|
|
||||||
!hasExamEnded
|
if (hasMissing) return setShowBlankModal(true);
|
||||||
) {
|
|
||||||
setShowBlankModal(true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
onFinish(userSolutions);
|
||||||
if (solution) {
|
}
|
||||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
|
|
||||||
} else {
|
|
||||||
onFinish(userSolutions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const previousExercise = (solution?: UserSolution) => {
|
|
||||||
scrollToTop();
|
|
||||||
if (solution) {
|
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
|
|
||||||
}
|
|
||||||
setStoreQuestionIndex(0);
|
|
||||||
|
|
||||||
setExerciseIndex(exerciseIndex - 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExercise = () => {
|
|
||||||
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
|
|
||||||
return {
|
|
||||||
...exercise,
|
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (partIndex > -1 && exerciseIndex > -1) {
|
|
||||||
const exercise = getExercise();
|
|
||||||
setMultipleChoicesDone((prev) => prev.filter((x) => x.id !== exercise.id));
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [exerciseIndex, partIndex]);
|
|
||||||
|
|
||||||
const calculateExerciseIndex = () => {
|
const calculateExerciseIndex = () => {
|
||||||
if (partIndex === -1) return 0;
|
if (partIndex === -1) return 0;
|
||||||
@@ -185,6 +208,22 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderPartExercises = () => {
|
||||||
|
const exercises = partIndex > -1 ? exam.parts[partIndex].exercises : []
|
||||||
|
const formattedExercises = exercises.map(exercise => ({
|
||||||
|
...exercise,
|
||||||
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{formattedExercises.map(e => showSolutions
|
||||||
|
? renderSolution(e, nextExercise, previousExercise, undefined, true)
|
||||||
|
: renderExercise(e, exam.id, nextExercise, previousExercise, undefined, true))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const renderAudioInstructionsPlayer = () => (
|
const renderAudioInstructionsPlayer = () => (
|
||||||
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
||||||
<div className="flex flex-col w-full gap-2">
|
<div className="flex flex-col w-full gap-2">
|
||||||
@@ -200,16 +239,28 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
||||||
{exam?.parts[partIndex]?.audio?.source ? (
|
{exam?.parts[partIndex]?.audio?.source ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col w-full gap-2">
|
<div className="w-full items-start flex justify-between">
|
||||||
<h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4>
|
<div className="flex flex-col w-full gap-2">
|
||||||
<span className="text-base">
|
<h4 className="text-xl font-semibold">Please listen to the following audio attentively.</h4>
|
||||||
{(() => {
|
<span className="text-base">
|
||||||
const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes;
|
{(() => {
|
||||||
return audioRepeatTimes && audioRepeatTimes > 0
|
const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes;
|
||||||
? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).`
|
return audioRepeatTimes && audioRepeatTimes > 0
|
||||||
: "You may listen to the audio as many times as you would like.";
|
? `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.";
|
||||||
</span>
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{partIndex > -1 && !examState.assignment && !!exam.parts[partIndex].script && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowTextModal(true)}
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
className="w-full max-w-[200px]"
|
||||||
|
>
|
||||||
|
View Transcript
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
|
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
@@ -230,6 +281,25 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const progressButtons = () => (
|
||||||
|
<div className="flex justify-between w-full gap-8">
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
variant="outline"
|
||||||
|
onClick={previousExercise}
|
||||||
|
className="max-w-[200px] w-full">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color="purple"
|
||||||
|
onClick={nextPart}
|
||||||
|
className="max-w-[200px] self-end w-full">
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showPartDivider ?
|
{showPartDivider ?
|
||||||
@@ -243,14 +313,19 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
/> : (
|
/> : (
|
||||||
<>
|
<>
|
||||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||||
|
{partIndex > -1 && exam.parts[partIndex].script &&
|
||||||
|
<ScriptModal script={exam.parts[partIndex].script!} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />
|
||||||
|
}
|
||||||
<div className="flex flex-col h-full w-full gap-8 justify-between">
|
<div className="flex flex-col h-full w-full gap-8 justify-between">
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
exerciseIndex={calculateExerciseIndex()}
|
exerciseIndex={partIndex + 1}
|
||||||
minTimer={exam.minTimer}
|
minTimer={exam.minTimer}
|
||||||
module="listening"
|
module="listening"
|
||||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
totalExercises={exam.parts.length}
|
||||||
disableTimer={showSolutions}
|
disableTimer={showSolutions}
|
||||||
|
indexLabel="Part"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Audio Player for the Instructions */}
|
{/* Audio Player for the Instructions */}
|
||||||
{partIndex === -1 && renderAudioInstructionsPlayer()}
|
{partIndex === -1 && renderAudioInstructionsPlayer()}
|
||||||
|
|
||||||
@@ -258,18 +333,14 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
{partIndex > -1 && renderAudioPlayer()}
|
{partIndex > -1 && renderAudioPlayer()}
|
||||||
|
|
||||||
{/* Exercise renderer */}
|
{/* Exercise renderer */}
|
||||||
{exerciseIndex > -1 &&
|
|
||||||
partIndex > -1 &&
|
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
|
||||||
!showSolutions &&
|
|
||||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
|
||||||
|
|
||||||
{/* Solution renderer */}
|
{exerciseIndex > -1 && partIndex > -1 && (
|
||||||
{exerciseIndex > -1 &&
|
<>
|
||||||
partIndex > -1 &&
|
{progressButtons()}
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
{renderPartExercises()}
|
||||||
showSolutions &&
|
{progressButtons()}
|
||||||
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && (
|
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && (
|
||||||
@@ -294,12 +365,12 @@ export default function Listening({ exam, showSolutions = false, preview = false
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{partIndex === -1 && exam.variant !== "partial" && (
|
{partIndex === -1 && exam.variant !== "partial" && (
|
||||||
<Button color="purple" onClick={() => setPartIndex(0)} className="max-w-[200px] self-end w-full justify-self-end">
|
<Button color="purple" onClick={() => nextPart()} className="max-w-[200px] self-end w-full justify-self-end">
|
||||||
Start now
|
Start now
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && (
|
{exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && (
|
||||||
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end">
|
<Button color="purple" onClick={() => nextPart()} className="max-w-[200px] self-end w-full justify-self-end">
|
||||||
Start now
|
Start now
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user