Merged in feature/ExamGenRework (pull request #115)
More modal patches and a bug in level that I'm still trying to solve Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {Fragment} from "react";
|
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,9 +12,53 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AbandonPopup({ isOpen, abandonPopupTitle, abandonPopupDescription, abandonConfirmButtonText, onAbandon, onCancel }: Props) {
|
export default function AbandonPopup({ isOpen, abandonPopupTitle, abandonPopupDescription, abandonConfirmButtonText, onAbandon, onCancel }: Props) {
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setMounted(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen && mounted) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setMounted(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, mounted]);
|
||||||
|
|
||||||
|
const blockMultipleClicksClose = useCallback((cancel: boolean) => {
|
||||||
|
if (isClosing) return;
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
const func = cancel ? onCancel : onAbandon;
|
||||||
|
func();
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClosing, onCancel, onAbandon]);
|
||||||
|
|
||||||
|
if (!mounted && !isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition show={isOpen} as={Fragment}>
|
<Transition
|
||||||
<Dialog onClose={onCancel} className="relative z-50">
|
show={isOpen}
|
||||||
|
as={Fragment}
|
||||||
|
beforeEnter={() => setIsClosing(false)}
|
||||||
|
beforeLeave={() => setIsClosing(true)}
|
||||||
|
afterLeave={() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
setMounted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog onClose={() => blockMultipleClicksClose(true)} className="relative z-50">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@@ -39,10 +83,10 @@ export default function AbandonPopup({isOpen, abandonPopupTitle, abandonPopupDes
|
|||||||
<Dialog.Title className="font-bold text-xl">{abandonPopupTitle}</Dialog.Title>
|
<Dialog.Title className="font-bold text-xl">{abandonPopupTitle}</Dialog.Title>
|
||||||
<span>{abandonPopupDescription}</span>
|
<span>{abandonPopupDescription}</span>
|
||||||
<div className="w-full flex justify-between mt-8">
|
<div className="w-full flex justify-between mt-8">
|
||||||
<Button color="purple" onClick={onCancel} variant="outline" className="max-w-[200px] self-end w-full">
|
<Button color="purple" onClick={() => blockMultipleClicksClose(true)} variant="outline" className="max-w-[200px] self-end w-full">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="purple" onClick={onAbandon} className="max-w-[200px] self-end w-full">
|
<Button color="purple" onClick={() => blockMultipleClicksClose(false)} className="max-w-[200px] self-end w-full">
|
||||||
{abandonConfirmButtonText}
|
{abandonConfirmButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ interface Props {
|
|||||||
letter: string;
|
letter: string;
|
||||||
word: string;
|
word: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isUsed: boolean;
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onEdit?: (newWord: string) => void;
|
onEdit?: (newWord: string) => void;
|
||||||
@@ -15,7 +14,6 @@ const FillBlanksWord: React.FC<Props> = ({
|
|||||||
letter,
|
letter,
|
||||||
word,
|
word,
|
||||||
isSelected,
|
isSelected,
|
||||||
isUsed,
|
|
||||||
onClick,
|
onClick,
|
||||||
onRemove,
|
onRemove,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -36,10 +34,8 @@ const FillBlanksWord: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={isUsed}
|
|
||||||
className={`
|
className={`
|
||||||
min-w-0 flex-1 flex items-center gap-2 p-2 rounded-md border text-left transition-colors
|
min-w-0 flex-1 flex items-center gap-2 p-2 rounded-md border text-left transition-colors
|
||||||
${isUsed ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-blue-50'}
|
|
||||||
${isSelected ? 'border-blue-500 bg-blue-100' : 'border-gray-200'}
|
${isSelected ? 'border-blue-500 bg-blue-100' : 'border-gray-200'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -195,11 +195,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWordUsed = (word: string): boolean => {
|
|
||||||
if (local.allowRepetition) return false;
|
|
||||||
return Array.from(answers.values()).includes(word);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditWord = (index: number, newWord: string) => {
|
const handleEditWord = (index: number, newWord: string) => {
|
||||||
if (!editing) setEditing(true);
|
if (!editing) setEditing(true);
|
||||||
|
|
||||||
@@ -299,7 +294,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
|||||||
letter={wordItem.letter}
|
letter={wordItem.letter}
|
||||||
word={wordItem.word}
|
word={wordItem.word}
|
||||||
isSelected={answers.get(selectedBlankId || '') === wordItem.word}
|
isSelected={answers.get(selectedBlankId || '') === wordItem.word}
|
||||||
isUsed={isWordUsed(wordItem.word)}
|
|
||||||
onClick={() => handleWordSelect(wordItem.word)}
|
onClick={() => handleWordSelect(wordItem.word)}
|
||||||
onRemove={isEditMode ? () => handleRemoveWord(index) : undefined}
|
onRemove={isEditMode ? () => handleRemoveWord(index) : undefined}
|
||||||
onEdit={isEditMode ? (newWord) => handleEditWord(index, newWord) : undefined}
|
onEdit={isEditMode ? (newWord) => handleEditWord(index, newWord) : undefined}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import WordUploader from './WordUploader';
|
|||||||
import GenLoader from '../Exercises/Shared/GenLoader';
|
import GenLoader from '../Exercises/Shared/GenLoader';
|
||||||
import useExamEditorStore from '@/stores/examEditor';
|
import useExamEditorStore from '@/stores/examEditor';
|
||||||
|
|
||||||
const ImportOrFromScratch: React.FC<{ module: Module; }> = ({ module }) => {
|
const ImportOrFromScratch: React.FC<{
|
||||||
|
module: Module;
|
||||||
|
setNumberOfLevelParts: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
}> = ({ module, setNumberOfLevelParts }) => {
|
||||||
const { currentModule, dispatch } = useExamEditorStore();
|
const { currentModule, dispatch } = useExamEditorStore();
|
||||||
const { importing } = useExamEditorStore((store) => store.modules[currentModule])
|
const { importing } = useExamEditorStore((store) => store.modules[currentModule])
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ const ImportOrFromScratch: React.FC<{ module: Module; }> = ({ module }) => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
<WordUploader module={module} />
|
<WordUploader module={module} setNumberOfLevelParts={setNumberOfLevelParts} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import useExamEditorStore from '@/stores/examEditor';
|
|||||||
import { LevelPart, ListeningPart, ReadingPart } from '@/interfaces/exam';
|
import { LevelPart, ListeningPart, ReadingPart } from '@/interfaces/exam';
|
||||||
import { defaultSectionSettings } from '@/stores/examEditor/defaults';
|
import { defaultSectionSettings } from '@/stores/examEditor/defaults';
|
||||||
|
|
||||||
const WordUploader: React.FC<{ module: Module }> = ({ module }) => {
|
const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: React.Dispatch<React.SetStateAction<number>> }> = ({ module, setNumberOfLevelParts }) => {
|
||||||
const { currentModule, dispatch } = useExamEditorStore();
|
const { currentModule, dispatch } = useExamEditorStore();
|
||||||
|
const {sectionLabels} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const examInputRef = useRef<HTMLInputElement>(null);
|
const examInputRef = useRef<HTMLInputElement>(null);
|
||||||
const solutionsInputRef = useRef<HTMLInputElement>(null);
|
const solutionsInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -74,6 +75,20 @@ const WordUploader: React.FC<{ module: Module }> = ({ module }) => {
|
|||||||
const newSectionsStates = data.parts.map(
|
const newSectionsStates = data.parts.map(
|
||||||
(part: ReadingPart | ListeningPart | LevelPart, index: number) => defaultSectionSettings(module, index + 1, part)
|
(part: ReadingPart | ListeningPart | LevelPart, index: number) => defaultSectionSettings(module, index + 1, part)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (module === "level") {
|
||||||
|
// default is 1
|
||||||
|
const newLabelCount = data.parts.length - 2;
|
||||||
|
setNumberOfLevelParts(newLabelCount);
|
||||||
|
|
||||||
|
const newLabels = Array.from({ length: newLabelCount }, (_, index) => ({
|
||||||
|
id: index + 2,
|
||||||
|
label: `Part ${index + 2}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch({type: "UPDATE_MODULE", payload: { updates: { sectionLabels: [...sectionLabels, ...newLabels] }}})
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_MODULE", payload: {
|
type: "UPDATE_MODULE", payload: {
|
||||||
updates: {
|
updates: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Input from "../Low/Input";
|
|||||||
import Select from "../Low/Select";
|
import Select from "../Low/Select";
|
||||||
import { capitalize } from "lodash";
|
import { capitalize } from "lodash";
|
||||||
import { Difficulty } from "@/interfaces/exam";
|
import { Difficulty } from "@/interfaces/exam";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { ModuleState, SectionState } from "@/stores/examEditor/types";
|
import { ModuleState, SectionState } from "@/stores/examEditor/types";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
@@ -33,7 +33,7 @@ const ExamEditor: React.FC = () => {
|
|||||||
importModule
|
importModule
|
||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const [numberOfParts, setNumberOfParts] = useState(1);
|
const [numberOfLevelParts, setNumberOfLevelParts] = useState(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentSections = sections;
|
const currentSections = sections;
|
||||||
@@ -41,23 +41,22 @@ const ExamEditor: React.FC = () => {
|
|||||||
let updatedSections: SectionState[];
|
let updatedSections: SectionState[];
|
||||||
let updatedLabels: any;
|
let updatedLabels: any;
|
||||||
|
|
||||||
if (numberOfParts > currentSections.length) {
|
if (numberOfLevelParts > currentSections.length) {
|
||||||
const newSections = [...currentSections];
|
const newSections = [...currentSections];
|
||||||
const newLabels = [...currentLabels];
|
const newLabels = [...currentLabels];
|
||||||
|
|
||||||
for (let i = currentSections.length; i < numberOfParts; i++) {
|
for (let i = currentSections.length; i < numberOfLevelParts; i++) {
|
||||||
newSections.push(defaultSectionSettings(currentModule, i + 1));
|
newSections.push(defaultSectionSettings(currentModule, i + 1));
|
||||||
newLabels.push({
|
newLabels.push({
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
label: `Part ${i + 1}`
|
label: `Part ${i + 1}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedSections = newSections;
|
updatedSections = newSections;
|
||||||
updatedLabels = newLabels;
|
updatedLabels = newLabels;
|
||||||
} else if (numberOfParts < currentSections.length) {
|
} else if (numberOfLevelParts < currentSections.length) {
|
||||||
updatedSections = currentSections.slice(0, numberOfParts);
|
updatedSections = currentSections.slice(0, numberOfLevelParts);
|
||||||
updatedLabels = currentLabels.slice(0, numberOfParts);
|
updatedLabels = currentLabels.slice(0, numberOfLevelParts);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,7 +76,7 @@ const ExamEditor: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [numberOfParts]);
|
}, [numberOfLevelParts]);
|
||||||
|
|
||||||
|
|
||||||
const sectionIds = sections.map((section) => section.sectionId)
|
const sectionIds = sections.map((section) => section.sectionId)
|
||||||
@@ -103,11 +102,11 @@ const ExamEditor: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Settings = ModuleSettings[currentModule];
|
const Settings = ModuleSettings[currentModule];
|
||||||
|
const showImport = importModule && ["reading", "listening", "level"].includes(currentModule);
|
||||||
|
|
||||||
const showImport = importModule && (currentModule === "reading" || currentModule === "listening" || currentModule === "level");
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showImport ? <ImportOrStartFromScratch module={currentModule} /> : (
|
{showImport ? <ImportOrStartFromScratch module={currentModule} setNumberOfLevelParts={setNumberOfLevelParts}/> : (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-4 w-full items-center">
|
<div className="flex gap-4 w-full items-center">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -153,7 +152,7 @@ const ExamEditor: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-3 w-1/3">
|
<div className="flex flex-col gap-3 w-1/3">
|
||||||
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
|
<label className="font-normal text-base text-mti-gray-dim">Number of Parts</label>
|
||||||
<Input type="number" name="Number of Parts" min={1} onChange={(v) => setNumberOfParts(parseInt(v))} value={numberOfParts} />
|
<Input type="number" name="Number of Parts" min={1} onChange={(v) => setNumberOfLevelParts(parseInt(v))} value={numberOfLevelParts} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-3 w-fit h-fit">
|
<div className="flex flex-col gap-3 w-fit h-fit">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Fragment, ReactElement} from "react";
|
import { Fragment, ReactElement, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -13,9 +13,54 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ isOpen, maxWidth, title, className, titleClassName, onClose, children }: Props) {
|
export default function Modal({ isOpen, maxWidth, title, className, titleClassName, onClose, children }: Props) {
|
||||||
|
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setMounted(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen && mounted) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setMounted(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, mounted]);
|
||||||
|
|
||||||
|
const blockMultipleClicksClose = useCallback(() => {
|
||||||
|
if (isClosing) return;
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClosing, onClose]);
|
||||||
|
|
||||||
|
if (!mounted && !isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition
|
||||||
<Dialog as="div" className="relative z-[200]" onClose={onClose}>
|
appear
|
||||||
|
show={isOpen}
|
||||||
|
as={Fragment}
|
||||||
|
beforeEnter={() => setIsClosing(false)}
|
||||||
|
beforeLeave={() => setIsClosing(true)}
|
||||||
|
afterLeave={() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
setMounted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog as="div" className="relative z-[200]" onClose={() => blockMultipleClicksClose()}>
|
||||||
<TransitionChild
|
<TransitionChild
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {Fragment} from "react";
|
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||||
import Button from "./Low/Button";
|
import Button from "./Low/Button";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,9 +8,52 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TimerEndedModal({ isOpen, onClose }: Props) {
|
export default function TimerEndedModal({ isOpen, onClose }: Props) {
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setMounted(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen && mounted) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setMounted(false);
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen, mounted]);
|
||||||
|
|
||||||
|
const blockMultipleClicksClose = useCallback(() => {
|
||||||
|
if (isClosing) return;
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [isClosing, onClose]);
|
||||||
|
|
||||||
|
if (!mounted && !isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition show={isOpen} as={Fragment}>
|
<Transition
|
||||||
<Dialog onClose={onClose} className="relative z-50">
|
show={isOpen}
|
||||||
|
as={Fragment}
|
||||||
|
beforeEnter={() => setIsClosing(false)}
|
||||||
|
beforeLeave={() => setIsClosing(true)}
|
||||||
|
afterLeave={() => {
|
||||||
|
setIsClosing(false);
|
||||||
|
setMounted(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog onClose={()=> blockMultipleClicksClose()} className="relative z-50">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@@ -37,7 +80,7 @@ export default function TimerEndedModal({isOpen, onClose}: Props) {
|
|||||||
The timer has ended! Your answers have been registered and saved, you will now move on to the next module (or to the
|
The timer has ended! Your answers have been registered and saved, you will now move on to the next module (or to the
|
||||||
finish screen, if this was the last one).
|
finish screen, if this was the last one).
|
||||||
</span>
|
</span>
|
||||||
<Button color="purple" onClick={onClose} className="max-w-[200px] self-end w-full mt-8">
|
<Button color="purple" onClick={()=> blockMultipleClicksClose()} className="max-w-[200px] self-end w-full mt-8">
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
Reference in New Issue
Block a user