Exam generation rework, batch user tables, fastapi endpoint switch
This commit is contained in:
230
src/components/ExamEditor/Exercises/MatchSentences/index.tsx
Normal file
230
src/components/ExamEditor/Exercises/MatchSentences/index.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
MdAdd,
|
||||
MdVisibility,
|
||||
MdVisibilityOff
|
||||
} from 'react-icons/md';
|
||||
import { MatchSentencesExercise, ReadingPart } from '@/interfaces/exam';
|
||||
import Alert, { AlertItem } from '../Shared/Alert';
|
||||
import ReferenceViewer from './ParagraphViewer';
|
||||
import Header from '../../Shared/Header';
|
||||
import SortableQuestion from '../Shared/SortableQuestion';
|
||||
import QuestionsList from '../Shared/QuestionsList';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
import useSectionEdit from '../../Hooks/useSectionEdit';
|
||||
import validateMatchSentences from './validation';
|
||||
import setEditingAlert from '../Shared/setEditingAlert';
|
||||
import { toast } from 'react-toastify';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local';
|
||||
|
||||
const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, 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 [local, setLocal] = useState(exercise);
|
||||
const [selectedParagraph, setSelectedParagraph] = useState<string | null>(null);
|
||||
const [showReference, setShowReference] = useState(false);
|
||||
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||
|
||||
const { editing, setEditing, handleSave, handleDiscard, modeHandle } = useSectionEdit({
|
||||
sectionId,
|
||||
onSave: () => {
|
||||
|
||||
const isValid = validateMatchSentences(local.sentences, setAlerts);
|
||||
|
||||
if (!isValid) {
|
||||
toast.error("Please fix the errors before saving!");
|
||||
return;
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
setAlerts([]);
|
||||
|
||||
const newState = { ...section };
|
||||
newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? local : ex);
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
setSelectedParagraph(null);
|
||||
setShowReference(false);
|
||||
},
|
||||
onMode: () => {
|
||||
const newSection = {
|
||||
...section,
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
}
|
||||
});
|
||||
|
||||
const usedOptions = useMemo(() => {
|
||||
return local.sentences.reduce((acc, sentence) => {
|
||||
if (sentence.solution) {
|
||||
acc.add(sentence.solution);
|
||||
}
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
}, [local.sentences]);
|
||||
|
||||
const addHeading = () => {
|
||||
setEditing(true);
|
||||
const newId = (parseInt(local.sentences[local.sentences.length - 1].id) + 1).toString();
|
||||
setLocal({
|
||||
...local,
|
||||
sentences: [
|
||||
...local.sentences,
|
||||
{
|
||||
id: newId,
|
||||
sentence: "",
|
||||
solution: ""
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const updateHeading = (index: number, field: string, value: string) => {
|
||||
setEditing(true);
|
||||
const newSentences = [...local.sentences];
|
||||
|
||||
if (field === 'solution') {
|
||||
const oldSolution = newSentences[index].solution;
|
||||
if (oldSolution) {
|
||||
usedOptions.delete(oldSolution);
|
||||
}
|
||||
}
|
||||
|
||||
newSentences[index] = { ...newSentences[index], [field]: value };
|
||||
setLocal({ ...local, sentences: newSentences });
|
||||
};
|
||||
|
||||
const deleteHeading = (index: number) => {
|
||||
setEditing(true);
|
||||
if (local.sentences.length <= 1) {
|
||||
toast.error(`There needs to be at least one ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedSolution = local.sentences[index].solution;
|
||||
if (deletedSolution) {
|
||||
usedOptions.delete(deletedSolution);
|
||||
}
|
||||
|
||||
const newSentences = local.sentences.filter((_, i) => i !== index);
|
||||
setLocal({ ...local, sentences: newSentences });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
validateMatchSentences(local.sentences, setAlerts);
|
||||
}, [local.sentences]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditingAlert(editing, setAlerts);
|
||||
}, [editing]);
|
||||
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
setEditing(true);
|
||||
setLocal(handleMatchSentencesReorder(event, local));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mx-auto p-2">
|
||||
<Header
|
||||
title={exercise.variant && exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"}
|
||||
description={`Edit ${exercise.variant && exercise.variant == "ideaMatch" ? "ideas/opinions" : "headings"} and their matches`}
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDiscard={handleDiscard}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowReference(!showReference)}
|
||||
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
{showReference ? <MdVisibilityOff size={18} /> : <MdVisibility size={18} />}
|
||||
{showReference ? 'Hide Reference' : 'Show Reference'}
|
||||
</button>
|
||||
</Header>
|
||||
|
||||
<div className="space-y-4">
|
||||
{alerts.length > 0 && <Alert alerts={alerts} />}
|
||||
<QuestionsList
|
||||
ids={local.sentences.map(s => s.id)}
|
||||
handleDragEnd={handleDragEnd}
|
||||
>
|
||||
{local.sentences.map((sentence, index) => (
|
||||
<SortableQuestion
|
||||
key={sentence.id}
|
||||
id={sentence.id}
|
||||
index={index}
|
||||
deleteQuestion={() => deleteHeading(index)}
|
||||
onFocus={() => setSelectedParagraph(sentence.solution)}
|
||||
>
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={sentence.sentence}
|
||||
onChange={(e) => updateHeading(index, 'sentence', e.target.value)}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none text-mti-gray-dim"
|
||||
placeholder={`Enter ${exercise.variant && exercise.variant == "ideaMatch" ? "idea/opinion" : "heading"} ...`}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={sentence.solution}
|
||||
onChange={(e) => {
|
||||
updateHeading(index, 'solution', e.target.value);
|
||||
setSelectedParagraph(e.target.value);
|
||||
}}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none bg-white text-mti-gray-dim"
|
||||
>
|
||||
<option value="">Select matching {exercise.variant == "ideaMatch" ? "author" : "paragraph"}...</option>
|
||||
{local.options.map((option) => {
|
||||
const isUsed = usedOptions.has(option.id);
|
||||
const isCurrentSelection = sentence.solution === option.id;
|
||||
|
||||
return (
|
||||
<option
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
disabled={isUsed && !isCurrentSelection}
|
||||
>
|
||||
{exercise.variant == "ideaMatch" ? "Author" : "Paragraph"} {option.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
</SortableQuestion>
|
||||
))}
|
||||
</QuestionsList>
|
||||
|
||||
<button
|
||||
onClick={addHeading}
|
||||
className="w-full p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-colors flex items-center justify-center gap-2 text-gray-600 hover:text-blue-600"
|
||||
>
|
||||
<MdAdd size={18} />
|
||||
Add New Match
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ReferenceViewer
|
||||
headings={exercise.variant !== "ideaMatch"}
|
||||
showReference={showReference}
|
||||
selectedReference={selectedParagraph}
|
||||
options={local.options}
|
||||
setShowReference={setShowReference}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MatchSentences;
|
||||
Reference in New Issue
Block a user