Merge, do not push to develop yet, Listening.tsx is not updated
This commit is contained in:
@@ -7,7 +7,7 @@ import useExamStore from "@/stores/exam";
|
||||
import { calculateBandScore, getGradingLabel } from "@/utils/score";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
BsArrowCounterclockwise,
|
||||
BsBan,
|
||||
@@ -40,6 +40,7 @@ interface Props {
|
||||
user: User;
|
||||
modules: Module[];
|
||||
scores: Score[];
|
||||
practiceScores: Score[]
|
||||
information: {
|
||||
timeSpent?: number;
|
||||
inactivity?: number;
|
||||
@@ -51,14 +52,17 @@ interface Props {
|
||||
destination?: string
|
||||
}
|
||||
|
||||
export default function Finish({ user, scores, modules, information, solutions, isLoading, assignment, onViewResults, destination }: Props) {
|
||||
export default function Finish({ user, practiceScores, scores, modules, information, solutions, isLoading, assignment, onViewResults, destination }: Props) {
|
||||
const [selectedModule, setSelectedModule] = useState(modules[0]);
|
||||
const [selectedScore, setSelectedScore] = useState<Score>(scores.find((x) => x.module === modules[0])!);
|
||||
const [selectedPracticeScore, setSelectedPracticeScore] = useState<Score | undefined>(practiceScores.find((x) => x.module === modules[0]));
|
||||
const [isExtraInformationOpen, setIsExtraInformationOpen] = useState(false);
|
||||
const {selectedModules, exams, dispatch} = useExamStore((s) => s);
|
||||
|
||||
const aiUsage = Math.round(ai_usage(solutions) * 100);
|
||||
const { gradingSystem } = useGradingSystem();
|
||||
|
||||
const entity = useMemo(() => assignment?.entity || user.entities[0]?.id || "", [assignment?.entity, user.entities])
|
||||
const { gradingSystem } = useGradingSystem(entity);
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -234,7 +238,7 @@ export default function Finish({ user, scores, modules, information, solutions,
|
||||
{!isLoading && !(assignment && !assignment.released) && (
|
||||
<div className="mb-20 mt-32 flex w-full items-center justify-between gap-9">
|
||||
<span className="max-w-3xl">{moduleResultText(selectedModule, bandScore)}</span>
|
||||
<div className="flex gap-9 px-16">
|
||||
<div className="flex items-center gap-9 px-16">
|
||||
<div
|
||||
className={clsx("radial-progress overflow-hidden", moduleColors[selectedModule].progress)}
|
||||
style={
|
||||
@@ -265,21 +269,32 @@ export default function Finish({ user, scores, modules, information, solutions,
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="bg-mti-purple-light mt-1 h-3 w-3 rounded-full" />
|
||||
<div className="bg-mti-purple-light mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-purple-light">{selectedScore.correct.toString().padStart(2, "0")}</span>
|
||||
<span className="text-lg">Correct</span>
|
||||
<span className="text-lg whitespace-nowrap">Correct (Graded)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="bg-mti-rose-light mt-1 h-3 w-3 rounded-full" />
|
||||
<div className="bg-mti-rose-light mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-rose-light">
|
||||
{(selectedScore.total - selectedScore.correct).toString().padStart(2, "0")}
|
||||
</span>
|
||||
<span className="text-lg">Wrong</span>
|
||||
<span className="text-lg whitespace-nowrap">Wrong (Graded)</span>
|
||||
</div>
|
||||
</div>
|
||||
{selectedPracticeScore && (
|
||||
<div className="flex gap-2">
|
||||
<div className="bg-mti-green mt-1 h-3 min-h-[0.75rem] w-3 min-w-[0.75rem] rounded-full" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-mti-green">
|
||||
{selectedPracticeScore.correct} / {selectedScore.total}
|
||||
</span>
|
||||
<span className="text-lg whitespace-nowrap">Practice Questions</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-28 h-full" />
|
||||
|
||||
372
src/exams/Listening.tsx.bak
Normal file
372
src/exams/Listening.tsx.bak
Normal file
@@ -0,0 +1,372 @@
|
||||
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>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user