Exam generation rework, batch user tables, fastapi endpoint switch

This commit is contained in:
Carlos-Mesquita
2024-11-04 23:29:14 +00:00
parent a2bc997e8f
commit 15c9c4d4bd
148 changed files with 11348 additions and 3901 deletions

View 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;