ENCOA-228 Now when user navigates between modules the generation items persist. Reading, listening and writing added to level module
This commit is contained in:
297
src/components/ExamEditor/ImportExam/WordUploader.tsx
Normal file
297
src/components/ExamEditor/ImportExam/WordUploader.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import { FaFileUpload, FaCheckCircle, FaLock, FaTimes } from 'react-icons/fa';
|
||||
import { capitalize } from 'lodash';
|
||||
import { Module } from '@/interfaces';
|
||||
import { toast } from 'react-toastify';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
import { LevelPart, ReadingPart } from '@/interfaces/exam';
|
||||
import { defaultSectionSettings } from '@/stores/examEditor/defaults';
|
||||
|
||||
const WordUploader: React.FC<{ module: Module }> = ({ module }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
|
||||
const examInputRef = useRef<HTMLInputElement>(null);
|
||||
const solutionsInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showUploaders, setShowUploaders] = useState(false);
|
||||
const [examFile, setExamFile] = useState<File | null>(null);
|
||||
const [solutionsFile, setSolutionsFile] = useState<File | null>(null);
|
||||
|
||||
const handleExamChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.type === 'application/msword' ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
setExamFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSolutionsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.type === 'application/msword' ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
setSolutionsFile(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = useCallback(async () => {
|
||||
try {
|
||||
if (!examFile) {
|
||||
toast.error('Exam file is required');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: "UPDATE_MODULE", payload: { updates: { importing: true }, module } })
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('exercises', examFile);
|
||||
if (solutionsFile) {
|
||||
formData.append('solutions', solutionsFile);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/exam/${module}/import/`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
toast.error(`An unknown error has occured while import ${module} exam!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
toast.success(`${capitalize(module)} exam imported successfully!`);
|
||||
|
||||
setExamFile(null);
|
||||
setSolutionsFile(null);
|
||||
setShowUploaders(false);
|
||||
|
||||
switch (currentModule) {
|
||||
case 'reading': {
|
||||
const newSectionsStates = data.parts.map(
|
||||
(part: ReadingPart, index: number) => defaultSectionSettings(module, index + 1, part)
|
||||
);
|
||||
dispatch({
|
||||
type: "UPDATE_MODULE", payload: {
|
||||
updates: {
|
||||
sections: newSectionsStates,
|
||||
minTimer: data.minTimer,
|
||||
importModule: false,
|
||||
importing: false,
|
||||
},
|
||||
module
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'level': {
|
||||
const newSectionsStates = data.parts.map(
|
||||
(part: LevelPart, index: number) => defaultSectionSettings(module, index + 1, part)
|
||||
);
|
||||
dispatch({
|
||||
type: "UPDATE_MODULE", payload: {
|
||||
updates: {
|
||||
sections: newSectionsStates,
|
||||
minTimer: data.minTimer,
|
||||
importModule: false,
|
||||
importing: false,
|
||||
sectionLabels: Array.from({ length: newSectionsStates.length }, (_, index) => ({
|
||||
id: index + 1,
|
||||
label: `Part ${index + 1}`
|
||||
}))
|
||||
},
|
||||
module
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(`Make sure you've imported a valid word document (.docx)!`);
|
||||
} finally {
|
||||
dispatch({ type: "UPDATE_MODULE", payload: { updates: { importing: false }, module } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
examFile,
|
||||
solutionsFile,
|
||||
dispatch,
|
||||
currentModule
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!showUploaders ? (
|
||||
<div
|
||||
onClick={() => setShowUploaders(true)}
|
||||
className="flex flex-col items-center gap-6 h-full justify-center p-8 border-2 border-blue-200 rounded-xl
|
||||
bg-gradient-to-b from-blue-50 to-blue-100
|
||||
hover:from-blue-100 hover:to-blue-200
|
||||
cursor-pointer transition-all duration-300
|
||||
shadow-sm hover:shadow-md group"
|
||||
>
|
||||
<div className="transform group-hover:scale-105 transition-transform duration-300">
|
||||
<Image
|
||||
src="/microsoft-word-icon.png"
|
||||
width={200}
|
||||
height={100}
|
||||
alt="Upload Word"
|
||||
className="drop-shadow-md"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-stone-600 group-hover:text-stone-800 transition-colors duration-300">
|
||||
Upload {capitalize(module)} Exam
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full gap-4 p-6 justify-between border-2 border-blue-200 rounded-xl bg-white shadow-md">
|
||||
<div className='flex flex-col flex-1 justify-center gap-8'>
|
||||
<div
|
||||
onClick={() => examInputRef.current?.click()}
|
||||
className={clsx(
|
||||
"relative p-6 border-2 border-dashed rounded-lg cursor-pointer transition-all duration-300",
|
||||
examFile ? "border-green-300 bg-green-50" : "border-gray-300 hover:border-blue-400 hover:bg-blue-50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FaFileUpload className={clsx(
|
||||
"w-8 h-8",
|
||||
examFile ? "text-green-500" : "text-gray-400"
|
||||
)} />
|
||||
<div className="flex-grow">
|
||||
<h3 className="font-semibold text-gray-700">Exam Document</h3>
|
||||
<p className="text-sm text-gray-500">Required</p>
|
||||
</div>
|
||||
{examFile && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCheckCircle className="w-6 h-6 text-green-500" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExamFile(null);
|
||||
}}
|
||||
className="p-1.5 hover:bg-green-100 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<FaTimes className="w-4 h-4 text-green-600" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{examFile && (
|
||||
<div className="mt-2 text-sm text-green-600 font-medium">
|
||||
{examFile.name}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
ref={examInputRef}
|
||||
onChange={handleExamChange}
|
||||
accept=".doc,.docx"
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => solutionsInputRef.current?.click()}
|
||||
className={clsx(
|
||||
"relative p-6 border-2 border-dashed rounded-lg cursor-pointer transition-all duration-300",
|
||||
solutionsFile ? "border-green-300 bg-green-50" : "border-gray-300 hover:border-blue-400 hover:bg-blue-50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FaFileUpload className={clsx(
|
||||
"w-8 h-8",
|
||||
solutionsFile ? "text-green-500" : "text-gray-400"
|
||||
)} />
|
||||
<div className="flex-grow">
|
||||
<h3 className="font-semibold text-gray-700">Solutions Document</h3>
|
||||
<p className="text-sm text-gray-500">Optional</p>
|
||||
</div>
|
||||
{solutionsFile ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<FaCheckCircle className="w-6 h-6 text-green-500" />
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSolutionsFile(null);
|
||||
}}
|
||||
className="p-1.5 hover:bg-green-100 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<FaTimes className="w-4 h-4 text-green-600" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 font-medium px-2 py-1 bg-gray-100 rounded">
|
||||
OPTIONAL
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{solutionsFile && (
|
||||
<div className="mt-2 text-sm text-green-600 font-medium">
|
||||
{solutionsFile.name}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
ref={solutionsInputRef}
|
||||
onChange={handleSolutionsChange}
|
||||
accept=".doc,.docx"
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => setShowUploaders(false)}
|
||||
className={
|
||||
clsx("px-6 py-2.5 text-sm font-semibold text-gray-700 bg-white border-2 border-gray-200",
|
||||
"rounded-lg hover:bg-gray-50 hover:border-gray-300",
|
||||
"transition-all duration-300 min-w-[120px]",
|
||||
"focus:outline-none focus:ring-2 focus:ring-gray-200 focus:ring-offset-2",
|
||||
"active:scale-95")}
|
||||
>
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<FaTimes className="w-4 h-4" />
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={!examFile}
|
||||
className={clsx(
|
||||
"flex-grow px-6 py-2.5 text-sm font-semibold rounded-lg",
|
||||
"transition-all duration-300 min-w-[120px]",
|
||||
"focus:outline-none focus:ring-2 focus:ring-offset-2",
|
||||
"flex items-center justify-center gap-2",
|
||||
examFile
|
||||
? "bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 active:scale-95 focus:ring-blue-500"
|
||||
: "bg-gradient-to-r from-gray-100 to-gray-200 text-gray-400 cursor-not-allowed border-2 border-gray-200"
|
||||
)}
|
||||
>
|
||||
{examFile ? (
|
||||
<>
|
||||
<FaFileUpload className="w-4 h-4" />
|
||||
Import Files
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaLock className="w-4 h-4" />
|
||||
Upload Exam First
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WordUploader;
|
||||
Reference in New Issue
Block a user