import AutoExpandingTextArea from "@/components/Low/AutoExpandingTextarea"; import { Card, CardContent } from "@/components/ui/card"; import { WriteBlanksExercise, ReadingPart, Difficulty } from "@/interfaces/exam"; import useExamEditorStore from "@/stores/examEditor"; import { DragEndEvent } from "@dnd-kit/core"; import { arrayMove } from "@dnd-kit/sortable"; import { useState, useEffect, useCallback } from "react"; import { MdEditOff, MdEdit, MdDelete, MdAdd } from "react-icons/md"; import { toast } from "react-toastify"; import useSectionEdit from "../../Hooks/useSectionEdit"; import Alert, { AlertItem } from "../Shared/Alert"; import QuestionsList from "../Shared/QuestionsList"; import setEditingAlert from "../Shared/setEditingAlert"; import SortableQuestion from "../Shared/SortableQuestion"; import { ParsedQuestion, parseLine, reconstructLine } from "./parsing"; import { validateQuestions, validateEmptySolutions, validateWordCount } from "./validation"; import Header from "../../Shared/Header"; import BlanksFormEditor from "./BlanksFormEditor"; import PromptEdit from "../Shared/PromptEdit"; import { uuidv4 } from "@firebase/util"; const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => { const { currentModule, dispatch } = useExamEditorStore(); const difficulty = useExamEditorStore((state) => state.modules[currentModule].difficulty); const { state } = useExamEditorStore( (state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)! ); const section = state as ReadingPart; const [alerts, setAlerts] = useState([]); const [local, setLocal] = useState(exercise); const [editingPrompt, setEditingPrompt] = useState(false); const [parsedQuestions, setParsedQuestions] = useState([]); const { editing, handleSave, handleDiscard, handleDelete, handlePractice, setEditing } = useSectionEdit({ sectionId, onSave: () => { const isQuestionsValid = validateQuestions(parsedQuestions, setAlerts); const isSolutionsValid = validateEmptySolutions(local.solutions, setAlerts); if (!isQuestionsValid || !isSolutionsValid) { toast.error("Please fix the errors before saving!"); return; } setEditing(false); setAlerts([]); const newSection = { ...section, exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex) }; dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection, module: currentModule } }); }, onDiscard: () => { setLocal(exercise); setParsedQuestions([]); }, 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(() => { const questions = local.text.split('\\n') .filter(line => line.trim()) .map(line => { const match = line.match(/{{(\d+)}}/); return { id: match ? match[1] : `unknown-${Date.now()}`, parts: parseLine(line), editingPlaceholders: true }; }); setParsedQuestions(questions); }, [local.text]); useEffect(() => { setEditingAlert(editing, setAlerts); }, [editing]); useEffect(() => { validateWordCount(local.solutions, local.maxWords, setAlerts); }, [local.maxWords, local.solutions]); const updateLocal = (exercise: WriteBlanksExercise) => { setLocal(exercise); setEditing(true); }; const addQuestion = () => { const existingIds = parsedQuestions.map(q => parseInt(q.id)); const newId = (Math.max(...existingIds, 0) + 1).toString(); const newLine = `New question with blank {{${newId}}}`; const updatedQuestions = [...parsedQuestions, { uuid: uuidv4(), id: newId, parts: parseLine(newLine), editingPlaceholders: true }]; const newText = updatedQuestions .map(q => reconstructLine(q.parts)) .join('\\n') + '\\n'; const updatedSolutions = [...local.solutions, { uuid: uuidv4(), id: newId, solution: [""] }]; updateLocal({ ...local, text: newText, solutions: updatedSolutions }); }; const deleteQuestion = (id: string) => { if (parsedQuestions.length === 1) { toast.error("There needs to be at least one question!"); return; } const updatedQuestions = parsedQuestions.filter(q => q.id !== id); const newText = updatedQuestions .map(q => reconstructLine(q.parts)) .join('\\n') + '\\n'; const updatedSolutions = local.solutions.filter(s => s.id !== id); updateLocal({ ...local, text: newText, solutions: updatedSolutions }); }; const handleQuestionUpdate = (questionId: string, newText: string) => { const updatedQuestions = parsedQuestions.map(q => q.id === questionId ? { ...q, parts: parseLine(newText) } : q ); const updatedText = updatedQuestions .map(q => reconstructLine(q.parts)) .join('\\n') + '\\n'; updateLocal({ ...local, text: updatedText }); }; const addSolution = (questionId: string) => { const newSolutions = local.solutions.map(s => s.id === questionId ? { ...s, solution: [...s.solution, ""] } : s ); updateLocal({ ...local, solutions: newSolutions }); }; const updateSolution = (questionId: string, index: number, value: string) => { const newSolutions = local.solutions.map(s => s.id === questionId ? { ...s, solution: s.solution.map((sol, i) => i === index ? value : sol) } : s ); updateLocal({ ...local, solutions: newSolutions }); }; const deleteSolution = (questionId: string, index: number) => { const solutions = local.solutions.find(s => s.id === questionId); if (solutions && solutions.solution.length <= 1) { toast.error("Each question must have at least one solution!"); return; } const newSolutions = local.solutions.map(s => s.id === questionId ? { ...s, solution: s.solution.filter((_, i) => i !== index) } : s ); updateLocal({ ...local, solutions: newSolutions }); }; const handleQuestionsReorder = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = parsedQuestions.findIndex(q => q.id === active.id); const newIndex = parsedQuestions.findIndex(q => q.id === over.id); const reorderedQuestions = arrayMove(parsedQuestions, oldIndex, newIndex); const newText = reorderedQuestions .map(q => reconstructLine(q.parts)) .join('\\n') + '\\n'; updateLocal({ ...local, text: newText }); }; const saveDifficulty = useCallback((diff: Difficulty) => { if (!difficulty.includes(diff)) { dispatch({ type: 'UPDATE_MODULE', payload: { updates: { difficulty: [...difficulty, diff]} } }); } const updatedExercise = { ...exercise, difficulty: diff }; 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 } }); }, [currentModule, difficulty, dispatch, exercise, section, sectionId]); return (
{alerts.length > 0 && } updateLocal({ ...local, prompt })}/>
q.id)} handleDragEnd={handleQuestionsReorder} > {parsedQuestions.map((question, index) => ( deleteQuestion(question.id)} variant="del-up" >
handleQuestionUpdate(question.id, newText)} />

Solutions:

{local.solutions.find(s => s.id === question.id)?.solution.map((solution, index) => (
updateSolution(question.id, index, e.target.value)} className="flex-1 p-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder={`Solution ${index + 1}`} />
))}
))}
); }; export default WriteBlanksForm;