Merge, do not push to develop yet, Listening.tsx is not updated

This commit is contained in:
Carlos-Mesquita
2024-11-26 10:33:02 +00:00
44 changed files with 1989 additions and 1452 deletions

View File

@@ -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">

View File

@@ -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;

View File

@@ -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">

View File

@@ -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>
</>

View File

@@ -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])}
/>

View File

@@ -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

View File

@@ -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();

View File

@@ -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}>

View File

@@ -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