diff --git a/src/components/AbandonPopup.tsx b/src/components/AbandonPopup.tsx index 50eec5bf..24ea6024 100644 --- a/src/components/AbandonPopup.tsx +++ b/src/components/AbandonPopup.tsx @@ -1,5 +1,5 @@ -import {Dialog, Transition} from "@headlessui/react"; -import {Fragment} from "react"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useCallback, useEffect, useState } from "react"; import Button from "./Low/Button"; interface Props { @@ -11,10 +11,54 @@ interface Props { onCancel: () => void; } -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 ( - - + setIsClosing(false)} + beforeLeave={() => setIsClosing(true)} + afterLeave={() => { + setIsClosing(false); + setMounted(false); + }} + > + blockMultipleClicksClose(true)} className="relative z-50"> {abandonPopupTitle} {abandonPopupDescription}
- -
diff --git a/src/components/ExamEditor/Exercises/Blanks/Letters/FillBlanksWord.tsx b/src/components/ExamEditor/Exercises/Blanks/Letters/FillBlanksWord.tsx index 0919cf50..8349bdc5 100644 --- a/src/components/ExamEditor/Exercises/Blanks/Letters/FillBlanksWord.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/Letters/FillBlanksWord.tsx @@ -4,7 +4,6 @@ interface Props { letter: string; word: string; isSelected: boolean; - isUsed: boolean; onClick: () => void; onRemove?: () => void; onEdit?: (newWord: string) => void; @@ -15,7 +14,6 @@ const FillBlanksWord: React.FC = ({ letter, word, isSelected, - isUsed, onClick, onRemove, onEdit, @@ -36,10 +34,8 @@ const FillBlanksWord: React.FC = ({ ) : (
- +
)} diff --git a/src/components/ExamEditor/ImportExam/WordUploader.tsx b/src/components/ExamEditor/ImportExam/WordUploader.tsx index 2a314599..cd7da5a7 100644 --- a/src/components/ExamEditor/ImportExam/WordUploader.tsx +++ b/src/components/ExamEditor/ImportExam/WordUploader.tsx @@ -9,8 +9,9 @@ import useExamEditorStore from '@/stores/examEditor'; import { LevelPart, ListeningPart, ReadingPart } from '@/interfaces/exam'; import { defaultSectionSettings } from '@/stores/examEditor/defaults'; -const WordUploader: React.FC<{ module: Module }> = ({ module }) => { +const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: React.Dispatch> }> = ({ module, setNumberOfLevelParts }) => { const { currentModule, dispatch } = useExamEditorStore(); + const {sectionLabels} = useExamEditorStore(state => state.modules[currentModule]); const examInputRef = useRef(null); const solutionsInputRef = useRef(null); @@ -74,6 +75,20 @@ const WordUploader: React.FC<{ module: Module }> = ({ module }) => { const newSectionsStates = data.parts.map( (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({ type: "UPDATE_MODULE", payload: { updates: { diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index 16616ec0..36acd08c 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -5,7 +5,7 @@ import Input from "../Low/Input"; import Select from "../Low/Select"; import { capitalize } from "lodash"; import { Difficulty } from "@/interfaces/exam"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "react-toastify"; import { ModuleState, SectionState } from "@/stores/examEditor/types"; import { Module } from "@/interfaces"; @@ -33,7 +33,7 @@ const ExamEditor: React.FC = () => { importModule } = useExamEditorStore(state => state.modules[currentModule]); - const [numberOfParts, setNumberOfParts] = useState(1); + const [numberOfLevelParts, setNumberOfLevelParts] = useState(1); useEffect(() => { const currentSections = sections; @@ -41,23 +41,22 @@ const ExamEditor: React.FC = () => { let updatedSections: SectionState[]; let updatedLabels: any; - if (numberOfParts > currentSections.length) { + if (numberOfLevelParts > currentSections.length) { const newSections = [...currentSections]; 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)); newLabels.push({ id: i + 1, label: `Part ${i + 1}` }); } - updatedSections = newSections; updatedLabels = newLabels; - } else if (numberOfParts < currentSections.length) { - updatedSections = currentSections.slice(0, numberOfParts); - updatedLabels = currentLabels.slice(0, numberOfParts); + } else if (numberOfLevelParts < currentSections.length) { + updatedSections = currentSections.slice(0, numberOfLevelParts); + updatedLabels = currentLabels.slice(0, numberOfLevelParts); } else { return; } @@ -77,7 +76,7 @@ const ExamEditor: React.FC = () => { } }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numberOfParts]); + }, [numberOfLevelParts]); const sectionIds = sections.map((section) => section.sectionId) @@ -103,11 +102,11 @@ const ExamEditor: React.FC = () => { }; const Settings = ModuleSettings[currentModule]; - - const showImport = importModule && (currentModule === "reading" || currentModule === "listening" || currentModule === "level"); + const showImport = importModule && ["reading", "listening", "level"].includes(currentModule); + return ( <> - {showImport ? : ( + {showImport ? : ( <>
@@ -153,7 +152,7 @@ const ExamEditor: React.FC = () => { ) : (
- setNumberOfParts(parseInt(v))} value={numberOfParts} /> + setNumberOfLevelParts(parseInt(v))} value={numberOfLevelParts} />
)}
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 5d2aa7a3..8e87f222 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -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 {Fragment, ReactElement} from "react"; +import { Fragment, ReactElement, useCallback, useEffect, useState } from "react"; interface Props { isOpen: boolean; @@ -12,10 +12,55 @@ interface Props { children?: ReactElement; } -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 ( - - + setIsClosing(false)} + beforeLeave={() => setIsClosing(true)} + afterLeave={() => { + setIsClosing(false); + setMounted(false); + }} + > + blockMultipleClicksClose()}> void; } -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 ( - - + setIsClosing(false)} + beforeLeave={() => setIsClosing(true)} + afterLeave={() => { + setIsClosing(false); + setMounted(false); + }} + > + blockMultipleClicksClose()} className="relative z-50"> -