From 3b6fd2bc6b561ada9b63cc6f891645126e5c4c59 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Wed, 4 Dec 2024 04:15:31 +0000 Subject: [PATCH] ENCOA-224, ENCOA-256: Added the import templates, Speaking didn't have the navbar yet, added multiple choice to reading since they've placed that in the import --- .../ExamEditor/ExercisePicker/exercises.ts | 29 ++- .../ExamEditor/ImportExam/Templates.tsx | 213 ++++++++++++++++++ .../ExamEditor/ImportExam/WordUploader.tsx | 39 +++- src/exams/Speaking.tsx | 37 +-- 4 files changed, 292 insertions(+), 26 deletions(-) create mode 100644 src/components/ExamEditor/ImportExam/Templates.tsx diff --git a/src/components/ExamEditor/ExercisePicker/exercises.ts b/src/components/ExamEditor/ExercisePicker/exercises.ts index 5c34935d..3613f519 100644 --- a/src/components/ExamEditor/ExercisePicker/exercises.ts +++ b/src/components/ExamEditor/ExercisePicker/exercises.ts @@ -20,6 +20,8 @@ import { FaQuestionCircle, } from 'react-icons/fa'; import { ExerciseGen } from './generatedExercises'; +import { MdRadioButtonChecked } from 'react-icons/md'; +import { BsListCheck } from 'react-icons/bs'; const quantity = (quantity: number, tooltip?: string) => { return { @@ -39,6 +41,21 @@ const generate = () => { const reading = (passage: number) => { const readingExercises = [ + { + label: `Passage ${passage} - Multiple Choice`, + type: `reading_${passage}`, + icon: BsListCheck, + sectionId: passage, + extra: [ + { + param: "name", + value: "multipleChoice" + }, + quantity(5, "Quantity of Multiple Choice Questions"), + generate() + ], + module: "reading" + }, { label: `Passage ${passage} - Fill Blanks`, type: `reading_${passage}`, @@ -110,7 +127,7 @@ const reading = (passage: number) => { generate() ], module: "reading" - } + }, ]; if (passage === 3) { @@ -153,7 +170,7 @@ const listening = (section: number) => { module: "listening" }, { - label: `Section ${section} - Write the Blanks: Questions`, + label: `Section ${section} - Write Blanks: Questions`, type: `listening_${section}`, icon: FaQuestionCircle, sectionId: section, @@ -187,7 +204,7 @@ const listening = (section: number) => { if (section === 1 || section === 4) { listeningExercises.push( { - label: `Section ${section} - Write the Blanks: Fill`, + label: `Section ${section} - Write Blanks: Fill`, type: `listening_${section}`, icon: FaEdit, sectionId: section, @@ -204,7 +221,7 @@ const listening = (section: number) => { ); listeningExercises.push( { - label: `Section ${section} - Write the Blanks: Form`, + label: `Section ${section} - Write Blanks: Form`, type: `listening_${section}`, sectionId: section, icon: FaWpforms, @@ -239,7 +256,7 @@ const EXERCISES: ExerciseGen[] = [ module: "level" },*/ { - label: "Multiple Choice - Blank Space", + label: "Multiple Choice: Blank Space", type: "mcBlank", icon: FaEdit, extra: [ @@ -253,7 +270,7 @@ const EXERCISES: ExerciseGen[] = [ module: "level" }, { - label: "Multiple Choice - Underlined", + label: "Multiple Choice: Underlined", type: "mcUnderline", icon: FaUnderline, extra: [ diff --git a/src/components/ExamEditor/ImportExam/Templates.tsx b/src/components/ExamEditor/ImportExam/Templates.tsx new file mode 100644 index 00000000..d836262a --- /dev/null +++ b/src/components/ExamEditor/ImportExam/Templates.tsx @@ -0,0 +1,213 @@ +import Button from "@/components/Low/Button"; +import { Module } from "@/interfaces"; +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from "@headlessui/react"; +import { capitalize } from "lodash"; +import React, { Fragment, useCallback, useEffect, useState } from "react"; +import { FaFileDownload } from "react-icons/fa"; +import { HiOutlineDocumentText } from "react-icons/hi"; +import { IoInformationCircleOutline } from "react-icons/io5"; + +interface Props { + module: Module; + state: { isOpen: boolean, type: "exam" | "solutions" }; + setState: React.Dispatch>; +} + +const Templates: React.FC = ({ module, state, setState }) => { + const [isClosing, setIsClosing] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + if (state.isOpen) { + setMounted(true); + } + }, [state]); + + useEffect(() => { + if (!state.isOpen && mounted) { + const timer = setTimeout(() => { + setMounted(false); + setIsClosing(false); + }, 300); + return () => clearTimeout(timer); + } + }, [state, mounted]); + + const blockMultipleClicksClose = useCallback(() => { + if (isClosing) return; + setIsClosing(true); + setState({ isOpen: false, type: state.type }); + + const timer = setTimeout(() => { + setIsClosing(false); + }, 300); + + return () => clearTimeout(timer); + }, [isClosing, setState, state]); + + if (!mounted && !state.isOpen) return null; + + const moduleExercises = { + "reading": [ + "Multiple Choice", + "Write Blanks", + "True False", + "Paragraph Match", + "Idea Match" + ], + "listening": [ + "Multiple Choice", + "True False", + "Write Blanks: Questions", + "Write Blanks: Fill", + "Write Blanks: Form", + ], + "level": [ + "Fill Blanks: Multiple Choice", + "Multiple Choice: Blank Space", + "Multiple Choice: Underline", + "Multiple Choice: Reading Passage" + ], + "writing": [], + "speaking": [], + } + + const handleTemplateDownload = () => { + const fileName = `${capitalize(module)}${state.type === "exam" ? "Exam" : "Solutions"}Template`; + const url = `https://firebasestorage.googleapis.com/v0/b/encoach-staging.appspot.com/o/import_templates%2F${fileName}.docx?alt=media&token=b771a535-bf95-4060-889c-a086df65d480`; + + const link = document.createElement('a'); + link.href = url; + + link.download = `${fileName}.docx`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + return ( + setIsClosing(false)} + beforeLeave={() => setIsClosing(true)} + afterLeave={() => { + setIsClosing(false); + setMounted(false); + }} + > + blockMultipleClicksClose()} className="relative z-50"> + +
+ + + +
+ + {`${capitalize(module)} ${state.type === "exam" ? 'Exam' : 'Solutions'} Import`} +
+ {state.type === "exam" ? ( + <> +
+
+ +

+ The {module} exam import accepts the following exercise types: +

+
+
    + {moduleExercises[module].map((item, index) => ( +
  • + {item} +
  • + ))} +
+
+ +
+
+ +

+ The uploaded document must: +

+
+
    +
  • + be a Word .docx document. +
  • +
  • + have clear part and exercise delineation (e.g. Part 1, ... , Part X, Question 1 - 10, ... , Question y - x). +
  • + {["reading", "level"].includes(module) && ( +
  • + a part must only contain a reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises. +
  • + )} +
  • + if solutions are going to be uploaded the exercise numbers/id's must match the ones in the solutions. +
  • +
+
+ + ) : + <> +
+
+ +

+ The uploaded document must: +

+
+
    +
  • + be a Word .docx document. +
  • +
  • + match the exercise numbers/id's that are in the exam document. +
  • +
+
+ + } +
+

+ {`The downloadable template is an example of a file that can be imported. Your document doesn't need to be a carbon copy of the template - it can have different styling and formatting but it must adhere to the previous requirements${state.type === "exam" ? "and exercises of the same type should have the same formatting" : ""}.`} +

+
+
+ + + +
+
+
+
+
+
+
+ ); +} + +export default Templates; diff --git a/src/components/ExamEditor/ImportExam/WordUploader.tsx b/src/components/ExamEditor/ImportExam/WordUploader.tsx index d1465a35..22d53b68 100644 --- a/src/components/ExamEditor/ImportExam/WordUploader.tsx +++ b/src/components/ExamEditor/ImportExam/WordUploader.tsx @@ -8,6 +8,8 @@ import { toast } from 'react-toastify'; import useExamEditorStore from '@/stores/examEditor'; import { LevelPart, ListeningPart, ReadingPart } from '@/interfaces/exam'; import { defaultSectionSettings } from '@/stores/examEditor/defaults'; +import Templates from './Templates'; +import { IoInformationCircleOutline } from 'react-icons/io5'; const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: number) => void; }> = ({ module, setNumberOfLevelParts }) => { const { currentModule, dispatch } = useExamEditorStore(); @@ -17,6 +19,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu const [showUploaders, setShowUploaders] = useState(false); const [examFile, setExamFile] = useState(null); const [solutionsFile, setSolutionsFile] = useState(null); + const [templateState, setTemplateState] = useState<{ isOpen: boolean, type: "exam" | "solutions" }>({ isOpen: false, type: "exam" }); const handleExamChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -78,7 +81,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu if (module === "level") { setNumberOfLevelParts(data.parts.length); } - + dispatch({ type: "UPDATE_MODULE", payload: { updates: { @@ -105,6 +108,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu return ( <> + {!showUploaders ? (
setShowUploaders(true)} @@ -146,7 +150,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu

Exam Document

Required

- {examFile && ( + {examFile ? (
+ ) : ( + )} {examFile && ( @@ -170,7 +184,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu type="file" ref={examInputRef} onChange={handleExamChange} - accept=".doc,.docx" + accept=".docx" className="hidden" /> @@ -205,9 +219,20 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu ) : ( - - OPTIONAL - + <> + + OPTIONAL + + + )} {solutionsFile && ( @@ -219,7 +244,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu type="file" ref={solutionsInputRef} onChange={handleSolutionsChange} - accept=".doc,.docx" + accept=".docx" className="hidden" /> diff --git a/src/exams/Speaking.tsx b/src/exams/Speaking.tsx index bcd2be29..4205ee87 100644 --- a/src/exams/Speaking.tsx +++ b/src/exams/Speaking.tsx @@ -12,6 +12,7 @@ import useExamTimer from "@/hooks/useExamTimer"; import useExamNavigation from "./Navigation/useExamNavigation"; import ProgressButtons from "./components/ProgressButtons"; import { calculateExerciseIndexSpeaking } from "./utils/calculateExerciseIndex"; +import SectionNavbar from "./Navigation/SectionNavbar"; const Speaking: React.FC> = ({ exam, showSolutions = false, preview = false }) => { @@ -36,7 +37,7 @@ const Speaking: React.FC> = ({ exam, showSolutions = fal const { nextExercise, previousExercise, showPartDivider, setShowPartDivider, - setSeenParts, + setSeenParts, seenParts, setIsBetweenParts } = useExamNavigation({ exam, module: "speaking", showSolutions, preview, disableBetweenParts: true }); useEffect(() => { @@ -90,7 +91,7 @@ const Speaking: React.FC> = ({ exam, showSolutions = fal setSeenParts((prev) => new Set(prev).add(exerciseIndex)); } - const memoizedExerciseIndex = useMemo(() => + const memoizedExerciseIndex = useMemo(() => calculateExerciseIndexSpeaking(exam, exerciseIndex, questionIndex) // eslint-disable-next-line react-hooks/exhaustive-deps , [exerciseIndex, questionIndex] @@ -108,19 +109,29 @@ const Speaking: React.FC> = ({ exam, showSolutions = fal sectionIndex={exerciseIndex} onNext={handlePartDividerClick} /> : ( -
- + {exam.exercises.length > 1 && - {!showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)} - {showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)} -
+ />} +
+ + {!showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)} + {showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)} +
+ )} );