221 lines
8.3 KiB
TypeScript
221 lines
8.3 KiB
TypeScript
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { WriteBlanksExercise, ReadingPart } from "@/interfaces/exam";
|
|
import useExamEditorStore from "@/stores/examEditor";
|
|
import { useState, useReducer, useEffect } from "react";
|
|
import { toast } from "react-toastify";
|
|
import BlanksEditor from "..";
|
|
import { AlertItem } from "../../Shared/Alert";
|
|
import setEditingAlert from "../../Shared/setEditingAlert";
|
|
import { blanksReducer } from "../BlanksReducer";
|
|
import { validateWriteBlanks } from "./validation";
|
|
import AlternativeSolutions from "./AlternativeSolutions";
|
|
|
|
const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
|
|
const { currentModule, dispatch } = useExamEditorStore();
|
|
const { state } = useExamEditorStore(
|
|
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
|
);
|
|
|
|
const section = state as ReadingPart;
|
|
|
|
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
|
const [local, setLocal] = useState(exercise);
|
|
const [selectedBlankId, setSelectedBlankId] = useState<string | null>(null);
|
|
const [editing, setEditing] = useState(false);
|
|
|
|
const updateLocal = (exercise: WriteBlanksExercise) => {
|
|
setLocal(exercise);
|
|
setEditingAlert(true, setAlerts);
|
|
setEditing(true);
|
|
};
|
|
|
|
const [blanksState, blanksDispatcher] = useReducer(blanksReducer, {
|
|
text: exercise.text || "",
|
|
blanks: [],
|
|
selectedBlankId: null,
|
|
draggedItemId: null,
|
|
textMode: false,
|
|
setEditing,
|
|
});
|
|
|
|
const { handleSave, handleDiscard, handleDelete, handlePractice } = useSectionEdit({
|
|
sectionId,
|
|
editing,
|
|
setEditing,
|
|
onSave: () => {
|
|
if (!validateWriteBlanks(local.solutions, local.maxWords, setAlerts)) {
|
|
toast.error("Please fix the errors before saving!");
|
|
return;
|
|
}
|
|
|
|
setEditing(false);
|
|
setAlerts([]);
|
|
|
|
const updatedExercise = {
|
|
...local,
|
|
text: blanksState.text,
|
|
};
|
|
|
|
const newState = { ...section };
|
|
newState.exercises = newState.exercises.map((ex) =>
|
|
ex.id === exercise.id ? updatedExercise : ex
|
|
);
|
|
|
|
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
|
},
|
|
onDiscard: () => {
|
|
setSelectedBlankId(null);
|
|
setLocal(exercise);
|
|
blanksDispatcher({ type: "RESET", payload: { text: exercise.text } });
|
|
},
|
|
onDelete: () => {
|
|
const newSection = {
|
|
...section,
|
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
|
};
|
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } });
|
|
},
|
|
onPractice: () => {
|
|
const updatedExercise = {
|
|
...local,
|
|
isPractice: !local.isPractice
|
|
};
|
|
const newState = { ...section };
|
|
newState.exercises = newState.exercises.map((ex) =>
|
|
ex.id === exercise.id ? updatedExercise : ex
|
|
);
|
|
setLocal((prev) => ({ ...prev, isPractice: !local.isPractice }))
|
|
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } });
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!editing) {
|
|
setLocal(exercise);
|
|
}
|
|
}, [exercise, editing]);
|
|
|
|
const handleAddSolution = (blankId: string) => {
|
|
if (!editing) setEditing(true);
|
|
setLocal(prev => ({
|
|
...prev,
|
|
solutions: prev.solutions.map(s =>
|
|
s.id === blankId
|
|
? { ...s, solution: [...s.solution, ""] }
|
|
: s
|
|
)
|
|
}));
|
|
};
|
|
|
|
const handleRemoveSolution = (blankId: string, index: number) => {
|
|
if (!editing) setEditing(true);
|
|
|
|
const solutions = local.solutions.find(s => s.id === blankId);
|
|
if (solutions && solutions.solution.length <= 1) {
|
|
toast.error("Each blank must have at least one solution!");
|
|
return;
|
|
}
|
|
|
|
setLocal(prev => ({
|
|
...prev,
|
|
solutions: prev.solutions.map(s =>
|
|
s.id === blankId
|
|
? { ...s, solution: s.solution.filter((_, i) => i !== index) }
|
|
: s
|
|
)
|
|
}));
|
|
};
|
|
|
|
const handleEditSolution = (blankId: string, index: number, value: string) => {
|
|
if (!editing) setEditing(true);
|
|
|
|
setLocal(prev => ({
|
|
...prev,
|
|
solutions: prev.solutions.map(s =>
|
|
s.id === blankId
|
|
? {
|
|
...s,
|
|
solution: s.solution.map((sol, i) => i === index ? value : sol)
|
|
}
|
|
: s
|
|
)
|
|
}));
|
|
};
|
|
|
|
const handleBlankRemove = (blankId: number) => {
|
|
if (!editing) setEditing(true);
|
|
setLocal(prev => ({
|
|
...prev,
|
|
solutions: prev.solutions.filter(s => s.id !== blankId.toString())
|
|
}));
|
|
|
|
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
|
};
|
|
|
|
useEffect(() => {
|
|
validateWriteBlanks(local.solutions, local.maxWords, setAlerts);
|
|
}, [local.solutions, local.maxWords]);
|
|
|
|
useEffect(() => {
|
|
setEditingAlert(editing, setAlerts);
|
|
}, [editing]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<BlanksEditor
|
|
title="Write Blanks: Fill"
|
|
alerts={alerts}
|
|
editing={editing}
|
|
state={blanksState}
|
|
blanksDispatcher={blanksDispatcher}
|
|
description={local.prompt}
|
|
initialText={local.text}
|
|
module={currentModule}
|
|
showBlankBank={true}
|
|
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
|
onBlankRemove={handleBlankRemove}
|
|
onSave={handleSave}
|
|
onDiscard={handleDiscard}
|
|
onDelete={handleDelete}
|
|
onPractice={handlePractice}
|
|
setEditing={setEditing}
|
|
isEvaluationEnabled={!local.isPractice}
|
|
prompt={local.prompt}
|
|
updatePrompt={(prompt: string) => updateLocal({ ...local, prompt })}
|
|
>
|
|
{!blanksState.textMode && (
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-lg font-semibold">
|
|
{selectedBlankId
|
|
? `Solutions for Blank ${selectedBlankId}`
|
|
: "Click a blank to edit its solutions"}
|
|
</span>
|
|
{selectedBlankId && (
|
|
<span className="text-sm text-gray-500">
|
|
Max words per solution: {local.maxWords}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{selectedBlankId && (
|
|
<AlternativeSolutions
|
|
solutions={local.solutions.find(s => s.id === selectedBlankId)?.solution || []}
|
|
onAdd={() => handleAddSolution(selectedBlankId)}
|
|
onRemove={(index: number) => handleRemoveSolution(selectedBlankId, index)}
|
|
onEdit={(index: number, value: string) => handleEditSolution(selectedBlankId, index, value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</BlanksEditor>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WriteBlanksFill; |