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:
@@ -1,13 +1,26 @@
|
||||
interface Props {
|
||||
label: string;
|
||||
preview?: React.ReactNode;
|
||||
type: string;
|
||||
firstId: string;
|
||||
lastId: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
const ExerciseLabel: React.FC<Props> = ({label, preview}) => {
|
||||
const previewLabel = (text: string) => {
|
||||
return <>
|
||||
"{text !== undefined ? text.replaceAll('\\n', ' ').split(' ').slice(0, 15).join(' ') : ""}..."
|
||||
</>
|
||||
}
|
||||
|
||||
const label = (type: string, firstId: string, lastId: string) => {
|
||||
return `${type} #${firstId} ${firstId === lastId ? '' : `- #${lastId}`}`;
|
||||
}
|
||||
|
||||
|
||||
const ExerciseLabel: React.FC<Props> = ({type, firstId, lastId, prompt}) => {
|
||||
return (
|
||||
<div className="flex w-full justify-between items-center mr-4">
|
||||
<span className="font-semibold">{label}</span>
|
||||
{preview && <div className="text-sm font-light italic">{preview}</div>}
|
||||
<span className="font-semibold">{label(type, firstId, lastId)}</span>
|
||||
<div className="text-sm font-light italic">{previewLabel(prompt)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { ExerciseGen } from './generatedExercises';
|
||||
import Image from 'next/image';
|
||||
import clsx from 'clsx';
|
||||
import { GiBrain } from 'react-icons/gi';
|
||||
import { IoTextOutline } from 'react-icons/io5';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
|
||||
interface Props {
|
||||
sectionId: number;
|
||||
exercises: ExerciseGen[];
|
||||
extraArgs?: Record<string, any>;
|
||||
onSubmit: (configurations: ExerciseConfig[]) => void;
|
||||
onDiscard: () => void;
|
||||
}
|
||||
|
||||
export interface ExerciseConfig {
|
||||
type: string;
|
||||
params: {
|
||||
[key: string]: string | number | boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const ExerciseWizard: React.FC<Props> = ({
|
||||
exercises,
|
||||
extraArgs,
|
||||
sectionId,
|
||||
onSubmit,
|
||||
onDiscard,
|
||||
}) => {
|
||||
const {currentModule} = useExamEditorStore();
|
||||
const { selectedExercises } = useExamEditorStore((store) => store.modules[currentModule].sections.find((s) => s.sectionId === sectionId))!;
|
||||
|
||||
const [configurations, setConfigurations] = useState<ExerciseConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const initialConfigs = selectedExercises.map(exerciseType => {
|
||||
const exercise = exercises.find(ex => {
|
||||
const fullType = ex.extra?.find(e => e.param === 'name')?.value
|
||||
? `${ex.type}/?name=${ex.extra.find(e => e.param === 'name')?.value}`
|
||||
: ex.type;
|
||||
return fullType === exerciseType;
|
||||
});
|
||||
|
||||
const params: { [key: string]: string | number | boolean } = {};
|
||||
exercise?.extra?.forEach(param => {
|
||||
if (param.param !== 'name') {
|
||||
if (exerciseType.includes('paragraphMatch') && param.param === 'quantity') {
|
||||
params[param.param] = extraArgs?.text.split("\n\n").length || 1;
|
||||
} else {
|
||||
params[param.param || ''] = param.value ?? '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
type: exerciseType,
|
||||
params
|
||||
};
|
||||
});
|
||||
|
||||
setConfigurations(initialConfigs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedExercises, exercises]);
|
||||
|
||||
const handleParameterChange = (
|
||||
exerciseIndex: number,
|
||||
paramName: string,
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
setConfigurations(prev => {
|
||||
const newConfigs = [...prev];
|
||||
newConfigs[exerciseIndex] = {
|
||||
...newConfigs[exerciseIndex],
|
||||
params: {
|
||||
...newConfigs[exerciseIndex].params,
|
||||
[paramName]: value
|
||||
}
|
||||
};
|
||||
return newConfigs;
|
||||
});
|
||||
};
|
||||
|
||||
const renderParameterInput = (
|
||||
param: NonNullable<ExerciseGen['extra']>[0],
|
||||
exerciseIndex: number,
|
||||
config: ExerciseConfig
|
||||
) => {
|
||||
if (typeof param.value === 'boolean') {
|
||||
const currentValue = Boolean(config.params[param.param || '']);
|
||||
return (
|
||||
<div className="flex flex-row items-center ml-auto">
|
||||
<GiBrain
|
||||
className="mx-4"
|
||||
size={28}
|
||||
color={currentValue ? `#F3F4F6` : `#1F2937`}
|
||||
/>
|
||||
<Switch
|
||||
checked={currentValue}
|
||||
onChange={(value) => handleParameterChange(
|
||||
exerciseIndex,
|
||||
param.param || '',
|
||||
value
|
||||
)}
|
||||
className={clsx(
|
||||
"relative inline-flex h-[30px] w-[58px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75",
|
||||
currentValue ? `bg-[#F3F4F6]` : `bg-[#1F2937]`
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={clsx(
|
||||
"pointer-events-none inline-block h-[26px] w-[26px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out",
|
||||
currentValue ? 'translate-x-7' : 'translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<IoTextOutline
|
||||
className="mx-4"
|
||||
size={28}
|
||||
color={!currentValue ? `#F3F4F6` : `#1F2937`}
|
||||
/>
|
||||
|
||||
<Tooltip id={`${exerciseIndex}`} className="z-50 bg-white shadow-md rounded-sm" />
|
||||
<a data-tooltip-id={`${exerciseIndex}`} data-tooltip-html="Generate or use placeholder?" className='ml-1 flex items-center justify-center'>
|
||||
<Image src="/mat-icon-info.svg" width={24} height={24} alt={"AI Generated?"} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ('type' in param && param.type === 'text') {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{param.label}
|
||||
</label>
|
||||
{param.tooltip && (
|
||||
<>
|
||||
<Tooltip id={config.type} className="z-50 bg-white shadow-md rounded-sm" />
|
||||
<a data-tooltip-id={config.type} data-tooltip-html={param.tooltip} className='ml-1 flex items-center justify-center'>
|
||||
<Image src="/mat-icon-info.svg" width={24} height={24} alt={param.tooltip} />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={config.params[param.param || ''] as string}
|
||||
onChange={(e) => handleParameterChange(
|
||||
exerciseIndex,
|
||||
param.param || '',
|
||||
e.target.value
|
||||
)}
|
||||
className="px-3 py-2 shadow-lg rounded-md text-mti-gray-dim w-full"
|
||||
placeholder="Enter here..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const inputValue = Number(config.params[param.param || '1'].toString());
|
||||
const isParagraphMatch = config.type.split("?name=")[1] === "paragraphMatch";
|
||||
const maxParagraphs = isParagraphMatch ? extraArgs!.text.split("\n\n").length : 50;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{`${param.label}${isParagraphMatch ? ` (out of ${extraArgs!.text.split("\n\n").length} paragraphs)` : ""}`}
|
||||
</label>
|
||||
{param.tooltip && (
|
||||
<>
|
||||
<Tooltip id={config.type} className="z-50 bg-white shadow-md rounded-sm" />
|
||||
<a data-tooltip-id={config.type} data-tooltip-html={param.tooltip} className='ml-1 flex items-center justify-center'>
|
||||
<Image src="/mat-icon-info.svg" width={24} height={24} alt={param.tooltip} />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={inputValue}
|
||||
onChange={(e) => handleParameterChange(
|
||||
exerciseIndex,
|
||||
param.param || '',
|
||||
e.target.value ? Number(e.target.value) : ''
|
||||
)}
|
||||
className="px-3 py-2 shadow-lg rounded-md text-mti-gray-dim w-full"
|
||||
min={1}
|
||||
max={maxParagraphs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderExerciseHeader = (
|
||||
exercise: ExerciseGen,
|
||||
exerciseIndex: number,
|
||||
config: ExerciseConfig,
|
||||
extraParams: boolean,
|
||||
) => {
|
||||
const generateParam = exercise.extra?.find(param => param.param === 'generate');
|
||||
|
||||
return (
|
||||
<div className={clsx("flex items-center w-full", extraParams ? "mb-4" : "py-4")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<exercise.icon className="h-5 w-5" />
|
||||
<h3 className="font-medium text-lg">{exercise.label}</h3>
|
||||
</div>
|
||||
{/* when placeholders are done uncomment this*/}
|
||||
{/*generateParam && renderParameterInput(generateParam, exerciseIndex, config)*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-4 py-6">
|
||||
{configurations.map((config, exerciseIndex) => {
|
||||
const exercise = exercises.find(ex => {
|
||||
const fullType = ex.extra?.find(e => e.param === 'name')?.value
|
||||
? `${ex.type}/?name=${ex.extra.find(e => e.param === 'name')?.value}`
|
||||
: ex.type;
|
||||
return fullType === config.type;
|
||||
});
|
||||
|
||||
if (!exercise) return null;
|
||||
|
||||
const nonGenerateParams = exercise.extra?.filter(
|
||||
param => param.param !== 'name' && param.param !== 'generate'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={config.type}
|
||||
className={`bg-ielts-${currentModule}/70 text-white rounded-lg p-4 shadow-xl`}
|
||||
>
|
||||
{renderExerciseHeader(exercise, exerciseIndex, config, (exercise.extra || []).length > 2)}
|
||||
|
||||
{nonGenerateParams && nonGenerateParams.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{nonGenerateParams.map(param => (
|
||||
<div key={param.param}>
|
||||
{renderParameterInput(param, exerciseIndex, config)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={onDiscard}
|
||||
className={`px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-400 transition-colors`}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSubmit(configurations)}
|
||||
className={`px-4 py-2 bg-ielts-${currentModule} text-white rounded-md hover:bg-ielts-${currentModule}/80 transition-colors`}
|
||||
>
|
||||
Add Exercises
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExerciseWizard;
|
||||
@@ -1,373 +0,0 @@
|
||||
import {
|
||||
FaListUl,
|
||||
FaUnderline,
|
||||
FaPen,
|
||||
FaBookOpen,
|
||||
FaEnvelope,
|
||||
FaComments,
|
||||
FaHandshake,
|
||||
FaParagraph,
|
||||
FaLightbulb,
|
||||
FaHeadphones,
|
||||
FaWpforms,
|
||||
} from 'react-icons/fa6';
|
||||
|
||||
import {
|
||||
FaEdit,
|
||||
FaFileAlt,
|
||||
FaUserFriends,
|
||||
FaCheckSquare,
|
||||
FaQuestionCircle,
|
||||
} from 'react-icons/fa';
|
||||
import { ExerciseGen } from './generatedExercises';
|
||||
|
||||
const quantity = (quantity: number, tooltip?: string) => {
|
||||
return {
|
||||
param: "quantity",
|
||||
label: "Quantity",
|
||||
tooltip: tooltip ? tooltip : "Exercise Quantity",
|
||||
value: quantity
|
||||
}
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
return {
|
||||
param: "generate",
|
||||
value: true
|
||||
}
|
||||
}
|
||||
|
||||
const reading = (passage: number) => {
|
||||
const readingExercises = [
|
||||
{
|
||||
label: `Passage ${passage} - Fill Blanks`,
|
||||
type: `reading_${passage}`,
|
||||
icon: FaEdit,
|
||||
sectionId: passage,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "fillBlanks"
|
||||
},
|
||||
{
|
||||
param: "num_random_words",
|
||||
label: "Random Words",
|
||||
tooltip: "Words that are not the solution",
|
||||
value: 1
|
||||
},
|
||||
quantity(4, "Quantity of Blanks"),
|
||||
generate()
|
||||
],
|
||||
module: "reading"
|
||||
},
|
||||
{
|
||||
label: `Passage ${passage} - Write Blanks`,
|
||||
type: `reading_${passage}`,
|
||||
icon: FaPen,
|
||||
sectionId: passage,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "writeBlanks"
|
||||
},
|
||||
{
|
||||
param: "max_words",
|
||||
label: "Word Limit",
|
||||
tooltip: "How many words a solution can have",
|
||||
value: 3
|
||||
},
|
||||
quantity(4, "Quantity of Blanks"),
|
||||
generate()
|
||||
],
|
||||
module: "reading"
|
||||
},
|
||||
{
|
||||
label: `Passage ${passage} - True False`,
|
||||
type: `reading_${passage}`,
|
||||
icon: FaCheckSquare,
|
||||
sectionId: passage,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "trueFalse"
|
||||
},
|
||||
quantity(4, "Quantity of Statements"),
|
||||
generate()
|
||||
],
|
||||
module: "reading"
|
||||
},
|
||||
{
|
||||
label: `Passage ${passage} - Paragraph Match`,
|
||||
type: `reading_${passage}`,
|
||||
icon: FaParagraph,
|
||||
sectionId: passage,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "paragraphMatch"
|
||||
},
|
||||
quantity(5, "Quantity of Matches"),
|
||||
generate()
|
||||
],
|
||||
module: "reading"
|
||||
}
|
||||
];
|
||||
|
||||
if (passage === 3) {
|
||||
readingExercises.push(
|
||||
{
|
||||
label: `Passage 3 - Idea Match`,
|
||||
type: `reading_3`,
|
||||
icon: FaLightbulb,
|
||||
sectionId: passage,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "ideaMatch"
|
||||
},
|
||||
quantity(5, "Quantity of Ideas"),
|
||||
generate()
|
||||
],
|
||||
module: "reading"
|
||||
},
|
||||
);
|
||||
}
|
||||
return readingExercises;
|
||||
}
|
||||
|
||||
const listening = (section: number) => {
|
||||
const listeningExercises = [
|
||||
{
|
||||
label: `Section ${section} - Multiple Choice`,
|
||||
type: `listening_${section}`,
|
||||
icon: FaHeadphones,
|
||||
sectionId: section,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: section == 3 ? "multipleChoice3Options" : "multipleChoice"
|
||||
},
|
||||
quantity(5, "Quantity of Multiple Choice Questions"),
|
||||
generate()
|
||||
],
|
||||
module: "listening"
|
||||
},
|
||||
{
|
||||
label: `Section ${section} - Write the Blanks: Questions`,
|
||||
type: `listening_${section}`,
|
||||
icon: FaQuestionCircle,
|
||||
sectionId: section,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "writeBlanksQuestions"
|
||||
},
|
||||
quantity(5, "Quantity of Blanks"),
|
||||
generate()
|
||||
],
|
||||
module: "listening"
|
||||
}
|
||||
];
|
||||
|
||||
if (section === 1 || section === 4) {
|
||||
listeningExercises.push(
|
||||
{
|
||||
label: `Section ${section} - Write the Blanks: Fill`,
|
||||
type: `listening_${section}`,
|
||||
icon: FaEdit,
|
||||
sectionId: section,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "writeBlanksFill"
|
||||
},
|
||||
quantity(5, "Quantity of Blanks"),
|
||||
generate()
|
||||
],
|
||||
module: "listening"
|
||||
}
|
||||
);
|
||||
listeningExercises.push(
|
||||
{
|
||||
label: `Section ${section} - Write the Blanks: Form`,
|
||||
type: `listening_${section}`,
|
||||
sectionId: section,
|
||||
icon: FaWpforms,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "writeBlanksForm"
|
||||
},
|
||||
quantity(5, "Quantity of Blanks"),
|
||||
generate()
|
||||
],
|
||||
module: "listening"
|
||||
}
|
||||
);
|
||||
}
|
||||
return listeningExercises;
|
||||
}
|
||||
|
||||
const EXERCISES: ExerciseGen[] = [
|
||||
/*{
|
||||
label: "Multiple Choice",
|
||||
type: "multipleChoice",
|
||||
icon: FaListUl,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "multipleChoice"
|
||||
},
|
||||
quantity(10, "Amount"),
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},*/
|
||||
{
|
||||
label: "Multiple Choice - Blank Space",
|
||||
type: "mcBlank",
|
||||
icon: FaEdit,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "mcBlank"
|
||||
},
|
||||
quantity(10, "Amount"),
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},
|
||||
{
|
||||
label: "Multiple Choice - Underlined",
|
||||
type: "mcUnderline",
|
||||
icon: FaUnderline,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "mcUnderline"
|
||||
},
|
||||
quantity(10, "Amount"),
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},
|
||||
/*{
|
||||
label: "Blank Space", <- Assuming this is FillBlanks aswell
|
||||
type: "blankSpaceText",
|
||||
icon: FaPen,
|
||||
extra: [
|
||||
quantity(10, "Nº of Blanks"),
|
||||
{
|
||||
label: "Passage Word Size",
|
||||
param: "text_size",
|
||||
value: "250"
|
||||
},
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},*/
|
||||
{
|
||||
label: "Fill Blanks: Multiple Choice",
|
||||
type: "fillBlanksMC",
|
||||
icon: FaPen,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "fillBlanksMC"
|
||||
},
|
||||
quantity(10, "Nº of Blanks"),
|
||||
{
|
||||
label: "Passage Word Size",
|
||||
param: "text_size",
|
||||
value: "250"
|
||||
},
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},
|
||||
{
|
||||
label: "Reading Passage: Multiple Choice",
|
||||
type: "passageUtas",
|
||||
icon: FaBookOpen,
|
||||
extra: [
|
||||
{
|
||||
param: "name",
|
||||
value: "passageUtas"
|
||||
},
|
||||
// in the utas exam there was only mc so I'm assuming short answers are deprecated
|
||||
/*{
|
||||
label: "Short Answers",
|
||||
param: "sa_qty",
|
||||
value: "10"
|
||||
},*/
|
||||
quantity(10, "Multiple Choice Quantity"),
|
||||
{
|
||||
label: "Reading Passage Topic",
|
||||
param: "topic",
|
||||
value: "",
|
||||
type: "text"
|
||||
},
|
||||
{
|
||||
label: "Passage Word Size",
|
||||
param: "text_size",
|
||||
value: "700"
|
||||
},
|
||||
generate()
|
||||
],
|
||||
module: "level"
|
||||
},
|
||||
{
|
||||
label: "Task 1 - Letter",
|
||||
type: "writing_letter",
|
||||
icon: FaEnvelope,
|
||||
extra: [
|
||||
generate()
|
||||
],
|
||||
module: "writing"
|
||||
},
|
||||
{
|
||||
label: "Task 2 - Essay",
|
||||
type: "writing_2",
|
||||
icon: FaFileAlt,
|
||||
extra: [
|
||||
generate()
|
||||
],
|
||||
module: "writing"
|
||||
},
|
||||
{
|
||||
label: "Exercise 1",
|
||||
type: "speaking_1",
|
||||
icon: FaComments,
|
||||
extra: [
|
||||
generate()
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
{
|
||||
label: "Exercise 2",
|
||||
type: "speaking_2",
|
||||
icon: FaUserFriends,
|
||||
extra: [
|
||||
generate()
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
{
|
||||
label: "Interactive",
|
||||
type: "speaking_3",
|
||||
icon: FaHandshake,
|
||||
extra: [
|
||||
generate()
|
||||
],
|
||||
module: "speaking"
|
||||
},
|
||||
...reading(1),
|
||||
...reading(2),
|
||||
...reading(3),
|
||||
...listening(1),
|
||||
...listening(2),
|
||||
...listening(3),
|
||||
...listening(4),
|
||||
]
|
||||
|
||||
export default EXERCISES;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { IconType } from "react-icons";
|
||||
|
||||
export interface GeneratedExercises {
|
||||
exercises: Record<string, string>[];
|
||||
sectionId: number;
|
||||
module: string;
|
||||
}
|
||||
|
||||
export interface GeneratorState {
|
||||
loading: boolean;
|
||||
sectionId: number;
|
||||
}
|
||||
|
||||
|
||||
export interface ExerciseGen {
|
||||
label: string;
|
||||
type: string;
|
||||
icon: IconType;
|
||||
sectionId?: number;
|
||||
extra?: { param?: string; value?: string | number | boolean; label?: string; tooltip?: string, type?: string}[];
|
||||
module: string
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import EXERCISES from "./exercises";
|
||||
import clsx from "clsx";
|
||||
import { ExerciseGen, GeneratedExercises, GeneratorState } from "./generatedExercises";
|
||||
import Modal from "@/components/Modal";
|
||||
import { useState } from "react";
|
||||
import ExerciseWizard, { ExerciseConfig } from "./ExerciseWizard";
|
||||
import { generate } from "../../SettingsEditor/Shared/Generate";
|
||||
import { Module } from "@/interfaces";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { ListeningPart, Message, ReadingPart } from "@/interfaces/exam";
|
||||
import { BsArrowRepeat } from "react-icons/bs";
|
||||
|
||||
interface ExercisePickerProps {
|
||||
module: string;
|
||||
sectionId: number;
|
||||
difficulty: string;
|
||||
extraArgs?: Record<string, any>;
|
||||
}
|
||||
|
||||
const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
module,
|
||||
sectionId,
|
||||
extraArgs = undefined,
|
||||
}) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { difficulty } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||
const section = useExamEditorStore((store) => store.modules[currentModule].sections.find((s) => s.sectionId == sectionId));
|
||||
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
if (section === undefined) return <></>;
|
||||
|
||||
const { state, selectedExercises } = section;
|
||||
|
||||
const getFullExerciseType = (exercise: ExerciseGen): string => {
|
||||
if (exercise.extra && exercise.extra.length > 0) {
|
||||
const extraValue = exercise.extra.find(e => e.param === 'name')?.value;
|
||||
return extraValue ? `${exercise.type}/?name=${extraValue}` : exercise.type;
|
||||
}
|
||||
return exercise.type;
|
||||
};
|
||||
|
||||
const handleChange = (exercise: ExerciseGen) => {
|
||||
const fullType = getFullExerciseType(exercise);
|
||||
|
||||
const newSelected = selectedExercises.includes(fullType)
|
||||
? selectedExercises.filter(type => type !== fullType)
|
||||
: [...selectedExercises, fullType];
|
||||
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { module: currentModule, sectionId, field: "selectedExercises", value: newSelected } })
|
||||
};
|
||||
|
||||
const moduleExercises = (sectionId && module !== "level" ? EXERCISES.filter((ex) => ex.module === module && ex.sectionId == sectionId) : EXERCISES.filter((ex) => ex.module === module));
|
||||
|
||||
const onModuleSpecific = (configurations: ExerciseConfig[]) => {
|
||||
const exercises = configurations.map(config => {
|
||||
const exerciseType = config.type.split('name=')[1];
|
||||
|
||||
return {
|
||||
type: exerciseType,
|
||||
quantity: Number(config.params.quantity || 1),
|
||||
...(config.params.num_random_words !== undefined && {
|
||||
num_random_words: Number(config.params.num_random_words)
|
||||
}),
|
||||
...(config.params.max_words !== undefined && {
|
||||
max_words: Number(config.params.max_words)
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
let context, moduleState;
|
||||
|
||||
switch (module) {
|
||||
case 'reading':
|
||||
moduleState = state as ReadingPart;
|
||||
context = {
|
||||
text: moduleState.text.content
|
||||
}
|
||||
break;
|
||||
case 'listening':
|
||||
moduleState = state as ListeningPart;
|
||||
let script = moduleState.script;
|
||||
let text, dialog;
|
||||
switch (sectionId) {
|
||||
case 1:
|
||||
case 3:
|
||||
dialog = script as Message[];
|
||||
text = dialog.map((d) => `${d.name}: ${d.text}`).join("\n");
|
||||
context = { text: text }
|
||||
break;
|
||||
case 2:
|
||||
case 4:
|
||||
text = script as string;
|
||||
context = { text: text }
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
context = {}
|
||||
}
|
||||
generate(
|
||||
sectionId,
|
||||
module as Module,
|
||||
"exercises",
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
...context,
|
||||
exercises: exercises,
|
||||
difficulty: difficulty
|
||||
}
|
||||
},
|
||||
(data: any) => [{
|
||||
exercises: data.exercises
|
||||
}]
|
||||
);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "selectedExercises", value: [] } })
|
||||
setPickerOpen(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={pickerOpen} onClose={() => setPickerOpen(false)} title="Exercise Wizard"
|
||||
titleClassName={clsx(
|
||||
"text-2xl font-semibold text-center py-4",
|
||||
`bg-ielts-${module} text-white`,
|
||||
"shadow-sm",
|
||||
"-mx-6 -mt-6",
|
||||
"mb-6"
|
||||
)}
|
||||
>
|
||||
<ExerciseWizard
|
||||
sectionId={sectionId}
|
||||
exercises={moduleExercises}
|
||||
onSubmit={onModuleSpecific}
|
||||
onDiscard={() => setPickerOpen(false)}
|
||||
extraArgs={extraArgs}
|
||||
/>
|
||||
</Modal>
|
||||
<div className="flex flex-col gap-4 px-4" key={sectionId}>
|
||||
<div className="space-y-2">
|
||||
{moduleExercises.map((exercise) => {
|
||||
const fullType = getFullExerciseType(exercise);
|
||||
return (
|
||||
<label
|
||||
key={fullType}
|
||||
className={`flex items-center space-x-3 text-white font-semibold cursor-pointer p-2 hover:bg-ielts-${exercise.module}/70 rounded bg-ielts-${exercise.module}/90`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="exercise"
|
||||
value={fullType}
|
||||
checked={selectedExercises.includes(fullType)}
|
||||
onChange={() => handleChange(exercise)}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<exercise.icon className="h-5 w-5 text-white" />
|
||||
<span>{exercise.label}</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-row justify-center">
|
||||
<button
|
||||
className={
|
||||
clsx("flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300 disabled:cursor-not-allowed",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/40 `,
|
||||
)
|
||||
}
|
||||
onClick={() => setPickerOpen(true)}
|
||||
disabled={selectedExercises.length == 0}
|
||||
>
|
||||
{section.generating === "exercises" ? (
|
||||
<div key={`section-${sectionId}`} className="flex items-center justify-center">
|
||||
<BsArrowRepeat className="text-white animate-spin" size={25} />
|
||||
</div>
|
||||
) : (
|
||||
<>Set Up Exercises ({selectedExercises.length}) </>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExercisePicker;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Module } from "@/interfaces";
|
||||
import clsx from "clsx";
|
||||
import { ReactNode } from "react";
|
||||
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave, MdGrade, MdOutlineGrade } from "react-icons/md";
|
||||
import { MdDelete, MdEdit, MdEditOff, MdRefresh, MdSave} from "react-icons/md";
|
||||
import { HiOutlineClipboardCheck, HiOutlineClipboardList} from "react-icons/hi";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -10,14 +11,15 @@ interface Props {
|
||||
module?: Module;
|
||||
handleSave: () => void;
|
||||
handleDiscard: () => void;
|
||||
modeHandle?: () => void;
|
||||
evaluationHandle?: () => void;
|
||||
handleDelete?: () => void;
|
||||
handlePractice?: () => void;
|
||||
handleEdit?: () => void;
|
||||
isEvaluationEnabled?: boolean;
|
||||
mode?: "delete" | "edit";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, modeHandle, evaluationHandle, children, mode = "delete", module }) => {
|
||||
const Header: React.FC<Props> = ({
|
||||
title, description, editing, isEvaluationEnabled, handleSave, handleDiscard, handleDelete, handleEdit, handlePractice, children, module }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-6 text-sm">
|
||||
<div>
|
||||
@@ -48,26 +50,18 @@ const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnab
|
||||
<MdRefresh size={18} />
|
||||
Discard
|
||||
</button>
|
||||
{mode === "delete" ? (
|
||||
{handleEdit && (
|
||||
<button
|
||||
onClick={modeHandle}
|
||||
className="px-4 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<MdDelete size={18} />
|
||||
Delete
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={modeHandle}
|
||||
onClick={handleEdit}
|
||||
className={`px-4 py-2 bg-ielts-${module}/80 text-white hover:bg-ielts-${module} rounded-lg transition-all duration-200 flex items-center gap-2`}
|
||||
>
|
||||
{editing ? <MdEditOff size={18} /> : <MdEdit size={18} />}
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
{mode === "delete" &&
|
||||
{handlePractice &&
|
||||
<button
|
||||
onClick={evaluationHandle}
|
||||
onClick={handlePractice}
|
||||
className={clsx(
|
||||
"px-4 py-2 rounded-lg flex items-center gap-2 transition-all duration-200",
|
||||
isEvaluationEnabled
|
||||
@@ -75,10 +69,19 @@ const Header: React.FC<Props> = ({ title, description, editing, isEvaluationEnab
|
||||
: 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||
)}
|
||||
>
|
||||
{isEvaluationEnabled ? <MdGrade size={18} /> : <MdOutlineGrade size={18} />}
|
||||
{isEvaluationEnabled ? 'Graded Exercise' : 'Practice Only'}
|
||||
{isEvaluationEnabled ? <HiOutlineClipboardCheck size={18} /> : <HiOutlineClipboardList size={18} />}
|
||||
{isEvaluationEnabled ? 'Graded' : 'Practice'}
|
||||
</button>
|
||||
}
|
||||
{handleDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 bg-white border border-red-500 text-red-500 hover:bg-red-50 rounded-lg transition-all duration-200 flex items-center gap-2"
|
||||
>
|
||||
<MdDelete size={18} />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FaPencilAlt } from 'react-icons/fa';
|
||||
import { Module } from '@/interfaces';
|
||||
import clsx from 'clsx';
|
||||
import WordUploader from './WordUploader';
|
||||
import GenLoader from '../../Exercises/Shared/GenLoader';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
|
||||
const ImportOrFromScratch: React.FC<{ module: Module; }> = ({ module }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const { importing } = useExamEditorStore((store) => store.modules[currentModule])
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch({ type: "UPDATE_MODULE", payload: { updates: { importModule: false } } });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{importing ? (
|
||||
<GenLoader module={module} custom={`Importing ${module} exam ...`} className='flex flex-grow justify-center bg-slate-200 ' />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 w-full flex-1 gap-6">
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
"flex flex-col items-center flex-1 gap-6 justify-center p-8",
|
||||
"border-2 border-gray-200 rounded-xl",
|
||||
`bg-ielts-${module}/20 hover:bg-ielts-${module}/30`,
|
||||
"transition-all duration-300",
|
||||
"shadow-sm hover:shadow-md group")}
|
||||
>
|
||||
<div className="transform group-hover:scale-105 transition-transform duration-300">
|
||||
<FaPencilAlt className={clsx("w-20 h-20 transition-colors duration-300",
|
||||
module === "reading" && "text-indigo-800 group-hover:text-indigo-950",
|
||||
module === "listening" && "text-purple-800 group-hover:text-purple-950",
|
||||
module === "level" && "text-teal-700 group-hover:text-teal-900"
|
||||
)} />
|
||||
</div>
|
||||
<span className={clsx("text-lg font-bold transition-colors duration-300",
|
||||
module === "reading" && "text-indigo-800 group-hover:text-indigo-950",
|
||||
module === "listening" && "text-purple-800 group-hover:text-purple-950",
|
||||
module === "level" && "text-teal-700 group-hover:text-teal-900"
|
||||
)}>
|
||||
Start from Scratch
|
||||
</span>
|
||||
</button>
|
||||
<div className='h-full'>
|
||||
<WordUploader module={module} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportOrFromScratch;
|
||||
@@ -1,297 +0,0 @@
|
||||
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