Merge, do not push to develop yet, Listening.tsx is not updated
This commit is contained in:
@@ -8,6 +8,7 @@ import { CommonProps } from "../types";
|
||||
import { v4 } from "uuid";
|
||||
import MCDropdown from "./MCDropdown";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||
import PracticeBadge from "@/components/Low/PracticeBadge";
|
||||
|
||||
const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -166,7 +167,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{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 relative", (!headerButtons && !footerButtons) && "mb-20")}>
|
||||
{variant !== "mc" && (
|
||||
<span className="text-sm w-full leading-6">
|
||||
{prompt.split("\\n").map((line, index) => (
|
||||
@@ -177,6 +178,7 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">{memoizedLines}</span>
|
||||
{variant !== "mc" && (
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-4">
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { InteractiveSpeakingExercise } from "@/interfaces/exam";
|
||||
import { CommonProps } from "../types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill } from "react-icons/bs";
|
||||
import dynamic from "next/dynamic";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||
|
||||
const Waveform = dynamic(() => import("../../Waveform"), { ssr: false });
|
||||
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const InteractiveSpeaking: React.FC<InteractiveSpeakingExercise & CommonProps> = ({
|
||||
id,
|
||||
title,
|
||||
first_title,
|
||||
second_title,
|
||||
examID,
|
||||
type,
|
||||
prompts,
|
||||
userSolutions,
|
||||
isPractice = false,
|
||||
registerSolution,
|
||||
headerButtons,
|
||||
footerButtons,
|
||||
preview,
|
||||
}) => {
|
||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||
const [answers, setAnswers] = useState<{ prompt: string; blob: string; questionIndex: number }[]>([]);
|
||||
|
||||
const examState = useExamStore((state) => state);
|
||||
const persistentExamState = usePersistentExamStore((state) => state);
|
||||
|
||||
const { questionIndex } = !preview ? examState : persistentExamState;
|
||||
|
||||
useEffect(() => {
|
||||
setAnswers((prev) => [...prev.filter(x => x.questionIndex !== questionIndex), {
|
||||
questionIndex: questionIndex,
|
||||
prompt: prompts[questionIndex].text,
|
||||
blob: mediaBlob!,
|
||||
}]);
|
||||
setMediaBlob(undefined);
|
||||
}, [answers, mediaBlob, prompts, questionIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
registerSolution(() => ({
|
||||
exercise: id,
|
||||
solutions: answers,
|
||||
score: { correct: 100, total: 100, missing: 0 },
|
||||
type,
|
||||
isPractice
|
||||
}));
|
||||
}, [id, answers, mediaBlob, type, isPractice, prompts, registerSolution]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userSolutions.length > 0 && answers.length === 0) {
|
||||
const solutions = userSolutions as unknown as typeof answers;
|
||||
setAnswers(solutions);
|
||||
|
||||
if (!mediaBlob) setMediaBlob(solutions.find((x) => x.questionIndex === questionIndex)?.blob);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userSolutions, mediaBlob, answers]);
|
||||
|
||||
useEffect(() => {
|
||||
let recordingInterval: NodeJS.Timer | undefined = undefined;
|
||||
if (isRecording) {
|
||||
recordingInterval = setInterval(() => setRecordingDuration((prev) => prev + 1), 1000);
|
||||
} else if (recordingInterval) {
|
||||
clearInterval(recordingInterval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (recordingInterval) clearInterval(recordingInterval);
|
||||
};
|
||||
}, [isRecording]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 mt-4 w-full">
|
||||
{headerButtons}
|
||||
<div className="flex flex-col h-full w-full gap-9">
|
||||
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-semibold">{!!first_title && !!second_title ? `${first_title} & ${second_title}` : title}</span>
|
||||
</div>
|
||||
{prompts && prompts.length > 0 && (
|
||||
<div className="flex flex-col gap-4 w-full items-center">
|
||||
<video key={questionIndex} autoPlay controls className="max-w-3xl rounded-xl">
|
||||
<source src={prompts[questionIndex].video_url} />
|
||||
</video>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ReactMediaRecorder
|
||||
audio
|
||||
key={questionIndex}
|
||||
onStop={(blob) => setMediaBlob(blob)}
|
||||
render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (
|
||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||
<p className="text-base font-normal">Record your answer:</p>
|
||||
<div className="flex gap-8 items-center justify-center py-8">
|
||||
{status === "idle" && (
|
||||
<>
|
||||
<div className="w-full h-2 max-w-4xl bg-mti-gray-smoke rounded-full" />
|
||||
{status === "idle" && (
|
||||
<BsMicFill
|
||||
onClick={() => {
|
||||
setRecordingDuration(0);
|
||||
startRecording();
|
||||
setIsRecording(true);
|
||||
}}
|
||||
className="h-5 w-5 text-mti-gray-cool cursor-pointer"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{status === "recording" && (
|
||||
<>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-xs w-9">
|
||||
{Math.floor(recordingDuration / 60)
|
||||
.toString(10)
|
||||
.padStart(2, "0")}
|
||||
:
|
||||
{Math.floor(recordingDuration % 60)
|
||||
.toString(10)
|
||||
.padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 max-w-4xl bg-mti-gray-smoke rounded-full" />
|
||||
<div className="flex gap-4 items-center">
|
||||
<BsPauseCircle
|
||||
onClick={() => {
|
||||
setIsRecording(false);
|
||||
pauseRecording();
|
||||
}}
|
||||
className="text-red-500 w-8 h-8 cursor-pointer"
|
||||
/>
|
||||
<BsCheckCircleFill
|
||||
onClick={() => {
|
||||
setIsRecording(false);
|
||||
stopRecording();
|
||||
}}
|
||||
className="text-mti-purple-light w-8 h-8 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{status === "paused" && (
|
||||
<>
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-xs w-9">
|
||||
{Math.floor(recordingDuration / 60)
|
||||
.toString(10)
|
||||
.padStart(2, "0")}
|
||||
:
|
||||
{Math.floor(recordingDuration % 60)
|
||||
.toString(10)
|
||||
.padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 max-w-4xl bg-mti-gray-smoke rounded-full" />
|
||||
<div className="flex gap-4 items-center">
|
||||
<BsPlayCircle
|
||||
onClick={() => {
|
||||
setIsRecording(true);
|
||||
resumeRecording();
|
||||
}}
|
||||
className="text-mti-purple-light w-8 h-8 cursor-pointer"
|
||||
/>
|
||||
<BsCheckCircleFill
|
||||
onClick={() => {
|
||||
setIsRecording(false);
|
||||
stopRecording();
|
||||
}}
|
||||
className="text-mti-purple-light w-8 h-8 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{status === "stopped" && mediaBlobUrl && (
|
||||
<>
|
||||
<Waveform audio={mediaBlobUrl} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
||||
<div className="flex gap-4 items-center">
|
||||
<BsTrashFill
|
||||
className="text-mti-gray-cool cursor-pointer w-5 h-5"
|
||||
onClick={() => {
|
||||
setRecordingDuration(0);
|
||||
clearBlobUrl();
|
||||
setMediaBlob(undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
<BsMicFill
|
||||
onClick={() => {
|
||||
clearBlobUrl();
|
||||
setRecordingDuration(0);
|
||||
startRecording();
|
||||
setIsRecording(true);
|
||||
setMediaBlob(undefined);
|
||||
}}
|
||||
className="h-5 w-5 text-mti-gray-cool cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{footerButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSpeaking;
|
||||
@@ -4,6 +4,7 @@ import { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { CommonProps } from "../types";
|
||||
import { DndContext, DragEndEvent } from "@dnd-kit/core";
|
||||
import { DraggableOptionArea, DroppableQuestionArea } from "./DragNDrop";
|
||||
import PracticeBadge from "../../Low/PracticeBadge";
|
||||
|
||||
const MatchSentences: React.FC<MatchSentencesExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -61,7 +62,7 @@ const MatchSentences: React.FC<MatchSentencesExercise & CommonProps> = ({
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<div className="flex flex-col gap-8 w-full items-center justify-between bg-mti-gray-smoke rounded-xl px-24 py-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import PracticeBadge from "@/components/Low/PracticeBadge";
|
||||
import { MultipleChoiceQuestion } from "@/interfaces/exam";
|
||||
import clsx from "clsx";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
@@ -8,6 +9,7 @@ interface Props {
|
||||
userSolution: string | undefined;
|
||||
onSelectOption?: (option: string) => void;
|
||||
showSolution?: boolean;
|
||||
isPractice?: boolean
|
||||
}
|
||||
|
||||
const Question: React.FC<MultipleChoiceQuestion & Props> = ({
|
||||
@@ -17,6 +19,7 @@ const Question: React.FC<MultipleChoiceQuestion & Props> = ({
|
||||
options,
|
||||
userSolution,
|
||||
onSelectOption,
|
||||
isPractice,
|
||||
}) => {
|
||||
const renderPrompt = (prompt: string) => {
|
||||
return reactStringReplace(prompt, /(<u>.*?<\/u>)/g, (match) => {
|
||||
@@ -26,11 +29,12 @@ const Question: React.FC<MultipleChoiceQuestion & Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-8 relative">
|
||||
{isPractice && <PracticeBadge className="absolute -top-4 -right-12" />}
|
||||
{isNaN(Number(id)) ? (
|
||||
<span className="text-lg">{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
|
||||
<span className={clsx("text-lg", isPractice && "text-mti-red")}>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
|
||||
) : (
|
||||
<span className="text-lg">
|
||||
<span className={clsx("text-lg", isPractice && "text-mti-red")}>
|
||||
<>
|
||||
{id} - <span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")}</span>
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import clsx from "clsx";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { CommonProps } from "../types";
|
||||
import Question from "./Question";
|
||||
import PracticeBadge from "../../Low/PracticeBadge";
|
||||
|
||||
|
||||
const MultipleChoice: React.FC<MultipleChoiceExercise & CommonProps> = ({
|
||||
@@ -81,6 +82,7 @@ const MultipleChoice: React.FC<MultipleChoiceExercise & CommonProps> = ({
|
||||
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}
|
||||
isPractice={isPractice}
|
||||
userSolution={answers.find((x) => question.id === x.question)?.option}
|
||||
onSelectOption={(option) => onSelectOption(option, question)}
|
||||
/>
|
||||
@@ -93,6 +95,7 @@ const MultipleChoice: React.FC<MultipleChoiceExercise & CommonProps> = ({
|
||||
{questionIndex < questions.length && (
|
||||
<Question
|
||||
{...questions[questionIndex]}
|
||||
isPractice={isPractice}
|
||||
userSolution={answers.find((x) => questions[questionIndex].id === x.question)?.option}
|
||||
onSelectOption={(option) => onSelectOption(option, questions[questionIndex])}
|
||||
/>
|
||||
@@ -103,6 +106,7 @@ const MultipleChoice: React.FC<MultipleChoiceExercise & CommonProps> = ({
|
||||
<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]}
|
||||
isPractice={isPractice}
|
||||
userSolution={answers.find((x) => questions[questionIndex + 1].id === x.question)?.option}
|
||||
onSelectOption={(option) => onSelectOption(option, questions[questionIndex + 1])}
|
||||
/>
|
||||
|
||||
@@ -6,6 +6,7 @@ import dynamic from "next/dynamic";
|
||||
import Button from "../Low/Button";
|
||||
import Modal from "../Modal";
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||
import PracticeBadge from "../Low/PracticeBadge";
|
||||
|
||||
const Waveform = dynamic(() => import("../Waveform"), { ssr: false });
|
||||
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||
@@ -133,6 +134,8 @@ const Speaking: React.FC<SpeakingExercise & CommonProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
|
||||
{prompts && prompts.length > 0 && (
|
||||
<div className="w-full h-full flex flex-col gap-4">
|
||||
<textarea
|
||||
|
||||
@@ -3,6 +3,7 @@ import clsx from "clsx";
|
||||
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { CommonProps } from "./types";
|
||||
import Button from "../Low/Button";
|
||||
import PracticeBadge from "../Low/PracticeBadge";
|
||||
|
||||
const TrueFalse: React.FC<TrueFalseExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -80,6 +81,7 @@ const TrueFalse: React.FC<TrueFalseExercise & CommonProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm w-full leading-6">You can click a selected option again to deselect it.</span>
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
<div className="bg-mti-gray-smoke rounded-xl px-5 py-6 flex flex-col gap-8">
|
||||
{questions.map((question, index) => {
|
||||
const id = question.id.toString();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import { CommonProps } from "../types";
|
||||
import Blank from "./Blank";
|
||||
import PracticeBadge from "../../Low/PracticeBadge";
|
||||
|
||||
const WriteBlanks: React.FC<WriteBlanksExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -63,7 +64,7 @@ const WriteBlanks: React.FC<WriteBlanksExercise & CommonProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 relative">
|
||||
{headerButtons}
|
||||
<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">
|
||||
@@ -74,6 +75,7 @@ const WriteBlanks: React.FC<WriteBlanksExercise & CommonProps> = ({
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
<span className="bg-mti-gray-smoke rounded-xl px-5 py-6">
|
||||
{text.split("\\n").map((line, index) => (
|
||||
<p key={index}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Dialog, DialogPanel, Transition, TransitionChild } from "@headlessui/re
|
||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||
import { CommonProps } from "./types";
|
||||
import { toast } from "react-toastify";
|
||||
import PracticeBadge from "../Low/PracticeBadge";
|
||||
|
||||
const Writing: React.FC<WritingExercise & CommonProps> = ({
|
||||
id,
|
||||
@@ -145,6 +146,8 @@ const Writing: React.FC<WritingExercise & CommonProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isPractice && <PracticeBadge className="w-fit self-end" />}
|
||||
|
||||
<div className="w-full h-full flex flex-col gap-4">
|
||||
<span className="whitespace-pre-wrap">{suffix}</span>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user