ENCOA-149, ENCOA-150, ENCOA-152, ENCOA-153, ENCOA-155, ENCOA-156, ENCOA-157, ENCOA-158, ENCOA-161 -> Updated the mc buttons, no longer shows a context only div on parts that have context, removed line numbers on lines between paragraphs, applied bold and underline to 'not correct' in underline prompts, added another pop up to confirm submission.
This commit is contained in:
@@ -173,23 +173,11 @@ const FillBlanks: React.FC<FillBlanksExercise & CommonProps> = ({
|
||||
className={clsx(
|
||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base",
|
||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
||||
"border-mti-purple-light",
|
||||
"!bg-mti-purple-light !text-white",
|
||||
)}>
|
||||
<span className="font-semibold">{key}.</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
|
||||
/*<button
|
||||
className={clsx(
|
||||
"border border-mti-purple-light rounded-full px-3 py-0.5 transition ease-in-out duration-300",
|
||||
!!answers.find((x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id) &&
|
||||
"bg-mti-purple-dark text-white",
|
||||
)}
|
||||
key={v4()}
|
||||
onClick={() => onSelection(currentMCSelection.id, value)}
|
||||
>
|
||||
{value}
|
||||
</button>;*/
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ function Question({
|
||||
onClick={() => (onSelectOption ? onSelectOption(option.id.toString()) : null)}
|
||||
className={clsx(
|
||||
"flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base select-none",
|
||||
userSolution === option.id.toString() && "border-mti-purple-light",
|
||||
userSolution === option.id.toString() && "!bg-mti-purple-light !text-white",
|
||||
)}>
|
||||
<span className="font-semibold">{option.id.toString()}.</span>
|
||||
<span>{option.text}</span>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import { moduleLabels } from "@/utils/moduleUtils";
|
||||
import clsx from "clsx";
|
||||
import { Fragment, ReactNode, useCallback, useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { BsBook, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsStopwatch } from "react-icons/bs";
|
||||
import ProgressBar from "../Low/ProgressBar";
|
||||
import Timer from "./Timer";
|
||||
import { Exam, Exercise, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
||||
import { Exercise, LevelExam, MultipleChoiceExercise, ShuffleMap, UserSolution } from "@/interfaces/exam";
|
||||
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import Button from "../Low/Button";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import useExamStore from "@/stores/examStore";
|
||||
import Modal from "../Modal";
|
||||
import React from "react";
|
||||
@@ -145,7 +143,7 @@ export default function ModuleTitle({
|
||||
return (
|
||||
<div key={index} className="text-2xl font-semibold flex flex-col gap-2">
|
||||
{partInstructions.split("\\n").map((line, lineIndex) => (
|
||||
<span key={lineIndex}>{line}</span>
|
||||
<span key={lineIndex} dangerouslySetInnerHTML={{__html: line.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,11 @@ interface Props {
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
children?: ReactElement;
|
||||
}
|
||||
|
||||
export default function Modal({isOpen, title, className, onClose, children}: Props) {
|
||||
export default function Modal({isOpen, title, className, titleClassName, onClose, children}: Props) {
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
||||
@@ -41,7 +42,7 @@ export default function Modal({isOpen, title, className, onClose, children}: Pro
|
||||
className,
|
||||
)}>
|
||||
{title && (
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
<Dialog.Title as="h3" className={clsx(titleClassName ? titleClassName : "text-lg font-medium leading-6 text-gray-900")}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,7 @@ const PartDivider: React.FC<Props> = ({ partIndex, part, onNext }) => {
|
||||
<div className={clsx("flex flex-col h-fit border bg-white rounded-3xl p-12 gap-8", part.intro ? "w-3/6" : "items-center my-auto")}>
|
||||
{/** only level for now */}
|
||||
<div className="flex flex-row gap-4 items-center"><div className="w-12 h-12 bg-ielts-level flex items-center justify-center rounded-lg">{moduleIcon["level"]}</div><p className="text-3xl">{part.intro ? `Part ${partIndex + 1}` : "Placement Test"}</p></div>
|
||||
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip">{x}</p>)}
|
||||
{part.intro && part.intro.split('\\n\\n').map((x, index) => <p key={`line-${index}`} className="text-2xl text-clip" dangerouslySetInnerHTML={{__html: x.replace('that is not correct', 'that is <span class="font-bold"><u>not correct</u></span>')}}></p>)}
|
||||
<div className="flex items-center justify-center mt-4">
|
||||
<Button color="purple" onClick={() => onNext()} className="max-w-[200px] self-end w-full text-2xl">
|
||||
{partIndex === 0 ? `Start now`: `Start Part ${partIndex + 1}`}
|
||||
|
||||
@@ -11,6 +11,7 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const [lineNumbers, setLineNumbers] = useState<number[]>([]);
|
||||
const [lineHeight, setLineHeight] = useState<number>(0);
|
||||
const [addBreaksTo, setAddBreaksTo] = useState<number[]>([]);
|
||||
|
||||
const calculateLineNumbers = () => {
|
||||
if (textRef.current) {
|
||||
@@ -31,17 +32,33 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
offscreenElement.style.textAlign = computedStyle.textAlign as CanvasTextAlign;
|
||||
|
||||
const textContent = textRef.current.textContent || '';
|
||||
const lines = textContent.split(/\n/).map(line =>
|
||||
line.split(/(\s+)/).map(word => {
|
||||
|
||||
const paragraphs = textContent.split(/\n\n/);
|
||||
const betweenParagraphs: string[][] = Array.from({ length: paragraphs.length }, () => []);
|
||||
|
||||
const lines = paragraphs.map((line, lineIndex) => {
|
||||
const paragraphWords = line.split(/(\s+)/);
|
||||
return paragraphWords.map((word, wordIndex) => {
|
||||
|
||||
if (lineIndex !== 0 && wordIndex == 0 && lineIndex < paragraphs.length) {
|
||||
betweenParagraphs[lineIndex - 1][1] = word;
|
||||
}
|
||||
|
||||
if (wordIndex == paragraphWords.length - 1 && lineIndex < paragraphs.length) {
|
||||
betweenParagraphs[lineIndex][0] = word;
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = word;
|
||||
return span;
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// Append all spans to offscreenElement
|
||||
lines.forEach(line => {
|
||||
line.forEach(span => offscreenElement.appendChild(span));
|
||||
line.forEach((span, index) => {
|
||||
offscreenElement.appendChild(span);
|
||||
});
|
||||
offscreenElement.appendChild(document.createElement('br'));
|
||||
});
|
||||
|
||||
@@ -59,10 +76,21 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
|
||||
const spans = offscreenElement.querySelectorAll<HTMLSpanElement>('span');
|
||||
|
||||
spans.forEach(span => {
|
||||
let betweenIndex = 0;
|
||||
const addBreaksTo: number[] = [];
|
||||
spans.forEach((span, index)=> {
|
||||
const rect = span.getBoundingClientRect();
|
||||
const top = rect.top;
|
||||
|
||||
if (
|
||||
betweenIndex < paragraphs.length - 1 &&
|
||||
span.textContent === betweenParagraphs[betweenIndex][1] &&
|
||||
spans[index - 1].textContent === betweenParagraphs[betweenIndex][0]
|
||||
) {
|
||||
addBreaksTo.push(currentLine);
|
||||
betweenIndex = betweenIndex + 1;
|
||||
}
|
||||
|
||||
if (currentLineTop !== undefined && top > currentLineTop) {
|
||||
currentLine++;
|
||||
currentLineTop = top;
|
||||
@@ -75,6 +103,9 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
contextWordLine = currentLine;
|
||||
}
|
||||
});
|
||||
|
||||
setAddBreaksTo(addBreaksTo);
|
||||
|
||||
setLineNumbers(processedLines.map((_, index) => index + 1));
|
||||
if (contextWordLine) {
|
||||
setContextWordLine(contextWordLine);
|
||||
@@ -85,7 +116,6 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
calculateLineNumbers();
|
||||
|
||||
@@ -107,30 +137,17 @@ const TextComponent: React.FC<Props> = ({ part, contextWord, setContextWordLine
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [part.context, contextWord]);
|
||||
|
||||
/*if (typeof part.showContextLines === "undefined") {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="border border-mti-gray-dim w-full rounded-full opacity-10" />
|
||||
{!!part.context &&
|
||||
part.context
|
||||
.split(/\n|(\\n)/g)
|
||||
.filter((x) => x && x.length > 0 && x !== "\\n")
|
||||
.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<p key={index}>{line}</p>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}*/
|
||||
|
||||
return (
|
||||
<div className="flex mt-2">
|
||||
<div className="flex-shrink-0 w-8 pr-2">
|
||||
{lineNumbers.map(num => (
|
||||
<>
|
||||
<div key={num} className="text-gray-400 flex justify-end" style={{ lineHeight: `${lineHeight}px` }}>
|
||||
{num}
|
||||
</div>
|
||||
{/* Do not delete the space between the span or else the lines get messed up */}
|
||||
{addBreaksTo.includes(num) && <span className={`h-[${lineHeight}px] whitespace-pre-wrap`}> </span>}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
<div ref={textRef} className="h-fit whitespace-pre-wrap ml-2">
|
||||
|
||||
@@ -14,6 +14,7 @@ import PartDivider from "./PartDivider";
|
||||
import Timer from "@/components/Medium/Timer";
|
||||
import shuffleExamExercise from "./Shuffle";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import Modal from "@/components/Modal";
|
||||
|
||||
interface Props {
|
||||
exam: LevelExam;
|
||||
@@ -51,7 +52,10 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setCurrentSolution
|
||||
} = useExamStore((state) => state);
|
||||
|
||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||
// In case client want to switch back
|
||||
const textRenderDisabled = true;
|
||||
|
||||
const [showSubmissionModal, setShowSubmissionModal] = useState(false);
|
||||
const [showQuestionsModal, setShowQuestionsModal] = useState(false);
|
||||
const [continueAnyways, setContinueAnyways] = useState(false);
|
||||
const [textRender, setTextRender] = useState(false);
|
||||
@@ -59,7 +63,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const [nextExerciseCalled, setNextExerciseCalled] = useState(false);
|
||||
const [currentSolutionSet, setCurrentSolutionSet] = useState(false);
|
||||
|
||||
const [seenParts, setSeenParts] = useState<number[]>(showSolutions ? exam.parts.map((_, index) => index) : [0]);
|
||||
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0]));
|
||||
|
||||
const [questionModalKwargs, setQuestionModalKwargs] = useState<{
|
||||
type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined;
|
||||
@@ -147,24 +151,25 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
}
|
||||
|
||||
if (partIndex + 1 < exam.parts.length && !hasExamEnded) {
|
||||
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.includes(partIndex + 1)) {
|
||||
if (!answeredEveryQuestion(partIndex) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) {
|
||||
modalKwargs();
|
||||
setShowQuestionsModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showSolutions && exam.parts[0].intro && !seenParts.includes(partIndex + 1)) {
|
||||
if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) {
|
||||
setShowPartDivider(true);
|
||||
setBgColor(levelBgColor);
|
||||
}
|
||||
setSeenParts((prev) => [...prev, partIndex + 1])
|
||||
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context) {
|
||||
|
||||
setSeenParts(prev => new Set(prev).add(partIndex + 1));
|
||||
|
||||
if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) {
|
||||
setTextRender(true);
|
||||
}
|
||||
setPartIndex(partIndex + 1);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : questionIndex }]);
|
||||
setCurrentSolutionSet(false);
|
||||
return;
|
||||
}
|
||||
@@ -194,7 +199,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
const previousExercise = (solution?: UserSolution) => {
|
||||
scrollToTop();
|
||||
|
||||
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender) {
|
||||
if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) {
|
||||
setTextRender(true);
|
||||
return;
|
||||
}
|
||||
@@ -221,31 +226,6 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
if (previousExercise.type === "multipleChoice") {
|
||||
setQuestionIndex(previousExercise.questions.length - 1)
|
||||
}
|
||||
const multipleChoiceQuestionsDone = [];
|
||||
for (let i = 0; i < exam.parts.length; i++) {
|
||||
if (i == (partIndex - 1)) break;
|
||||
for (let j = 0; j < exam.parts[i].exercises.length; j++) {
|
||||
const exercise = exam.parts[i].exercises[j];
|
||||
switch(exercise.type) {
|
||||
case 'multipleChoice':
|
||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 })
|
||||
break;
|
||||
case 'fillBlanks':
|
||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 })
|
||||
break;
|
||||
case 'writeBlanks':
|
||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.solutions.length - 1 })
|
||||
break;
|
||||
case 'matchSentences':
|
||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.sentences.length - 1})
|
||||
break;
|
||||
case 'trueFalse':
|
||||
multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1})
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
setMultipleChoicesDone(multipleChoiceQuestionsDone);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -264,7 +244,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
<div className={clsx("flex flex-col gap-6 w-full bg-mti-gray-seasalt rounded-xl mt-4 relative py-8 px-16")}>
|
||||
<>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
{textRender ? (
|
||||
{textRender && !textRenderDisabled ? (
|
||||
<>
|
||||
<h4 className="text-xl font-semibold">
|
||||
Please read the following excerpt attentively, you will then be asked questions about the text you've read.
|
||||
@@ -286,7 +266,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
{textRender && (
|
||||
{textRender && !textRenderDisabled && (
|
||||
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
|
||||
<Button
|
||||
color="purple"
|
||||
@@ -306,16 +286,17 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
);
|
||||
|
||||
const partLabel = () => {
|
||||
const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : '';
|
||||
if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words))
|
||||
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})\n\n${currentExercise.prompt}`
|
||||
return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||
|
||||
if (currentExercise?.type === "multipleChoice") {
|
||||
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})\n\n${currentExercise.prompt}`
|
||||
return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}`
|
||||
}
|
||||
|
||||
if (typeof exam.parts[partIndex].context === "string") {
|
||||
const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise;
|
||||
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})\n\n${nextExercise.prompt}`
|
||||
return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,7 +369,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
if (partIndex === exam.parts.length - 1) {
|
||||
kwargs.type = "submit"
|
||||
kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestion(partIndex));
|
||||
kwargs.onClose = function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||
kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } };
|
||||
}
|
||||
setQuestionModalKwargs(kwargs);
|
||||
}
|
||||
@@ -408,7 +389,7 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
setChangedPrompt(false);
|
||||
return (
|
||||
<>
|
||||
{textRender ?
|
||||
{textRender && !textRenderDisabled ?
|
||||
renderText() :
|
||||
<>
|
||||
{exam.parts[partIndex].context && renderText()}
|
||||
@@ -425,6 +406,25 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
return (
|
||||
<>
|
||||
<div className={clsx("flex flex-col h-full w-full gap-8 items-center", showPartDivider && "justify-center")}>
|
||||
<Modal
|
||||
className={"!w-2/6 !p-8"}
|
||||
titleClassName={"font-bold text-3xl text-mti-rose-light"}
|
||||
isOpen={showSubmissionModal}
|
||||
onClose={() => { }}
|
||||
title={"Confirm Submission"}
|
||||
>
|
||||
<>
|
||||
<p className="text-xl mt-8 mb-12">Are you sure you want to proceed with the submission?</p>
|
||||
<div className="w-full flex justify-between">
|
||||
<Button color="purple" onClick={() => setShowSubmissionModal(false)} variant="outline" className="max-w-[200px] self-end w-full !text-xl">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="rose" onClick={() => { setShowSubmissionModal(false); setContinueAnyways(true)}} className="max-w-[200px] self-end w-full !text-xl">
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
<QuestionsModal isOpen={showQuestionsModal} {...questionModalKwargs} />
|
||||
{
|
||||
!(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) &&
|
||||
@@ -438,20 +438,27 @@ export default function Level({ exam, showSolutions = false, onFinish, editing =
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-ielts-level/20 p-1">
|
||||
{exam.parts.map((_, index) =>
|
||||
<Tab key={index} onClick={(e) => {
|
||||
if (!seenParts.includes(index)) {
|
||||
/*
|
||||
// If client wants to revert uncomment and remove the added if statement
|
||||
if (!seenParts.has(index)) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
*/
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
if (!seenParts.has(index)) {
|
||||
setShowPartDivider(true);
|
||||
setBgColor(levelBgColor);
|
||||
setSeenParts(prev => new Set(prev).add(index));
|
||||
}
|
||||
}}
|
||||
className={({ selected }) =>
|
||||
clsx(
|
||||
"w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-ielts-level/80",
|
||||
"ring-white ring-opacity-60 focus:outline-none",
|
||||
"transition duration-300 ease-in-out",
|
||||
"transition duration-300 ease-in-out hover:bg-white/70",
|
||||
selected && "bg-white shadow",
|
||||
seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed"
|
||||
// seenParts.includes(index) ? "hover:bg-white/70" : "cursor-not-allowed"
|
||||
)
|
||||
}
|
||||
>{`Part ${index + 1}`}</Tab>
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface LevelExam extends ExamBase {
|
||||
export interface LevelPart {
|
||||
context?: string;
|
||||
intro?: string;
|
||||
category?: string;
|
||||
exercises: Exercise[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user