Fixed some navigation issues and updated Listening
This commit is contained in:
@@ -167,7 +167,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full relative", (!headerButtons && !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full relative", (headerButtons && footerButtons) && "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) => (
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const MatchSentences: React.FC<MatchSentencesExercise & CommonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4", (!headerButtons && !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4", (headerButtons && footerButtons) && "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}>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const WriteBlanks: React.FC<WriteBlanksExercise & CommonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 relative">
|
<div className="flex flex-col gap-4 relative">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (!headerButtons && !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (headerButtons && footerButtons) && "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}>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ const FillBlanksSolutions: React.FC<FillBlanksExercise & CommonProps> = ({ id, s
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (!headerButtons || !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (headerButtons && footerButtons) && "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) => (
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function MatchSentencesSolutions({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (!headerButtons || !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (headerButtons && footerButtons) && "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}>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
|
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4", (!headerButtons || !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4", (headerButtons && footerButtons) && "mb-20")}>
|
||||||
{(!headerButtons || !footerButtons) ? renderAllQuestions() : renderTwoQuestions()}
|
{(!headerButtons || !footerButtons) ? renderAllQuestions() : renderTwoQuestions()}
|
||||||
|
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function TrueFalseSolution({ prompt, type, id, questions, userSol
|
|||||||
<div className="flex flex-col gap-4 mt-4">
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
|
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (!headerButtons || !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (headerButtons && footerButtons) && "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}>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function WriteBlanksSolutions({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{headerButtons}
|
{headerButtons}
|
||||||
|
|
||||||
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (!headerButtons || !footerButtons) && "mb-20")}>
|
<div className={clsx("flex flex-col gap-4 mt-4 h-full w-full", (headerButtons && footerButtons) && "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}>
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export default function Finish({ user, practiceScores, scores, modules, informat
|
|||||||
|
|
||||||
const aiUsage = Math.round(ai_usage(solutions) * 100);
|
const aiUsage = Math.round(ai_usage(solutions) * 100);
|
||||||
|
|
||||||
const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities])
|
//const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities])
|
||||||
const { gradingSystem } = useGradingSystem(entity);
|
//const { gradingSystem } = useGradingSystem(entity);
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export default function Finish({ user, practiceScores, scores, modules, informat
|
|||||||
|
|
||||||
const showLevel = (level: number) => {
|
const showLevel = (level: number) => {
|
||||||
if (selectedModule === "level") {
|
if (selectedModule === "level") {
|
||||||
const label = getGradingLabel(level, gradingSystem?.steps || []);
|
const label = getGradingLabel(level, []);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center gap-1">
|
<div className="flex flex-col items-center justify-center gap-1">
|
||||||
<span className="text-xl font-bold">{label}</span>
|
<span className="text-xl font-bold">{label}</span>
|
||||||
|
|||||||
@@ -3,14 +3,9 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea
|
|||||||
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";
|
||||||
import AudioPlayer from "@/components/Low/AudioPlayer";
|
|
||||||
import Button from "@/components/Low/Button";
|
|
||||||
import BlankQuestionsModal from "@/components/QuestionsModal";
|
import BlankQuestionsModal from "@/components/QuestionsModal";
|
||||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||||
import PartDivider from "./Navigation/SectionDivider";
|
import PartDivider from "./Navigation/SectionDivider";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { capitalize } from "lodash";
|
|
||||||
import { mapBy } from "@/utils";
|
|
||||||
import ScriptModal from "./components/ScriptModal";
|
import ScriptModal from "./components/ScriptModal";
|
||||||
import { ExamProps } from "./types";
|
import { ExamProps } from "./types";
|
||||||
import useExamTimer from "@/hooks/useExamTimer";
|
import useExamTimer from "@/hooks/useExamTimer";
|
||||||
@@ -19,8 +14,6 @@ import RenderAudioInstructionsPlayer from "./components/RenderAudioInstructionsP
|
|||||||
import RenderAudioPlayer from "./components/RenderAudioPlayer";
|
import RenderAudioPlayer from "./components/RenderAudioPlayer";
|
||||||
import SectionNavbar from "./Navigation/SectionNavbar";
|
import SectionNavbar from "./Navigation/SectionNavbar";
|
||||||
import ProgressButtons from "./components/ProgressButtons";
|
import ProgressButtons from "./components/ProgressButtons";
|
||||||
import { countExercises } from "@/utils/moduleUtils";
|
|
||||||
import { calculateExerciseIndex } from "./utils/calculateExerciseIndex";
|
|
||||||
|
|
||||||
|
|
||||||
const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
||||||
@@ -36,9 +29,8 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
const persistentExamState = usePersistentExamStore((state) => state);
|
const persistentExamState = usePersistentExamStore((state) => state);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
exerciseIndex, partIndex, assignment,
|
partIndex, assignment,
|
||||||
userSolutions, flags, timeSpentCurrentModule,
|
userSolutions, flags, timeSpentCurrentModule,
|
||||||
questionIndex,
|
|
||||||
setBgColor, setUserSolutions, setTimeIsUp,
|
setBgColor, setUserSolutions, setTimeIsUp,
|
||||||
dispatch
|
dispatch
|
||||||
} = !preview ? examState : persistentExamState;
|
} = !preview ? examState : persistentExamState;
|
||||||
@@ -47,13 +39,16 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
|
|
||||||
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
||||||
|
|
||||||
const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
nextExercise, previousExercise,
|
nextExercise, previousExercise,
|
||||||
showPartDivider, setShowPartDivider,
|
showPartDivider, setShowPartDivider,
|
||||||
seenParts, setSeenParts
|
seenParts, setSeenParts,
|
||||||
} = useExamNavigation({ exam, module: "listening", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: true });
|
isBetweenParts, startNow
|
||||||
|
} = useExamNavigation({
|
||||||
|
exam, module: "listening", showBlankModal,
|
||||||
|
setShowBlankModal, showSolutions, preview,
|
||||||
|
allPartExercisesRender: true, disableBetweenParts: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -80,14 +75,23 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [solutionWasUpdated])
|
}, [solutionWasUpdated])
|
||||||
|
|
||||||
const currentExercise = useMemo<Exercise>(() => {
|
|
||||||
const exercise = exam.parts[partIndex].exercises[exerciseIndex];
|
const renderPartExercises = useMemo(() => {
|
||||||
return {
|
const exercises = exam.parts[partIndex].exercises;
|
||||||
|
const formattedExercises = exercises.map(exercise => ({
|
||||||
...exercise,
|
...exercise,
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||||
};
|
}))
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [partIndex, exerciseIndex]);
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{formattedExercises.map(e => showSolutions
|
||||||
|
? renderSolution(e)
|
||||||
|
: (!startNow && !showPartDivider && !isBetweenParts && !showSolutions) && renderExercise(e, exam.id, registerSolution, preview))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [partIndex, startNow, showPartDivider, isBetweenParts, showSolutions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const confirmFinishModule = (keepGoing?: boolean) => {
|
const confirmFinishModule = (keepGoing?: boolean) => {
|
||||||
@@ -100,18 +104,10 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const memoizedExerciseIndex = useMemo(() =>
|
|
||||||
calculateExerciseIndex(exam, partIndex, exerciseIndex, questionIndex)
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
, [partIndex, exerciseIndex, questionIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePartDividerClick = () => {
|
const handlePartDividerClick = () => {
|
||||||
setShowPartDivider(false);
|
setShowPartDivider(false);
|
||||||
setBgColor("bg-white");
|
setBgColor("bg-white");
|
||||||
setSeenParts((prev) => new Set(prev).add(partIndex));
|
setSeenParts((prev) => new Set(prev).add(partIndex));
|
||||||
if (isFirstTimeRender) setIsFirstTimeRender(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -123,6 +119,21 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
<ProgressButtons handlePrevious={previousExercise} handleNext={() => nextExercise()} />
|
<ProgressButtons handlePrevious={previousExercise} handleNext={() => nextExercise()} />
|
||||||
, [nextExercise, previousExercise]);
|
, [nextExercise, previousExercise]);
|
||||||
|
|
||||||
|
const memoizedRenderAudioPlayer = useMemo(() =>
|
||||||
|
<RenderAudioPlayer
|
||||||
|
audioSource={exam?.parts[partIndex]?.audio?.source}
|
||||||
|
repeatableTimes={exam?.parts[partIndex]?.audio?.repeatableTimes}
|
||||||
|
script={exam?.parts[partIndex]?.script}
|
||||||
|
assignment={assignment}
|
||||||
|
timesListened={timesListened}
|
||||||
|
setShowTextModal={setShowTextModal}
|
||||||
|
setTimesListened={setTimesListened}
|
||||||
|
/>, [partIndex, assignment, timesListened, setShowTextModal, setTimesListened])
|
||||||
|
|
||||||
|
const memoizedInstructions = useMemo(()=>
|
||||||
|
<RenderAudioInstructionsPlayer />
|
||||||
|
, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showPartDivider ?
|
{showPartDivider ?
|
||||||
@@ -136,13 +147,13 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
/> : (
|
/> : (
|
||||||
<>
|
<>
|
||||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||||
{!isFirstTimeRender && exam.parts[partIndex].script &&
|
{!startNow && exam.parts[partIndex].script &&
|
||||||
<ScriptModal script={exam.parts[partIndex].script!} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />
|
<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">
|
||||||
{exam.parts.length > 1 && <SectionNavbar
|
{exam.parts.length > 1 && <SectionNavbar
|
||||||
module="listening"
|
module="listening"
|
||||||
sectionLabel="Section"
|
sectionLabel="Part"
|
||||||
seenParts={seenParts}
|
seenParts={seenParts}
|
||||||
setShowPartDivider={setShowPartDivider}
|
setShowPartDivider={setShowPartDivider}
|
||||||
setSeenParts={setSeenParts}
|
setSeenParts={setSeenParts}
|
||||||
@@ -150,41 +161,35 @@ const Listening: React.FC<ExamProps<ListeningExam>> = ({ exam, showSolutions = f
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<ModuleTitle
|
<ModuleTitle
|
||||||
|
exerciseIndex={partIndex + 1}
|
||||||
minTimer={timer.current}
|
minTimer={timer.current}
|
||||||
module="listening"
|
module="listening"
|
||||||
exerciseIndex={memoizedExerciseIndex}
|
totalExercises={exam.parts.length}
|
||||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
|
||||||
disableTimer={showSolutions || preview}
|
disableTimer={showSolutions || preview}
|
||||||
indexLabel="Exercise"
|
indexLabel="Part"
|
||||||
preview={preview}
|
preview={preview}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Audio Player for the Instructions */}
|
{/* Audio Player for the Instructions */}
|
||||||
{isFirstTimeRender && <RenderAudioInstructionsPlayer />}
|
{startNow && memoizedInstructions}
|
||||||
|
|
||||||
{/* Part's audio player */}
|
{/* Part's audio player */}
|
||||||
{!isFirstTimeRender &&
|
{(!startNow && isBetweenParts) && memoizedRenderAudioPlayer}
|
||||||
<RenderAudioPlayer
|
|
||||||
audioSource={exam?.parts[partIndex]?.audio?.source}
|
|
||||||
repeatableTimes={exam?.parts[partIndex]?.audio?.repeatableTimes}
|
|
||||||
script={exam?.parts[partIndex]?.script}
|
|
||||||
assignment={assignment}
|
|
||||||
timesListened={timesListened}
|
|
||||||
setShowTextModal={setShowTextModal}
|
|
||||||
setTimesListened={setTimesListened}
|
|
||||||
/>}
|
|
||||||
|
|
||||||
{/* Exercise renderer */}
|
{/* Exercise renderer */}
|
||||||
{!isFirstTimeRender && !showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
<>
|
||||||
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
{(!startNow && !showPartDivider) && progressButtons}
|
||||||
|
{renderPartExercises}
|
||||||
|
{(!startNow && !showPartDivider && !isBetweenParts) && progressButtons}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{((isFirstTimeRender) && !showPartDivider && !showSolutions) &&
|
{((startNow) && !showPartDivider && !showSolutions) &&
|
||||||
<ProgressButtons
|
<ProgressButtons
|
||||||
hidePrevious={partIndex == 0 && isFirstTimeRender}
|
hidePrevious={partIndex == 0 && startNow}
|
||||||
nextLabel={isFirstTimeRender ? "Start now" : "Next Page"}
|
nextLabel={startNow ? "Start now" : "Next Page"}
|
||||||
handlePrevious={previousExercise}
|
handlePrevious={previousExercise}
|
||||||
handleNext={() => isFirstTimeRender ? setIsFirstTimeRender(false) : nextExercise()} />
|
handleNext={() => nextExercise()} />
|
||||||
}
|
}
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
import { ListeningExam, MultipleChoiceExercise, Script, UserSolution } from "@/interfaces/exam";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
|
||||||
import { renderExercise } from "@/components/Exercises";
|
|
||||||
import { renderSolution } from "@/components/Solutions";
|
|
||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
|
||||||
import AudioPlayer from "@/components/Low/AudioPlayer";
|
|
||||||
import Button from "@/components/Low/Button";
|
|
||||||
import BlankQuestionsModal from "@/components/QuestionsModal";
|
|
||||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
|
||||||
import PartDivider from "./Navigation/SectionDivider";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { capitalize } from "lodash";
|
|
||||||
import { mapBy } from "@/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
exam: ListeningExam;
|
|
||||||
showSolutions?: boolean;
|
|
||||||
preview?: boolean;
|
|
||||||
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 =
|
|
||||||
"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) {
|
|
||||||
const listeningBgColor = "bg-ielts-listening-light";
|
|
||||||
|
|
||||||
const [showTextModal, setShowTextModal] = useState(false);
|
|
||||||
const [timesListened, setTimesListened] = useState(0);
|
|
||||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
|
||||||
|
|
||||||
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 examState = useExamStore((state) => state);
|
|
||||||
const persistentExamState = usePersistentExamStore((state) => state);
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasExamEnded,
|
|
||||||
userSolutions,
|
|
||||||
exerciseIndex,
|
|
||||||
partIndex,
|
|
||||||
questionIndex: storeQuestionIndex,
|
|
||||||
setBgColor,
|
|
||||||
setUserSolutions,
|
|
||||||
setHasExamEnded,
|
|
||||||
setExerciseIndex,
|
|
||||||
setPartIndex,
|
|
||||||
setQuestionIndex: setStoreQuestionIndex
|
|
||||||
} = !preview ? examState : persistentExamState;
|
|
||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(exerciseIndex)) {
|
|
||||||
setShowPartDivider(true);
|
|
||||||
setBgColor(listeningBgColor);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [partIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showSolutions) return setExerciseIndex(0);
|
|
||||||
}, [setExerciseIndex, showSolutions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (partIndex === -1 && exam.variant === "partial") {
|
|
||||||
setPartIndex(0);
|
|
||||||
}
|
|
||||||
}, [partIndex, exam, setPartIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const previousParts = exam.parts.filter((_, index) => index < partIndex);
|
|
||||||
let previousMultipleChoice = previousParts.flatMap((x) => x.exercises).filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[];
|
|
||||||
|
|
||||||
if (partIndex > -1 && exerciseIndex > -1) {
|
|
||||||
const previousPartExercises = exam.parts[partIndex].exercises.filter((_, index) => index < exerciseIndex);
|
|
||||||
const partMultipleChoice = previousPartExercises.filter((x) => x.type === "multipleChoice") as MultipleChoiceExercise[];
|
|
||||||
|
|
||||||
previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice];
|
|
||||||
}
|
|
||||||
|
|
||||||
setMultipleChoicesDone(previousMultipleChoice.map((x) => ({ id: x.id, amount: x.questions.length - 1 })));
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasExamEnded) onFinish(userSolutions)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [hasExamEnded]);
|
|
||||||
|
|
||||||
const confirmFinishModule = (keepGoing?: boolean) => {
|
|
||||||
if (!keepGoing) {
|
|
||||||
setShowBlankModal(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFinish(userSolutions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextExercise = (solution?: UserSolution) => {
|
|
||||||
if (solution)
|
|
||||||
setUserSolutions([
|
|
||||||
...userSolutions.filter((x) => x.exercise !== solution.exercise),
|
|
||||||
{ ...solution, module: "listening", exam: exam.id }
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const previousExercise = (solution?: UserSolution) => {
|
|
||||||
scrollToTop();
|
|
||||||
if (solution)
|
|
||||||
setUserSolutions([
|
|
||||||
...userSolutions.filter((x) => x.exercise !== solution.exercise),
|
|
||||||
{ ...solution, module: "listening", exam: exam.id }
|
|
||||||
]);
|
|
||||||
|
|
||||||
setPartIndex(partIndex - 1)
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextPart = () => {
|
|
||||||
scrollToTop()
|
|
||||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
|
||||||
setPartIndex(partIndex + 1);
|
|
||||||
setExerciseIndex(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showSolutions && !hasExamEnded) {
|
|
||||||
const exercises = partIndex < exam.parts.length ? exam.parts[partIndex].exercises : []
|
|
||||||
const exerciseIDs = mapBy(exercises, 'id')
|
|
||||||
|
|
||||||
const hasMissing = userSolutions.filter(x => exerciseIDs.includes(x.exercise)).map(x => x.score.missing).some(x => x > 0)
|
|
||||||
|
|
||||||
if (hasMissing) return setShowBlankModal(true);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasExamEnded(false);
|
|
||||||
onFinish(userSolutions);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = () => (
|
|
||||||
<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">
|
|
||||||
<h4 className="text-xl font-semibold">Please listen to the instructions audio attentively.</h4>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
|
|
||||||
<AudioPlayer key={partIndex} src={INSTRUCTIONS_AUDIO_SRC} color="listening" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderAudioPlayer = () => (
|
|
||||||
<div className="flex flex-col gap-8 w-full bg-mti-gray-seasalt rounded-xl py-8 px-16">
|
|
||||||
{exam?.parts[partIndex]?.audio?.source ? (
|
|
||||||
<>
|
|
||||||
<div className="w-full items-start flex justify-between">
|
|
||||||
<div className="flex flex-col w-full gap-2">
|
|
||||||
<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
|
|
||||||
? `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>
|
|
||||||
</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 className="rounded-xl flex flex-col gap-4 items-center w-full h-fit">
|
|
||||||
<AudioPlayer
|
|
||||||
key={partIndex}
|
|
||||||
src={exam?.parts[partIndex]?.audio?.source ?? ''}
|
|
||||||
color="listening"
|
|
||||||
onEnd={() => setTimesListened((prev) => prev + 1)}
|
|
||||||
disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null &&
|
|
||||||
timesListened === exam.parts[partIndex]?.audio?.repeatableTimes}
|
|
||||||
disablePause
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>This section will be displayed the audio once it has been generated.</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</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 (
|
|
||||||
<>
|
|
||||||
{showPartDivider ?
|
|
||||||
<PartDivider
|
|
||||||
module="listening"
|
|
||||||
sectionLabel="Section"
|
|
||||||
defaultTitle="Listening exam"
|
|
||||||
section={exam.parts[partIndex]}
|
|
||||||
sectionIndex={partIndex}
|
|
||||||
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
|
|
||||||
/> : (
|
|
||||||
<>
|
|
||||||
<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">
|
|
||||||
<ModuleTitle
|
|
||||||
exerciseIndex={partIndex + 1}
|
|
||||||
minTimer={exam.minTimer}
|
|
||||||
module="listening"
|
|
||||||
totalExercises={exam.parts.length}
|
|
||||||
disableTimer={showSolutions || preview}
|
|
||||||
indexLabel="Part"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Audio Player for the Instructions */}
|
|
||||||
{partIndex === -1 && renderAudioInstructionsPlayer()}
|
|
||||||
|
|
||||||
{/* Part's audio player */}
|
|
||||||
{partIndex > -1 && renderAudioPlayer()}
|
|
||||||
|
|
||||||
{/* Exercise renderer */}
|
|
||||||
|
|
||||||
{exerciseIndex > -1 && partIndex > -1 && (
|
|
||||||
<>
|
|
||||||
{progressButtons()}
|
|
||||||
{renderPartExercises()}
|
|
||||||
{progressButtons()}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{exerciseIndex === -1 && partIndex > -1 && exam.variant !== "partial" && (
|
|
||||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
|
||||||
<Button
|
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (partIndex === 0) return setPartIndex(-1);
|
|
||||||
|
|
||||||
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
|
|
||||||
setPartIndex(partIndex - 1);
|
|
||||||
}}
|
|
||||||
className="max-w-[200px] w-full">
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button color="purple" onClick={() => setExerciseIndex(0)} className="max-w-[200px] self-end w-full">
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{partIndex === -1 && exam.variant !== "partial" && (
|
|
||||||
<Button color="purple" onClick={() => setPartIndex(0)} className="max-w-[200px] self-end w-full justify-self-end">
|
|
||||||
Start now
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{exerciseIndex === -1 && partIndex === 0 && exam.variant === "partial" && (
|
|
||||||
<Button color="purple" onClick={() => setExerciseIndex(0)} className="max-w-[200px] self-end w-full justify-self-end">
|
|
||||||
Start now
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -17,10 +17,13 @@ type UseExamNavigation = (props: {
|
|||||||
showSolutions: boolean;
|
showSolutions: boolean;
|
||||||
preview: boolean;
|
preview: boolean;
|
||||||
disableBetweenParts?: boolean;
|
disableBetweenParts?: boolean;
|
||||||
|
allPartExercisesRender?: boolean;
|
||||||
|
modalBetweenParts?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
showPartDivider: boolean;
|
showPartDivider: boolean;
|
||||||
seenParts: Set<number>;
|
seenParts: Set<number>;
|
||||||
isBetweenParts: boolean;
|
isBetweenParts: boolean;
|
||||||
|
startNow: boolean;
|
||||||
nextExercise: (isBetweenParts?: boolean) => void;
|
nextExercise: (isBetweenParts?: boolean) => void;
|
||||||
previousExercise: (isBetweenParts?: boolean) => void;
|
previousExercise: (isBetweenParts?: boolean) => void;
|
||||||
setShowPartDivider: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowPartDivider: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
@@ -36,6 +39,8 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
showSolutions,
|
showSolutions,
|
||||||
preview,
|
preview,
|
||||||
disableBetweenParts = false,
|
disableBetweenParts = false,
|
||||||
|
allPartExercisesRender = false,
|
||||||
|
modalBetweenParts = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const examState = useExamStore((state) => state);
|
const examState = useExamStore((state) => state);
|
||||||
@@ -50,7 +55,10 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
dispatch,
|
dispatch,
|
||||||
} = !preview ? examState : persistentExamState;
|
} = !preview ? examState : persistentExamState;
|
||||||
|
|
||||||
const [isBetweenParts, setIsBetweenParts] = useState(partIndex !== 0 && exerciseIndex == 0 && !disableBetweenParts);
|
const [isBetweenParts, setIsBetweenParts] = useState(module === "reading"
|
||||||
|
? (partIndex !== 0 && exerciseIndex === 0 && !disableBetweenParts)
|
||||||
|
: (exerciseIndex === 0 && !disableBetweenParts)
|
||||||
|
);
|
||||||
const isPartExam = ["reading", "listening", "level"].includes(exam.module);
|
const isPartExam = ["reading", "listening", "level"].includes(exam.module);
|
||||||
|
|
||||||
const [seenParts, setSeenParts] = useState<Set<number>>(
|
const [seenParts, setSeenParts] = useState<Set<number>>(
|
||||||
@@ -63,6 +71,14 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [showPartDivider, setShowPartDivider] = useState<boolean>(hasDivider(exam, 0));
|
const [showPartDivider, setShowPartDivider] = useState<boolean>(hasDivider(exam, 0));
|
||||||
|
const [startNow, setStartNow] = useState(!showPartDivider && !showSolutions);
|
||||||
|
|
||||||
|
// when navbar is used
|
||||||
|
useEffect(()=> {
|
||||||
|
if(startNow && partIndex !== 0) {
|
||||||
|
setStartNow(false);
|
||||||
|
}
|
||||||
|
} , [partIndex, startNow])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSolutions && hasDivider(exam, isPartExam ? partIndex : exerciseIndex) && !seenParts.has(partIndex)) {
|
if (!showSolutions && hasDivider(exam, isPartExam ? partIndex : exerciseIndex) && !seenParts.has(partIndex)) {
|
||||||
@@ -95,14 +111,42 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
const nextPartExam = (keepGoing: boolean) => {
|
const nextPartExam = (keepGoing: boolean) => {
|
||||||
const partExam = (exam as PartExam);
|
const partExam = (exam as PartExam);
|
||||||
|
|
||||||
const reachedFinalExercise = exerciseIndex + 1 === partExam.parts[partIndex].exercises.length;
|
if (startNow) {
|
||||||
const currentExercise = partExam.parts[partIndex].exercises[exerciseIndex];
|
setStartNow(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isBetweenParts) {
|
if (isBetweenParts) {
|
||||||
setIsBetweenParts(false);
|
setIsBetweenParts(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allPartExercisesRender) {
|
||||||
|
if (partIndex < partExam.parts.length - 1) {
|
||||||
|
if (!disableBetweenParts) setIsBetweenParts(true);
|
||||||
|
setPartIndex(partIndex + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) {
|
||||||
|
if (modalKwargs) modalKwargs();
|
||||||
|
setShowBlankModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
setPartIndex(0);
|
||||||
|
} else if (!showSolutions) {
|
||||||
|
dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reachedFinalExercise = exerciseIndex + 1 === partExam.parts[partIndex].exercises.length;
|
||||||
|
const currentExercise = partExam.parts[partIndex].exercises[exerciseIndex];
|
||||||
|
|
||||||
if (currentExercise.type === "multipleChoice") {
|
if (currentExercise.type === "multipleChoice") {
|
||||||
const nextQuestionIndex = questionIndex + MC_PER_PAGE;
|
const nextQuestionIndex = questionIndex + MC_PER_PAGE;
|
||||||
if (nextQuestionIndex < currentExercise.questions!.length) {
|
if (nextQuestionIndex < currentExercise.questions!.length) {
|
||||||
@@ -110,12 +154,20 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reachedFinalExercise) {
|
if (!reachedFinalExercise) {
|
||||||
setExerciseIndex(exerciseIndex + 1);
|
setExerciseIndex(exerciseIndex + 1);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(modalBetweenParts);
|
||||||
|
if (modalBetweenParts && !answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) {
|
||||||
|
if (modalKwargs) modalKwargs();
|
||||||
|
setShowBlankModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (partIndex < partExam.parts.length - 1) {
|
if (partIndex < partExam.parts.length - 1) {
|
||||||
if (!disableBetweenParts) setIsBetweenParts(true);
|
if (!disableBetweenParts) setIsBetweenParts(true);
|
||||||
setPartIndex(partIndex + 1);
|
setPartIndex(partIndex + 1);
|
||||||
@@ -124,34 +176,55 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) {
|
|
||||||
if (modalKwargs) modalKwargs()
|
if (!modalBetweenParts && !answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) {
|
||||||
|
if (modalKwargs) modalKwargs();
|
||||||
setShowBlankModal(true);
|
setShowBlankModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (preview) {
|
if (preview) {
|
||||||
setPartIndex(0);
|
setPartIndex(0);
|
||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
setQuestionIndex(0);
|
setQuestionIndex(0);
|
||||||
}
|
} else if (!showSolutions) {
|
||||||
|
|
||||||
if (!showSolutions) {
|
|
||||||
dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } });
|
dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } });
|
||||||
} else {
|
} else {
|
||||||
dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" });
|
dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const previousPartExam = () => {
|
const previousPartExam = () => {
|
||||||
const currentExercise = (exam as PartExam).parts[partIndex].exercises[exerciseIndex];
|
|
||||||
if (currentExercise.type === "multipleChoice" && questionIndex > 0) {
|
if (partIndex === 0 && exerciseIndex === 0 && !startNow) {
|
||||||
setQuestionIndex(Math.max(0, questionIndex - MC_PER_PAGE));
|
setStartNow(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exerciseIndex === 0 && !disableBetweenParts) {
|
if (!disableBetweenParts && isBetweenParts) {
|
||||||
setIsBetweenParts(true);
|
setIsBetweenParts(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allPartExercisesRender) {
|
||||||
|
if (isBetweenParts && partIndex !== 0) {
|
||||||
|
setPartIndex(partIndex - 1);
|
||||||
|
setIsBetweenParts(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disableBetweenParts) {
|
||||||
|
if (partIndex !== 0) {
|
||||||
|
setPartIndex(partIndex - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsBetweenParts(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentExercise = (exam as PartExam).parts[partIndex].exercises[exerciseIndex];
|
||||||
|
if (currentExercise.type === "multipleChoice" && questionIndex > 0 && !allPartExercisesRender) {
|
||||||
|
setQuestionIndex(Math.max(0, questionIndex - MC_PER_PAGE));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +295,7 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
setSeenParts,
|
setSeenParts,
|
||||||
isBetweenParts,
|
isBetweenParts,
|
||||||
setIsBetweenParts,
|
setIsBetweenParts,
|
||||||
|
startNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false
|
|||||||
exerciseIndex, partIndex, questionIndex,
|
exerciseIndex, partIndex, questionIndex,
|
||||||
userSolutions, flags, timeSpentCurrentModule,
|
userSolutions, flags, timeSpentCurrentModule,
|
||||||
setBgColor, setUserSolutions, setTimeIsUp,
|
setBgColor, setUserSolutions, setTimeIsUp,
|
||||||
dispatch
|
dispatch,
|
||||||
} = !preview ? examState : persistentExamState;
|
} = !preview ? examState : persistentExamState;
|
||||||
|
|
||||||
|
|
||||||
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60);
|
||||||
|
|
||||||
const { finalizeModule, timeIsUp } = flags;
|
const { finalizeModule, timeIsUp } = flags;
|
||||||
const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
nextExercise, previousExercise,
|
nextExercise, previousExercise,
|
||||||
showPartDivider, setShowPartDivider,
|
showPartDivider, setShowPartDivider,
|
||||||
seenParts, setSeenParts,
|
seenParts, setSeenParts,
|
||||||
isBetweenParts, setIsBetweenParts
|
isBetweenParts, setIsBetweenParts,
|
||||||
|
startNow
|
||||||
} = useExamNavigation({ exam, module: "reading", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: showSolutions });
|
} = useExamNavigation({ exam, module: "reading", showBlankModal, setShowBlankModal, showSolutions, preview, disableBetweenParts: showSolutions });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -118,7 +118,6 @@ const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false
|
|||||||
setShowPartDivider(false);
|
setShowPartDivider(false);
|
||||||
setBgColor("bg-white");
|
setBgColor("bg-white");
|
||||||
setSeenParts((prev) => new Set(prev).add(partIndex));
|
setSeenParts((prev) => new Set(prev).add(partIndex));
|
||||||
if (isFirstTimeRender) setIsFirstTimeRender(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -171,7 +170,7 @@ const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"mb-20 w-full",
|
"mb-20 w-full",
|
||||||
((isFirstTimeRender || isBetweenParts) && !showSolutions) ? "flex flex-col gap-2" : "grid grid-cols-2 gap-4",
|
((startNow || isBetweenParts) && !showSolutions) ? "flex flex-col gap-2" : "grid grid-cols-2 gap-4",
|
||||||
)}>
|
)}>
|
||||||
<ReadingPassage
|
<ReadingPassage
|
||||||
exam={exam}
|
exam={exam}
|
||||||
@@ -180,7 +179,7 @@ const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false
|
|||||||
isTextMinimized={isTextMinimized}
|
isTextMinimized={isTextMinimized}
|
||||||
setIsTextMinimized={setIsTextMinimzed}
|
setIsTextMinimized={setIsTextMinimzed}
|
||||||
/>
|
/>
|
||||||
{!isFirstTimeRender && !showPartDivider && !showSolutions && !isBetweenParts && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
{!startNow && !showPartDivider && !showSolutions && !isBetweenParts && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
||||||
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
||||||
</div>
|
</div>
|
||||||
{/*exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (
|
{/*exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (
|
||||||
@@ -193,12 +192,12 @@ const Reading: React.FC<ExamProps<ReadingExam>> = ({ exam, showSolutions = false
|
|||||||
</Button>
|
</Button>
|
||||||
)*/}
|
)*/}
|
||||||
</div>
|
</div>
|
||||||
{((isFirstTimeRender || isBetweenParts) && !showPartDivider && !showSolutions) &&
|
{((startNow || isBetweenParts) && !showPartDivider && !showSolutions) &&
|
||||||
<ProgressButtons
|
<ProgressButtons
|
||||||
hidePrevious={partIndex == 0 && isBetweenParts || isFirstTimeRender}
|
hidePrevious={partIndex == 0 && isBetweenParts || startNow}
|
||||||
nextLabel={isFirstTimeRender ? "Start now" : "Next Page"}
|
nextLabel={startNow ? "Start now" : "Next Page"}
|
||||||
handlePrevious={previousExercise}
|
handlePrevious={previousExercise}
|
||||||
handleNext={() => isFirstTimeRender ? setIsFirstTimeRender(false) : nextExercise()} />
|
handleNext={() => nextExercise()} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -193,9 +193,9 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeExam && moduleIndex !== -1) {
|
if (flags.finalizeExam && moduleIndex !== -1) {
|
||||||
setModuleIndex(-1);
|
setModuleIndex(-1);
|
||||||
}
|
}
|
||||||
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
|
}, [flags, moduleIndex, setModuleIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) {
|
if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) {
|
||||||
@@ -215,11 +215,11 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
|
|||||||
await axios.get("/api/stats/update");
|
await axios.get("/api/stats/update");
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setFlags({ finalizeExam: false });
|
setFlags({ finalizeExam: false });
|
||||||
dispatch({type: "UPDATE_EXAMS"})
|
dispatch({ type: "UPDATE_EXAMS" })
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions]);
|
}, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions, flags]);
|
||||||
|
|
||||||
|
|
||||||
const aggregateScoresByModule = (isPractice?: boolean): {
|
const aggregateScoresByModule = (isPractice?: boolean): {
|
||||||
|
|||||||
@@ -111,7 +111,17 @@ export const rootReducer = (
|
|||||||
} else {
|
} else {
|
||||||
// then check whether there are more modules in the exam, if there are
|
// then check whether there are more modules in the exam, if there are
|
||||||
// setup the next module
|
// setup the next module
|
||||||
if (state.moduleIndex + 1 < state.selectedModules.length) {
|
if (state.moduleIndex === state.selectedModules.length - 1) {
|
||||||
|
return {
|
||||||
|
showSolutions: true,
|
||||||
|
flags: {
|
||||||
|
...state.flags,
|
||||||
|
finalizeModule: false,
|
||||||
|
finalizeExam: true,
|
||||||
|
pendingEvaluation: hasUnevaluatedSolutions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (state.moduleIndex < state.selectedModules.length - 1) {
|
||||||
return {
|
return {
|
||||||
moduleIndex: state.moduleIndex + 1,
|
moduleIndex: state.moduleIndex + 1,
|
||||||
partIndex: 0,
|
partIndex: 0,
|
||||||
@@ -123,27 +133,14 @@ export const rootReducer = (
|
|||||||
finalizeModule: false,
|
finalizeModule: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// if there are no modules left, flag finalizeExam
|
|
||||||
// so that the stats are uploaded in ExamPage
|
|
||||||
// and the Finish view is set there, no need to
|
|
||||||
// dispatch another init
|
|
||||||
return {
|
|
||||||
showSolutions: true,
|
|
||||||
flags: {
|
|
||||||
...state.flags,
|
|
||||||
finalizeModule: false,
|
|
||||||
finalizeExam: true,
|
|
||||||
pendingEvaluation: hasUnevaluatedSolutions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'FINALIZE_MODULE_SOLUTIONS': {
|
case 'FINALIZE_MODULE_SOLUTIONS': {
|
||||||
if (state.flags.reviewAll) {
|
if (state.flags.reviewAll) {
|
||||||
const notLastModule = state.moduleIndex < state.selectedModules.length;
|
const notLastModule = state.moduleIndex < state.selectedModules.length - 1;
|
||||||
const moduleIndex = notLastModule ? state.moduleIndex + 1 : -1;
|
const moduleIndex = notLastModule ? state.moduleIndex + 1 : -1;
|
||||||
|
|
||||||
if (notLastModule) {
|
if (notLastModule) {
|
||||||
return {
|
return {
|
||||||
questionIndex: 0,
|
questionIndex: 0,
|
||||||
@@ -160,12 +157,12 @@ export const rootReducer = (
|
|||||||
moduleIndex: -1
|
moduleIndex: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
moduleIndex: -1
|
moduleIndex: -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
case 'UPDATE_EXAMS': {
|
case 'UPDATE_EXAMS': {
|
||||||
const exams = state.exams.map((e) => updateExamWithUserSolutions(e, state.userSolutions));
|
const exams = state.exams.map((e) => updateExamWithUserSolutions(e, state.userSolutions));
|
||||||
|
|||||||
Reference in New Issue
Block a user