Files
encoach_frontend/src/components/ExamEditor/ImportExam/WordUploader.tsx

302 lines
15 KiB
TypeScript

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, 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();
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 [templateState, setTemplateState] = useState<{ isOpen: boolean, type: "exam" | "solutions" }>({ isOpen: false, type: "exam" });
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);
const newSectionsStates = data.parts.map(
(part: ReadingPart | ListeningPart | LevelPart, index: number) => defaultSectionSettings(module, index + 1, part)
);
if (module === "level") {
setNumberOfLevelParts(data.parts.length);
}
dispatch({
type: "UPDATE_MODULE", payload: {
updates: {
sections: newSectionsStates,
minTimer: data.minTimer,
importModule: false,
importing: false,
},
module
}
});
} 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 (
<>
<Templates module={module} state={templateState} setState={setTemplateState} />
{!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>
) : (
<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 && (
<div className="mt-2 text-sm text-green-600 font-medium">
{examFile.name}
</div>
)}
<input
type="file"
ref={examInputRef}
onChange={handleExamChange}
accept=".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>
<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 && (
<div className="mt-2 text-sm text-green-600 font-medium">
{solutionsFile.name}
</div>
)}
<input
type="file"
ref={solutionsInputRef}
onChange={handleSolutionsChange}
accept=".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;