Exam generation rework, batch user tables, fastapi endpoint switch
This commit is contained in:
235
src/components/ExamEditor/Exercises/TrueFalse/index.tsx
Normal file
235
src/components/ExamEditor/Exercises/TrueFalse/index.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import {
|
||||
MdAdd,
|
||||
MdEdit,
|
||||
MdEditOff,
|
||||
} from 'react-icons/md';
|
||||
import Alert, { AlertItem } from '../Shared/Alert';
|
||||
import { ReadingPart, TrueFalseExercise } from '@/interfaces/exam';
|
||||
import QuestionsList from '../Shared/QuestionsList';
|
||||
import Header from '../../Shared/Header';
|
||||
import SortableQuestion from '../Shared/SortableQuestion';
|
||||
import clsx from 'clsx';
|
||||
import useExamEditorStore from '@/stores/examEditor';
|
||||
import useSectionEdit from '../../Hooks/useSectionEdit';
|
||||
import { toast } from 'react-toastify';
|
||||
import validateTrueFalseQuestions from './validation';
|
||||
import setEditingAlert from '../Shared/setEditingAlert';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { handleTrueFalseReorder } from '@/stores/examEditor/reorder/local';
|
||||
|
||||
const TrueFalse: React.FC<{ exercise: TrueFalseExercise, 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 [editingPrompt, setEditingPrompt] = useState(false);
|
||||
|
||||
const [alerts, setAlerts] = useState<AlertItem[]>([]);
|
||||
|
||||
const updateLocal = (exercise: TrueFalseExercise) => {
|
||||
setLocal(exercise);
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const updateQuestion = (index: number, field: string, value: string) => {
|
||||
const newQuestions = [...local.questions];
|
||||
newQuestions[index] = { ...newQuestions[index], [field]: value };
|
||||
updateLocal({ ...local, questions: newQuestions });
|
||||
};
|
||||
|
||||
const addQuestion = () => {
|
||||
const newId = (parseInt(local.questions[local.questions.length - 1].id) + 1).toString();
|
||||
updateLocal({
|
||||
...local,
|
||||
questions: [
|
||||
...local.questions,
|
||||
{
|
||||
prompt: "",
|
||||
solution: undefined,
|
||||
id: newId
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const deleteQuestion = (index: number) => {
|
||||
if (local.questions.length == 1) {
|
||||
toast.error("There needs to be at least one question!");
|
||||
return;
|
||||
}
|
||||
|
||||
const newQuestions = local.questions.filter((_, i) => i !== index);
|
||||
const minId = Math.min(...newQuestions.map(q => parseInt(q.id)));
|
||||
|
||||
const updatedQuestions = newQuestions.map((question, i) => ({
|
||||
...question,
|
||||
id: String(minId + i)
|
||||
}));
|
||||
|
||||
updateLocal({ ...local, questions: updatedQuestions });
|
||||
};
|
||||
|
||||
const { editing, handleSave, handleDiscard, modeHandle, setEditing } = useSectionEdit({
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const isValid = validateTrueFalseQuestions(
|
||||
local.questions,
|
||||
setAlerts
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
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 } });
|
||||
dispatch({ type: "REORDER_EXERCISES" })
|
||||
},
|
||||
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 } });
|
||||
dispatch({ type: "REORDER_EXERCISES" })
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
validateTrueFalseQuestions(local.questions, setAlerts);
|
||||
}, [local.questions]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditingAlert(editing, setAlerts);
|
||||
}, [editing]);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
setEditing(true);
|
||||
setLocal(handleTrueFalseReorder(event, local));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Header
|
||||
title='True/False/Not Given Exercise'
|
||||
description='Edit questions and their solutions'
|
||||
editing={editing}
|
||||
handleSave={handleSave}
|
||||
modeHandle={modeHandle}
|
||||
handleDiscard={handleDiscard}
|
||||
/>
|
||||
{alerts.length > 0 && <Alert className="mb-6" alerts={alerts} />}
|
||||
<Card className="mb-6">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
{editingPrompt ? (
|
||||
<textarea
|
||||
className="flex-1 p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none min-h-[100px]"
|
||||
value={local.prompt}
|
||||
onChange={(e) => updateLocal({ ...local, prompt: e.target.value })}
|
||||
onBlur={() => setEditingPrompt(false)}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-gray-800 mb-2">Question/Instructions displayed to the student:</h3>
|
||||
<p className="text-gray-600">{local.prompt}</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setEditingPrompt(!editingPrompt)}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
{editingPrompt ?
|
||||
<MdEditOff size={20} className="text-gray-500" /> :
|
||||
<MdEdit size={20} className="text-gray-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="space-y-4">
|
||||
<QuestionsList
|
||||
ids={local.questions.map(q => q.id)}
|
||||
handleDragEnd={handleDragEnd}
|
||||
>
|
||||
{local.questions.map((question, index) => (
|
||||
<SortableQuestion
|
||||
key={question.id}
|
||||
id={question.id}
|
||||
index={index}
|
||||
deleteQuestion={deleteQuestion}
|
||||
>
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={question.prompt}
|
||||
onChange={(e) => updateQuestion(index, 'prompt', e.target.value)}
|
||||
className="w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
placeholder="Enter question..."
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
{['true', 'false', 'not_given'].map((value) => (
|
||||
<label
|
||||
key={value}
|
||||
className="flex-1 cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"p-3 text-center rounded-lg border-2 transition-all flex items-center justify-center gap-2",
|
||||
question.solution === value
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`solution-${question.id}`}
|
||||
value={value}
|
||||
checked={question.solution === value}
|
||||
onChange={(e) => updateQuestion(index, 'solution', e.target.value)}
|
||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 sr-only"
|
||||
/>
|
||||
<span>
|
||||
{value.replace('_', ' ').charAt(0).toUpperCase() + value.slice(1).replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</SortableQuestion>
|
||||
))}
|
||||
</QuestionsList>
|
||||
|
||||
<button
|
||||
onClick={addQuestion}
|
||||
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 Question
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrueFalse;
|
||||
Reference in New Issue
Block a user