Listening preview and some more patches
This commit is contained in:
@@ -1,36 +1,63 @@
|
||||
import {ListeningExam, MultipleChoiceExercise, UserSolution} from "@/interfaces/exam";
|
||||
import {useEffect, useState} from "react";
|
||||
import {renderExercise} from "@/components/Exercises";
|
||||
import {renderSolution} from "@/components/Solutions";
|
||||
import { ListeningExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam";
|
||||
import { 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 from "@/stores/examStore";
|
||||
import {countExercises} from "@/utils/moduleUtils";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
||||
import { countExercises } from "@/utils/moduleUtils";
|
||||
import PartDivider from "./Navigation/SectionDivider";
|
||||
|
||||
interface Props {
|
||||
exam: ListeningExam;
|
||||
showSolutions?: boolean;
|
||||
preview?: boolean;
|
||||
onFinish: (userSolutions: UserSolution[]) => void;
|
||||
}
|
||||
|
||||
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, onFinish}: Props) {
|
||||
export default function Listening({ exam, showSolutions = false, preview = false, onFinish }: Props) {
|
||||
const listeningBgColor = "bg-ielts-listening-light";
|
||||
|
||||
const [timesListened, setTimesListened] = useState(0);
|
||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||
|
||||
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||
const {hasExamEnded, setHasExamEnded} = useExamStore((state) => state);
|
||||
const {partIndex, setPartIndex} = useExamStore((state) => state);
|
||||
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
|
||||
const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]);
|
||||
|
||||
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(-1);
|
||||
}, [setExerciseIndex, showSolutions]);
|
||||
@@ -52,7 +79,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice];
|
||||
}
|
||||
|
||||
setMultipleChoicesDone(previousMultipleChoice.map((x) => ({id: x.id, amount: x.questions.length - 1})));
|
||||
setMultipleChoicesDone(previousMultipleChoice.map((x) => ({ id: x.id, amount: x.questions.length - 1 })));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -74,11 +101,11 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
const nextExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]);
|
||||
setUserSolutions([...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}]);
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), { id: exercise.id, amount: storeQuestionIndex }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -109,7 +136,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
setHasExamEnded(false);
|
||||
|
||||
if (solution) {
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]);
|
||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
|
||||
} else {
|
||||
onFinish(userSolutions);
|
||||
}
|
||||
@@ -118,7 +145,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
if (solution) {
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]);
|
||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "listening", exam: exam.id }]);
|
||||
}
|
||||
setStoreQuestionIndex(0);
|
||||
|
||||
@@ -171,90 +198,109 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
||||
|
||||
const renderAudioPlayer = () => (
|
||||
<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 following audio attentively.</h4>
|
||||
<span className="text-base">
|
||||
{exam.parts[partIndex].audio.repeatableTimes > 0
|
||||
? `You will only be allowed to listen to the audio ${exam.parts[partIndex].audio.repeatableTimes - timesListened} time(s).`
|
||||
: "You may listen to the audio as many times as you would like."}
|
||||
</span>
|
||||
</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={timesListened === exam.parts[partIndex].audio.repeatableTimes}
|
||||
disablePause
|
||||
/>
|
||||
</div>
|
||||
{exam.parts[partIndex].audio ? (
|
||||
<>
|
||||
<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">
|
||||
{exam.parts[partIndex].audio.repeatableTimes > 0
|
||||
? `You will only be allowed to listen to the audio ${exam.parts[partIndex].audio.repeatableTimes - timesListened} time(s).`
|
||||
: "You may listen to the audio as many times as you would like."}
|
||||
</span>
|
||||
</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={timesListened === exam.parts[partIndex].audio.repeatableTimes}
|
||||
disablePause
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span>This section will be displayed the audio once it has been generated.</span>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||
<div className="flex flex-col h-full w-full gap-8 justify-between">
|
||||
<ModuleTitle
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
minTimer={exam.minTimer}
|
||||
{showPartDivider ?
|
||||
<PartDivider
|
||||
module="listening"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{/* Audio Player for the Instructions */}
|
||||
{partIndex === -1 && renderAudioInstructionsPlayer()}
|
||||
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} />
|
||||
<div className="flex flex-col h-full w-full gap-8 justify-between">
|
||||
<ModuleTitle
|
||||
exerciseIndex={calculateExerciseIndex()}
|
||||
minTimer={exam.minTimer}
|
||||
module="listening"
|
||||
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
|
||||
disableTimer={showSolutions}
|
||||
/>
|
||||
{/* Audio Player for the Instructions */}
|
||||
{partIndex === -1 && renderAudioInstructionsPlayer()}
|
||||
|
||||
{/* Part's audio player */}
|
||||
{partIndex > -1 && renderAudioPlayer()}
|
||||
{/* Part's audio player */}
|
||||
{partIndex > -1 && renderAudioPlayer()}
|
||||
|
||||
{/* Exercise renderer */}
|
||||
{exerciseIndex > -1 &&
|
||||
partIndex > -1 &&
|
||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||
!showSolutions &&
|
||||
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
||||
{/* 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 < exam.parts[partIndex].exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</div>
|
||||
{/* Solution renderer */}
|
||||
{exerciseIndex > -1 &&
|
||||
partIndex > -1 &&
|
||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||
showSolutions &&
|
||||
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
|
||||
</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);
|
||||
{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>
|
||||
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
|
||||
setPartIndex(partIndex - 1);
|
||||
}}
|
||||
className="max-w-[200px] w-full">
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Button color="purple" onClick={() => nextExercise()} 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={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end">
|
||||
Start now
|
||||
</Button>
|
||||
)}
|
||||
{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={() => nextExercise()} className="max-w-[200px] self-end w-full justify-self-end">
|
||||
Start now
|
||||
</Button>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function Reading({ exam, showSolutions = false, preview = false,
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||
const [isTextMinimized, setIsTextMinimzed] = useState(false);
|
||||
const [exerciseType, setExerciseType] = useState("");
|
||||
|
||||
|
||||
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 !== "");
|
||||
|
||||
@@ -304,15 +304,17 @@ export default function Reading({ exam, showSolutions = false, preview = false,
|
||||
|
||||
return (
|
||||
<>
|
||||
{(showPartDivider) ?
|
||||
<PartDivider
|
||||
module="reading"
|
||||
sectionLabel="Part"
|
||||
defaultTitle="Reading exam"
|
||||
section={exam.parts[partIndex]}
|
||||
sectionIndex={partIndex}
|
||||
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
|
||||
/> : (
|
||||
{showPartDivider ?
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<PartDivider
|
||||
module="reading"
|
||||
sectionLabel="Part"
|
||||
defaultTitle="Reading exam"
|
||||
section={exam.parts[partIndex]}
|
||||
sectionIndex={partIndex}
|
||||
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
|
||||
/>
|
||||
</div> : (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full gap-8">
|
||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||
|
||||
Reference in New Issue
Block a user