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

View File

@@ -6,102 +6,104 @@ import { BsArrowDown, BsArrowUp } from "react-icons/bs"
import Button from "../Low/Button"
interface Props<T> {
data: T[]
columns: ColumnDef<any, any>[]
searchFields: string[][]
size?: number
onDownload?: (rows: T[]) => void
data: T[]
columns: ColumnDef<any, any>[]
searchFields: string[][]
size?: number
onDownload?: (rows: T[]) => void
isDownloadLoading?: boolean
searchPlaceholder?: string
}
export default function Table<T>({ data, columns, searchFields, size = 16, onDownload }: Props<T>) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 16,
})
export default function Table<T>({ data, columns, searchFields, size = 16, onDownload, isDownloadLoading, searchPlaceholder }: Props<T>) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: size,
})
const { rows, renderSearch } = useListSearch<T>(searchFields, data);
const { rows, renderSearch } = useListSearch<T>(searchFields, data, searchPlaceholder);
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
pagination
}
});
const table = useReactTable({
data: rows,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
state: {
pagination
}
});
return (
<div className="w-full flex flex-col gap-2">
<div className="w-full flex gap-2 items-end">
{renderSearch()}
{onDownload && (
<Button className="w-full max-w-[200px] mb-1" variant="outline" onClick={() => onDownload(rows)}>
Download List
</Button>
)
}
</div>
return (
<div className="w-full flex flex-col gap-2">
<div className="w-full flex gap-2 items-end">
{renderSearch()}
{onDownload && (
<Button isLoading={isDownloadLoading} className="w-full max-w-[200px] mb-1" variant="outline" onClick={() => onDownload(rows)}>
Download
</Button>
)
}
</div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
<div>| Total: {table.getRowCount().toLocaleString()}</div>
</span>
<Button className="w-[200px]" disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>
Next Page
</Button>
</div>
</div>
<div className="w-full flex gap-2 justify-between items-center">
<div className="flex items-center gap-4 w-fit">
<Button className="w-[200px] h-fit" disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>
Previous Page
</Button>
</div>
<div className="flex items-center gap-4 w-fit">
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
<div>| Total: {table.getRowCount().toLocaleString()}</div>
</span>
<Button className="w-[200px]" disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>
Next Page
</Button>
</div>
</div>
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4 px-4 text-left" key={header.id} colSpan={header.colSpan}>
<div
className={clsx(header.column.getCanSort() && 'cursor-pointer select-none', 'flex items-center gap-2')}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: <BsArrowUp />,
desc: <BsArrowDown />,
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2 w-full">
{table.getRowModel().rows.map((row) => (
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
<table className="rounded-xl bg-mti-purple-ultralight/40 w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th className="py-4 px-4 text-left" key={header.id} colSpan={header.colSpan}>
<div
className={clsx(header.column.getCanSort() && 'cursor-pointer select-none', 'flex items-center gap-2')}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: <BsArrowUp />,
desc: <BsArrowDown />,
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="px-2 w-full">
{table.getRowModel().rows.map((row) => (
<tr className="odd:bg-white even:bg-mti-purple-ultralight/40 rounded-lg py-2" key={row.id}>
{row.getVisibleCells().map((cell) => (
<td className="px-4 py-2 items-center w-fit" key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { clsx } from "clsx"
interface Props {
className?: string
}
export default function PracticeBadge({ className }: Props) {
return (
<div className={clsx("bg-mti-rose/50 text-white px-2 py-1 rounded-full ring-1 ring-mti-red", className)}>Practice Exercise (Question Ungraded)</div>
)
}

View File

@@ -77,7 +77,6 @@ interface StatsGridItemProps {
assignments: Assignment[];
users: User[];
training?: boolean;
gradingSystem?: Step[];
selectedTrainingExams?: string[];
maxTrainingExams?: number;
setSelectedTrainingExams?: React.Dispatch<React.SetStateAction<string[]>>;
@@ -92,7 +91,6 @@ const StatsGridItem: React.FC<StatsGridItemProps> = ({
users,
training,
selectedTrainingExams,
gradingSystem,
setSelectedTrainingExams,
renderPdfIcon,
width = undefined,

View File

@@ -3,7 +3,7 @@ import {useEffect, useState} from "react";
import {motion} from "framer-motion";
import TimerEndedModal from "../TimerEndedModal";
import clsx from "clsx";
import {BsStopwatch} from "react-icons/bs";
import { BsStopwatch } from "react-icons/bs";
interface Props {
minTimer: number;
@@ -11,7 +11,7 @@ interface Props {
standalone?: boolean;
}
const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) => {
const Timer: React.FC<Props> = ({ minTimer, disableTimer, standalone = false }) => {
const [timer, setTimer] = useState(minTimer * 60);
const [showModal, setShowModal] = useState(false);
const [warningMode, setWarningMode] = useState(false);
@@ -51,9 +51,9 @@ const Timer: React.FC<Props> = ({minTimer, disableTimer, standalone = false}) =>
standalone ? "top-10" : "top-4",
warningMode && !disableTimer && "bg-mti-red-light text-mti-gray-seasalt",
)}
initial={{scale: warningMode && !disableTimer ? 0.8 : 1}}
animate={{scale: warningMode && !disableTimer ? 1.1 : 1}}
transition={{repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut"}}>
initial={{ scale: warningMode && !disableTimer ? 0.8 : 1 }}
animate={{ scale: warningMode && !disableTimer ? 1.1 : 1 }}
transition={{ repeat: Infinity, repeatType: "reverse", duration: 0.5, ease: "easeInOut" }}>
<BsStopwatch className="w-6 h-6" />
<span className="text-lg font-bold w-12">
{timer > 0 && (

View File

@@ -6,6 +6,7 @@ import reactStringReplace from "react-string-replace";
import { CommonProps } from ".";
import Button from "../Low/Button";
import { v4 } from "uuid";
import PracticeBadge from "../Low/PracticeBadge";
function Question({
id,
@@ -14,7 +15,8 @@ function Question({
solution,
options,
userSolution,
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean }) {
isPractice = false
}: MultipleChoiceQuestion & { userSolution: string | undefined; onSelectOption?: (option: string) => void; showSolution?: boolean, isPractice?: boolean }) {
const { userSolutions } = useExamStore((state) => state);
const questionShuffleMap = userSolutions.reduce((foundMap, userSolution) => {
@@ -44,7 +46,8 @@ function Question({
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 relative">
{isPractice && <PracticeBadge className="absolute -top-4 -right-12" />}
{isNaN(Number(id)) ? (
<span>{renderPrompt(prompt).filter((x) => x?.toString() !== "<u>")} </span>
) : (
@@ -89,7 +92,7 @@ function Question({
);
}
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, headerButtons, footerButtons}: MultipleChoiceExercise & CommonProps) {
export default function MultipleChoice({ id, type, prompt, questions, userSolutions, headerButtons, footerButtons, isPractice = false}: MultipleChoiceExercise & CommonProps) {
const { questionIndex, setQuestionIndex, partIndex, exam } = useExamStore((state) => state);
const stats = useExamStore((state) => state.userSolutions);
@@ -116,6 +119,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
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={userSolutions.find((x) => question.id === x.question)?.option}
/>
</div>
@@ -127,6 +131,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
{questionIndex < questions.length && (
<Question
{...questions[questionIndex]}
isPractice={isPractice}
userSolution={userSolutions.find((x) => questions[questionIndex].id === x.question)?.option}
/>
)}
@@ -136,6 +141,7 @@ export default function MultipleChoice({ id, type, prompt, questions, userSoluti
<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={userSolutions.find((x) => questions[questionIndex + 1].id === x.question)?.option}
/>
</div>