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
This commit is contained in:
@@ -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: [
|
||||
|
||||
213
src/components/ExamEditor/ImportExam/Templates.tsx
Normal file
213
src/components/ExamEditor/ImportExam/Templates.tsx
Normal file
@@ -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<React.SetStateAction<{ isOpen: boolean, type: "exam" | "solutions" }>>;
|
||||
}
|
||||
|
||||
const Templates: React.FC<Props> = ({ 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 (
|
||||
<Transition
|
||||
show={state.isOpen}
|
||||
as={Fragment}
|
||||
beforeEnter={() => setIsClosing(false)}
|
||||
beforeLeave={() => setIsClosing(true)}
|
||||
afterLeave={() => {
|
||||
setIsClosing(false);
|
||||
setMounted(false);
|
||||
}}
|
||||
>
|
||||
<Dialog onClose={() => blockMultipleClicksClose()} className="relative z-50">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black/30" />
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogPanel className={`bg-ielts-${module}-light w-full max-w-6xl h-fit p-8 rounded-xl flex flex-col gap-4`}>
|
||||
<DialogTitle className="flex font-bold text-xl justify-center text-gray-700"><span>{`${capitalize(module)} ${state.type === "exam" ? 'Exam' : 'Solutions'} Import`}</span></DialogTitle>
|
||||
<div className="flex flex-col w-full mt-4 gap-6">
|
||||
{state.type === "exam" ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HiOutlineDocumentText className={`w-5 h-5 text-ielts-${module}`} />
|
||||
<h2 className="text-lg font-semibold">
|
||||
The {module} exam import accepts the following exercise types:
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="flex flex-col pl-10 gap-2">
|
||||
{moduleExercises[module].map((item, index) => (
|
||||
<li key={index} className="text-gray-700 list-disc">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<IoInformationCircleOutline className={`w-5 h-5 text-ielts-${module}`} />
|
||||
<h2 className="text-lg font-semibold">
|
||||
The uploaded document must:
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="flex flex-col pl-10 gap-2">
|
||||
<li className="text-gray-700 list-disc">
|
||||
be a Word .docx document.
|
||||
</li>
|
||||
<li className="text-gray-700 list-disc">
|
||||
have clear part and exercise delineation (e.g. Part 1, ... , Part X, Question 1 - 10, ... , Question y - x).
|
||||
</li>
|
||||
{["reading", "level"].includes(module) && (
|
||||
<li className="text-gray-700 list-disc">
|
||||
a part must only contain a reading passage and it must be between the part delineator (e.g. Part 1) and the part exercises.
|
||||
</li>
|
||||
)}
|
||||
<li className="text-gray-700 list-disc">
|
||||
if solutions are going to be uploaded the exercise numbers/id's must match the ones in the solutions.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
) :
|
||||
<>
|
||||
<div className="flex flex-col gap-3 bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<IoInformationCircleOutline className={`w-5 h-5 text-ielts-${module}`} />
|
||||
<h2 className="text-lg font-semibold">
|
||||
The uploaded document must:
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="flex flex-col pl-10 gap-2">
|
||||
<li className="text-gray-700 list-disc">
|
||||
be a Word .docx document.
|
||||
</li>
|
||||
<li className="text-gray-700 list-disc">
|
||||
match the exercise numbers/id's that are in the exam document.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-gray-600">
|
||||
{`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" : ""}.`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full flex justify-between mt-4 gap-8">
|
||||
<Button color="purple" onClick={() => blockMultipleClicksClose()} variant="outline" className="self-end w-full bg-white">
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button color="purple" onClick={handleTemplateDownload} variant="solid" className="self-end w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFileDownload size={24} />
|
||||
Download Template
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</TransitionChild>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
||||
export default Templates;
|
||||
@@ -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<File | null>(null);
|
||||
const [solutionsFile, setSolutionsFile] = useState<File | null>(null);
|
||||
const [templateState, setTemplateState] = useState<{ isOpen: boolean, type: "exam" | "solutions" }>({ isOpen: false, type: "exam" });
|
||||
|
||||
const handleExamChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
@@ -105,6 +108,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu
|
||||
|
||||
return (
|
||||
<>
|
||||
<Templates module={module} state={templateState} setState={setTemplateState} />
|
||||
{!showUploaders ? (
|
||||
<div
|
||||
onClick={() => setShowUploaders(true)}
|
||||
@@ -146,7 +150,7 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu
|
||||
<h3 className="font-semibold text-gray-700">Exam Document</h3>
|
||||
<p className="text-sm text-gray-500">Required</p>
|
||||
</div>
|
||||
{examFile && (
|
||||
{examFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCheckCircle className="w-6 h-6 text-green-500" />
|
||||
<button
|
||||
@@ -159,6 +163,16 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu
|
||||
<FaTimes className="w-4 h-4 text-green-600" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTemplateState({ isOpen: true, type: "exam" });
|
||||
}}
|
||||
className="p-1.5 hover:bg-gray-200 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<IoInformationCircleOutline size={28} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
@@ -205,9 +219,20 @@ const WordUploader: React.FC<{ module: Module, setNumberOfLevelParts: (parts: nu
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 font-medium px-2 py-1 bg-gray-100 rounded">
|
||||
OPTIONAL
|
||||
</span>
|
||||
<>
|
||||
<span className="text-xs text-gray-400 font-medium px-2 py-1 bg-gray-100 rounded">
|
||||
OPTIONAL
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setTemplateState({ isOpen: true, type: "solutions" });
|
||||
}}
|
||||
className="p-1.5 hover:bg-gray-200 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<IoInformationCircleOutline size={28} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<ExamProps<SpeakingExam>> = ({ exam, showSolutions = false, preview = false }) => {
|
||||
@@ -36,7 +37,7 @@ const Speaking: React.FC<ExamProps<SpeakingExam>> = ({ exam, showSolutions = fal
|
||||
const {
|
||||
nextExercise, previousExercise,
|
||||
showPartDivider, setShowPartDivider,
|
||||
setSeenParts,
|
||||
setSeenParts, seenParts, setIsBetweenParts
|
||||
} = useExamNavigation({ exam, module: "speaking", showSolutions, preview, disableBetweenParts: true });
|
||||
|
||||
useEffect(() => {
|
||||
@@ -108,19 +109,29 @@ const Speaking: React.FC<ExamProps<SpeakingExam>> = ({ exam, showSolutions = fal
|
||||
sectionIndex={exerciseIndex}
|
||||
onNext={handlePartDividerClick}
|
||||
/> : (
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle
|
||||
label={convertCamelCaseToReadable(exam.exercises[exerciseIndex].type)}
|
||||
minTimer={timer.current}
|
||||
exerciseIndex={memoizedExerciseIndex}
|
||||
<>
|
||||
{exam.exercises.length > 1 && <SectionNavbar
|
||||
module="speaking"
|
||||
totalExercises={countExercises(exam.exercises)}
|
||||
disableTimer={showSolutions || preview}
|
||||
sectionLabel="Part"
|
||||
seenParts={seenParts}
|
||||
setShowPartDivider={setShowPartDivider}
|
||||
setSeenParts={setSeenParts}
|
||||
preview={preview}
|
||||
/>
|
||||
{!showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
||||
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
||||
</div>
|
||||
/>}
|
||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||
<ModuleTitle
|
||||
label={convertCamelCaseToReadable(exam.exercises[exerciseIndex].type)}
|
||||
minTimer={timer.current}
|
||||
exerciseIndex={memoizedExerciseIndex}
|
||||
module="speaking"
|
||||
totalExercises={countExercises(exam.exercises)}
|
||||
disableTimer={showSolutions || preview}
|
||||
preview={preview}
|
||||
/>
|
||||
{!showPartDivider && !showSolutions && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons)}
|
||||
{showSolutions && renderSolution(currentExercise, progressButtons, progressButtons)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user