import { Difficulty, FillBlanksExercise, ReadingPart } from "@/interfaces/exam"; import { useCallback, useEffect, useReducer, useState } from "react"; import BlanksEditor from ".."; import { Card, CardContent } from "@/components/ui/card"; import { MdEdit, MdEditOff } from "react-icons/md"; import FillBlanksWord from "./FillBlanksWord"; import { FaPlus } from "react-icons/fa"; import useExamEditorStore from "@/stores/examEditor"; import { blanksReducer, BlankState, getTextSegments } from "../BlanksReducer"; import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit"; import { AlertItem } from "../../Shared/Alert"; import validateBlanks from "../validateBlanks"; import { toast } from "react-toastify"; import setEditingAlert from "../../Shared/setEditingAlert"; import PromptEdit from "../../Shared/PromptEdit"; interface Word { letter: string; word: string; } const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => { 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 [selectedBlankId, setSelectedBlankId] = useState(null); const [answers, setAnswers] = useState>( new Map(exercise.solutions.map(({ id, solution }) => [id, solution])) ); const [isEditMode, setIsEditMode] = useState(false); const [newWord, setNewWord] = useState(''); const [editing, setEditing] = useState(false); const updateLocal = (exercise: FillBlanksExercise) => { 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 (!validateBlanks(blanksState.blanks, answers, alerts, setAlerts)) { toast.error("Please fix the errors before saving!"); return; } setEditing(false); setAlerts([]); const updatedExercise = { ...local, text: blanksState.text, solutions: Array.from(answers.entries()).map(([id, solution]) => ({ id, solution })) }; 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); setAnswers(new Map(exercise.solutions.map(({ id, solution }) => [id, solution]))); setIsEditMode(false); setNewWord(''); setLocal(exercise); blanksDispatcher({ type: "RESET", payload: { text: exercise.text } }); blanksDispatcher({ type: "SET_TEXT", payload: exercise.text || "" }); const tokens = getTextSegments(exercise.text || ""); const initialBlanks = tokens.reduce((acc, token, idx) => { if (token.type === 'blank') { acc.push({ id: token.id, position: idx }); } return acc; }, [] as BlankState[]); blanksDispatcher({ type: "SET_BLANKS", payload: initialBlanks }); }, 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); setAnswers(new Map(exercise.solutions.map(({ id, solution }) => [id, solution]))); } }, [exercise, editing]); const handleWordSelect = (word: string) => { if (!selectedBlankId) return; if (!editing) setEditing(true); const newAnswers = new Map(answers); newAnswers.set(selectedBlankId, word); setAnswers(newAnswers); setLocal(prev => ({ ...prev, solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({ id, solution })) })); }; const handleAddWord = () => { const word = newWord.trim(); if (!word) return; setLocal(prev => { const nextLetter = String.fromCharCode(65 + prev.words.length); return { ...prev, words: [...prev.words, { letter: nextLetter, word }] }; }); setNewWord(''); }; const handleRemoveWord = (index: number) => { if (!editing) setEditing(true); if (answers.size === 1) { toast.error("There needs to be at least 1 word!"); return; } setLocal(prev => { const newWords = prev.words.filter((_, i) => i !== index) as Word[]; const removedWord = prev.words[index] as Word; const newAnswers = new Map(answers); for (const [blankId, answer] of newAnswers.entries()) { if (answer === removedWord.word) { newAnswers.delete(blankId); } } setAnswers(newAnswers); return { ...prev, words: newWords, solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({ id, solution })) }; }); }; const handleEditWord = (index: number, newWord: string) => { if (!editing) setEditing(true); setLocal(prev => { const newWords = [...prev.words] as Word[]; const oldWord = newWords[index].word; newWords[index] = { ...newWords[index], word: newWord }; const newAnswers = new Map(answers); for (const [blankId, answer] of newAnswers.entries()) { if (answer === oldWord) { newAnswers.set(blankId, newWord); } } setAnswers(newAnswers); return { ...prev, words: newWords, solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({ id, solution })) }; }); }; const handleBlankRemove = (blankId: number) => { if (!editing) setEditing(true); const newAnswers = new Map(answers); newAnswers.delete(blankId.toString()); setAnswers(newAnswers); setLocal(prev => ({ ...prev, solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({ id, solution })) })); blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId }); }; useEffect(() => { validateBlanks(blanksState.blanks, answers, alerts, setAlerts); // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers, blanksState.blanks, blanksState.textMode]) useEffect(()=> { setEditingAlert(editing, setAlerts); }, [editing]) 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 (
setSelectedBlankId(blankId?.toString() || null)} onBlankRemove={handleBlankRemove} onSave={handleSave} onDiscard={handleDiscard} onDelete={handleDelete} setEditing={setEditing} onPractice={handlePractice} isEvaluationEnabled={!local.isPractice} prompt={local.prompt} updatePrompt={(prompt: string) => updateLocal({...local, prompt})} > <> {!blanksState.textMode &&
Word Bank
{(local.words as Word[]).map((wordItem, index) => ( handleWordSelect(wordItem.word)} onRemove={isEditMode ? () => handleRemoveWord(index) : undefined} onEdit={isEditMode ? (newWord) => handleEditWord(index, newWord) : undefined} isEditMode={isEditMode} /> ))}
{isEditMode && (
setNewWord(e.target.value)} placeholder="Enter new word" className="flex-1 px-3 py-2 border border-r-0 rounded-l-md focus:outline-none" name="" />
)}
}
); }; export default FillBlanksLetters;