import React, { useState, useEffect } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { MdAdd, MdEdit, MdEditOff, MdDelete, } from 'react-icons/md'; import QuestionsList from '../Shared/QuestionsList'; import SortableQuestion from '../Shared/SortableQuestion'; import { DragEndEvent } from '@dnd-kit/core'; import Header from '../../Shared/Header'; import clsx from 'clsx'; import Alert, { AlertItem } from '../Shared/Alert'; import AutoExpandingTextArea from '@/components/Low/AutoExpandingTextarea'; import { ReadingPart, WriteBlanksExercise } from '@/interfaces/exam'; import useExamEditorStore from '@/stores/examEditor'; import useSectionEdit from '../../Hooks/useSectionEdit'; import setEditingAlert from '../Shared/setEditingAlert'; import { toast } from 'react-toastify'; import { validateEmptySolutions, validateQuestionText, validateWordCount } from './validation'; import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local'; import { ParsedQuestion, parseText, reconstructText } from './parsing'; const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => { 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([]); const [local, setLocal] = useState(exercise); const [editingPrompt, setEditingPrompt] = useState(false); const [errors, setErrors] = useState<{ [key: string]: string[] }>({}); const [parsedQuestions, setParsedQuestions] = useState([]); const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({ sectionId, mode: "edit", onSave: () => { const isQuestionTextValid = validateQuestionText( parsedQuestions, setAlerts ); const isSolutionsValid = validateEmptySolutions( local.solutions, setAlerts ); if (!isQuestionTextValid || !isSolutionsValid) { toast.error("Please fix the errors before saving!"); return; } setEditing(false); setAlerts([]); //dispatch({ type: 'UPDATE_ROOT', payload: { updates: {globalEdit: globalEdit.filter(id => id !== sectionId)} } }); const newSection = { ...section, exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex) }; dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } }); }, onDiscard: () => { setLocal(exercise); }, onMode: () => { const newSection = { ...section, exercises: section.exercises.filter((ex) => ex.id !== local.id) }; dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } }); }, }); useEffect(() => { setParsedQuestions(parseText(local.text)); }, [local.text]); 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 newQuestion = { id: newId, questionText: "New question" }; const updatedQuestions = [...parsedQuestions, newQuestion]; const updatedText = reconstructText(updatedQuestions); const updatedSolutions = [...local.solutions, { id: newId, solution: [""] }]; updateLocal({ ...local, text: updatedText, solutions: updatedSolutions }); }; const updateQuestionText = (id: string, newText: string) => { const updatedQuestions = parsedQuestions.map(q => q.id === id ? { ...q, questionText: newText } : q ); const updatedText = reconstructText(updatedQuestions); updateLocal({ ...local, text: updatedText }); }; 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 updatedText = reconstructText(updatedQuestions); const updatedSolutions = local.solutions.filter(s => s.id !== id); updateLocal({ ...local, text: updatedText, solutions: updatedSolutions }); }; const addSolutionToQuestion = (questionId: string) => { const newSolutions = [...local.solutions]; const questionIndex = newSolutions.findIndex(s => s.id === questionId); if (questionIndex !== -1) { newSolutions[questionIndex] = { ...newSolutions[questionIndex], solution: [...newSolutions[questionIndex].solution, ""] }; updateLocal({ ...local, solutions: newSolutions }); } }; const updateSolution = (questionId: string, solutionIndex: number, value: string) => { const wordCount = value.trim().split(/\s+/).length; const newSolutions = [...local.solutions]; const questionIndex = newSolutions.findIndex(s => s.id === questionId); if (questionIndex !== -1) { const newSolutionArray = [...newSolutions[questionIndex].solution]; newSolutionArray[solutionIndex] = value; newSolutions[questionIndex] = { ...newSolutions[questionIndex], solution: newSolutionArray }; updateLocal({ ...local, solutions: newSolutions }); } if (wordCount > local.maxWords) { setAlerts(prev => { const filteredAlerts = prev.filter(alert => alert.tag !== `solution-error-${questionId}-${solutionIndex}`); return [...filteredAlerts, { variant: "error", tag: `solution-error-${questionId}-${solutionIndex}`, description: `Alternative solution ${solutionIndex + 1} for question ${questionId} exceeds maximum of ${local.maxWords} words (current: ${wordCount} words)` }]; }); } else { setAlerts(prev => prev.filter(alert => alert.tag !== `solution-error-${questionId}-${solutionIndex}`)); } }; const deleteSolution = (questionId: string, solutionIndex: number) => { const newSolutions = [...local.solutions]; const questionIndex = newSolutions.findIndex(s => s.id === questionId); if (questionIndex !== -1) { if (newSolutions[questionIndex].solution.length == 1) { toast.error("There needs to be at least one solution!"); return; } const newSolutionArray = newSolutions[questionIndex].solution.filter((_, i) => i !== solutionIndex); newSolutions[questionIndex] = { ...newSolutions[questionIndex], solution: newSolutionArray }; updateLocal({ ...local, solutions: newSolutions }); } }; const handleDragEnd = (event: DragEndEvent) => { setEditing(true); setLocal(handleWriteBlanksReorder(event, local)); } useEffect(() => { setEditingAlert(editing, setAlerts); }, [editing]); useEffect(() => { validateWordCount(local.solutions, local.maxWords, setAlerts); // eslint-disable-next-line react-hooks/exhaustive-deps }, [local.maxWords, local.solutions]); useEffect(() => { validateQuestionText(parsedQuestions, setAlerts); }, [parsedQuestions]); useEffect(() => { validateEmptySolutions(local.solutions, setAlerts); }, [local.solutions]); return (
{alerts.length > 0 && }
{editingPrompt ? ( updateLocal({ ...local, prompt: text })} onBlur={() => setEditingPrompt(false)} /> ) : (

Question/Instructions displayed to the student:

{local.prompt}

)}
q.id)} handleDragEnd={handleDragEnd} > {parsedQuestions.map((question) => { const questionSolutions = local.solutions.find(s => s.id === question.id)?.solution || []; return ( deleteQuestion(question.id)} variant="writeBlanks" questionText={question.questionText} onQuestionChange={(value) => updateQuestionText(question.id, value)} >
{questionSolutions.map((solution, solutionIndex) => (
updateSolution(question.id, solutionIndex, e.target.value)} className={clsx( "flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none", errors[question.id]?.[solutionIndex] && "border-red-500" )} placeholder="Enter solution..." />
))}
); })}
); }; export default WriteBlanks;