Blanks Exercises were removing the blanks on the editor and text but not on solutions, added submit and preview to writing and reading
This commit is contained in:
@@ -6,7 +6,7 @@ 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 "../FillBlanksReducer";
|
||||
import { blanksReducer, BlankState, getTextSegments } from "../BlanksReducer";
|
||||
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
|
||||
import { AlertItem } from "../../Shared/Alert";
|
||||
import validateBlanks from "../validateBlanks";
|
||||
@@ -75,7 +75,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
);
|
||||
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setSelectedBlankId(null);
|
||||
@@ -103,7 +102,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -211,11 +209,30 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
});
|
||||
};
|
||||
|
||||
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])
|
||||
@@ -232,6 +249,7 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
||||
module={currentModule}
|
||||
showBlankBank={true}
|
||||
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
||||
onBlankRemove={handleBlankRemove}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useReducer, useState } from "react";
|
||||
import BlanksEditor from "..";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import { blanksReducer, BlankState, getTextSegments } from "../FillBlanksReducer";
|
||||
import { blanksReducer, BlankState, getTextSegments } from "../BlanksReducer";
|
||||
import useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
|
||||
import { AlertItem } from "../../Shared/Alert";
|
||||
import validateBlanks from "../validateBlanks";
|
||||
@@ -73,7 +73,6 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
);
|
||||
|
||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setSelectedBlankId(null);
|
||||
@@ -99,7 +98,6 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,6 +189,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
|
||||
useEffect(() => {
|
||||
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [answers, blanksState.blanks, blanksState.textMode]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -216,8 +215,28 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
solution
|
||||
])
|
||||
));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleBlankRemove = (blankId: number) => {
|
||||
if (!editing) setEditing(true);
|
||||
|
||||
const newAnswers = new Map(answers);
|
||||
newAnswers.delete(blankId.toString());
|
||||
setAnswers(newAnswers);
|
||||
|
||||
setLocal(prev => ({
|
||||
...prev,
|
||||
words: (prev.words as FillBlanksMCOption[]).filter(w => w.id !== blankId.toString()),
|
||||
solutions: Array.from(newAnswers.entries()).map(([id, solution]) => ({
|
||||
id,
|
||||
solution
|
||||
}))
|
||||
}));
|
||||
|
||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<BlanksEditor
|
||||
@@ -234,6 +253,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
setEditing={setEditing}
|
||||
onBlankRemove={handleBlankRemove}
|
||||
>
|
||||
{!blanksState.textMode && selectedBlankId && (
|
||||
<Card className="p-4">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toast } from "react-toastify";
|
||||
import BlanksEditor from "..";
|
||||
import { AlertItem } from "../../Shared/Alert";
|
||||
import setEditingAlert from "../../Shared/setEditingAlert";
|
||||
import { blanksReducer } from "../FillBlanksReducer";
|
||||
import { blanksReducer } from "../BlanksReducer";
|
||||
import { validateWriteBlanks } from "./validation";
|
||||
import AlternativeSolutions from "./AlternativeSolutions";
|
||||
import clsx from "clsx";
|
||||
@@ -126,6 +126,16 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBlankRemove = (blankId: number) => {
|
||||
if (!editing) setEditing(true);
|
||||
setLocal(prev => ({
|
||||
...prev,
|
||||
solutions: prev.solutions.filter(s => s.id !== blankId.toString())
|
||||
}));
|
||||
|
||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
validateWriteBlanks(local.solutions, local.maxWords, setAlerts);
|
||||
}, [local.solutions, local.maxWords]);
|
||||
@@ -147,6 +157,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
||||
module={currentModule}
|
||||
showBlankBank={true}
|
||||
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
||||
onBlankRemove={handleBlankRemove}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
onDelete={modeHandle}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlertItem } from "../../Shared/Alert";
|
||||
import { BlankState } from "../FillBlanksReducer";
|
||||
import { BlankState } from "../BlanksReducer";
|
||||
|
||||
|
||||
export const validateWriteBlanks = (
|
||||
|
||||
@@ -18,7 +18,7 @@ import Alert, { AlertItem } from "../Shared/Alert";
|
||||
import clsx from "clsx";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Blank, DropZone } from "./DragNDrop";
|
||||
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./FillBlanksReducer";
|
||||
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./BlanksReducer";
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -33,6 +33,7 @@ interface Props {
|
||||
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
blanksDispatcher: React.Dispatch<BlanksAction>
|
||||
onBlankSelect?: (blankId: number | null) => void;
|
||||
onBlankRemove: (blankId: number) => void;
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onDelete: () => void;
|
||||
@@ -51,6 +52,7 @@ const BlanksEditor: React.FC<Props> = ({
|
||||
alerts,
|
||||
blanksDispatcher,
|
||||
onBlankSelect,
|
||||
onBlankRemove,
|
||||
onSave,
|
||||
onDiscard,
|
||||
onDelete,
|
||||
@@ -99,9 +101,24 @@ const BlanksEditor: React.FC<Props> = ({
|
||||
const handleTextChange = useCallback(
|
||||
(newText: string) => {
|
||||
const processedText = newText.replace(/\[(\d+)\]/g, "{{$1}}");
|
||||
|
||||
const existingBlankIds = getTextSegments(state.text)
|
||||
.filter(token => token.type === 'blank')
|
||||
.map(token => token.id);
|
||||
|
||||
const newBlankIds = getTextSegments(processedText)
|
||||
.filter(token => token.type === 'blank')
|
||||
.map(token => token.id);
|
||||
|
||||
const removedBlankIds = existingBlankIds.filter(id => !newBlankIds.includes(id));
|
||||
|
||||
removedBlankIds.forEach(id => {
|
||||
onBlankRemove(id);
|
||||
});
|
||||
|
||||
blanksDispatcher({ type: "SET_TEXT", payload: processedText });
|
||||
},
|
||||
[blanksDispatcher]
|
||||
[blanksDispatcher, state.text, onBlankRemove]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,8 +133,9 @@ const BlanksEditor: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
const handleBlankRemove = useCallback((blankId: number) => {
|
||||
onBlankRemove(blankId);
|
||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||
}, [blanksDispatcher]);
|
||||
}, [blanksDispatcher, onBlankRemove]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlertItem } from "../Shared/Alert";
|
||||
import { BlankState } from "./FillBlanksReducer";
|
||||
import { BlankState } from "./BlanksReducer";
|
||||
|
||||
|
||||
const validateBlanks = (
|
||||
|
||||
@@ -48,7 +48,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
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);
|
||||
@@ -61,7 +60,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -206,16 +204,16 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
||||
</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>
|
||||
{(section.text.content.split("\n\n").length - 1) === local.sentences.length && (
|
||||
<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}
|
||||
|
||||
@@ -86,7 +86,6 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
onDiscard: () => {
|
||||
setLocal(exercise);
|
||||
@@ -98,7 +97,6 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -165,7 +165,6 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
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);
|
||||
@@ -176,7 +175,6 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
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);
|
||||
@@ -108,7 +107,6 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||
};
|
||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||
dispatch({ type: "REORDER_EXERCISES" })
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
setEditing(true);
|
||||
console.log("ASOJNFOAI+SHJOIPFAS");
|
||||
setLocal(handleWriteBlanksReorder(event, local));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
|
||||
const Writing: React.FC<Props> = ({ sectionId }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
const {edit } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||
const { edit } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||
const { generating, genResult, state } = useExamEditorStore(
|
||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
||||
);
|
||||
@@ -62,8 +62,9 @@ const Writing: React.FC<Props> = ({ sectionId }) => {
|
||||
if (genResult !== undefined && generating === "context") {
|
||||
setEditing(true);
|
||||
setPrompt(genResult[0].prompt);
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined }})
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
||||
sectionId,
|
||||
mode: "edit",
|
||||
onSave: () => {
|
||||
const newState = {...state} as ReadingPart;
|
||||
let newState = {...state} as ReadingPart;
|
||||
newState.text.title = title;
|
||||
newState.text.content = content;
|
||||
dispatch({type: 'UPDATE_SECTION_STATE', payload: {sectionId, update: newState}})
|
||||
@@ -45,6 +45,7 @@ const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
||||
setContent(genResult[0].text)
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "genResult", value: undefined}})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||
|
||||
|
||||
|
||||
@@ -40,17 +40,16 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
||||
useEffect(() => {
|
||||
if (genResult !== undefined && generating === "exercises") {
|
||||
const newExercises = genResult[0].exercises;
|
||||
const newState = state as ExamPart;
|
||||
newState.exercises = [...newState.exercises, ...newExercises]
|
||||
dispatch({
|
||||
type: "UPDATE_SECTION_STATE", payload: {
|
||||
sectionId, update: {
|
||||
exercises: newExercises
|
||||
exercises: [...(state as ExamPart).exercises, ...newExercises]
|
||||
}
|
||||
}
|
||||
})
|
||||
dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: undefined } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [genResult, dispatch, sectionId, currentModule]);
|
||||
|
||||
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
||||
|
||||
@@ -23,7 +23,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Fill Blanks Question #${firstWordId} - #${lastWordId}`}
|
||||
label={`Fill Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
@@ -41,7 +41,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`Write Blanks Question #${firstWordId} - #${lastWordId}`}
|
||||
label={`Write Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
@@ -59,7 +59,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`${exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"} ${firstWordId == lastWordId ? `#${firstWordId}` : `#${firstWordId} - #${lastWordId}`}`}
|
||||
label={`${exercise.variant == "ideaMatch" ? "Idea Match" : "Paragraph Match"} #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
@@ -77,7 +77,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
||||
sectionId,
|
||||
label: (
|
||||
<ExerciseLabel
|
||||
label={`True/False/Not Given #${firstWordId} - #${lastWordId}`}
|
||||
label={`True/False/Not Given #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||
preview={
|
||||
<>
|
||||
"{previewLabel(exercise.prompt)}..."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { FaEye } from "react-icons/fa";
|
||||
import { FaEye, FaFileUpload } from "react-icons/fa";
|
||||
import clsx from "clsx";
|
||||
import Select from "@/components/Low/Select";
|
||||
import Input from "@/components/Low/Input";
|
||||
@@ -18,6 +18,8 @@ interface SettingsEditorProps {
|
||||
introPresets: Option[];
|
||||
children?: ReactNode;
|
||||
canPreview: boolean;
|
||||
canSubmit: boolean;
|
||||
submitModule: () => void;
|
||||
preview: () => void;
|
||||
}
|
||||
|
||||
@@ -28,7 +30,9 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
introPresets,
|
||||
children,
|
||||
preview,
|
||||
submitModule,
|
||||
canPreview,
|
||||
canSubmit
|
||||
}) => {
|
||||
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||
@@ -76,6 +80,10 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
});
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
const submitExam = () => {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
|
||||
<div className={`w-full flex justify-center text-ielts-${module} font-bold text-xl`}>{sectionLabel} Settings</div>
|
||||
@@ -118,7 +126,19 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
||||
</div>
|
||||
</Dropdown>
|
||||
{children}
|
||||
<div className="flex flex-row justify-center mt-4">
|
||||
<div className="flex flex-row justify-between mt-4">
|
||||
<button
|
||||
className={clsx(
|
||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||
`bg-ielts-${module}/70 border border-ielts-${module} hover:bg-ielts-${module} disabled:bg-ielts-${module}/30`,
|
||||
"disabled:cursor-not-allowed disabled:text-gray-200"
|
||||
)}
|
||||
onClick={submitModule}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
<FaFileUpload className="mr-2" size={18} />
|
||||
Submit Module as Exam
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
||||
|
||||
@@ -60,6 +60,8 @@ const LevelSettings: React.FC = () => {
|
||||
introPresets={defaultPresets}
|
||||
preview={()=>{}}
|
||||
canPreview={false}
|
||||
canSubmit={false}
|
||||
submitModule={()=> {}}
|
||||
>
|
||||
<div>
|
||||
<Dropdown title="Add Exercises" className={
|
||||
|
||||
@@ -69,6 +69,8 @@ const ListeningSettings: React.FC = () => {
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={()=> {}}
|
||||
canPreview={false}
|
||||
canSubmit={false}
|
||||
submitModule={()=> {}}
|
||||
>
|
||||
<Dropdown
|
||||
title="Audio Context"
|
||||
|
||||
@@ -7,21 +7,33 @@ import ExercisePicker from "../Shared/ExercisePicker";
|
||||
import { generate } from "./Shared/Generate";
|
||||
import GenerateBtn from "./Shared/GenerateBtn";
|
||||
import useSettingsState from "../Hooks/useSettingsState";
|
||||
import { ReadingPart } from "@/interfaces/exam";
|
||||
import { ReadingExam, ReadingPart } from "@/interfaces/exam";
|
||||
import { ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||
import useExamEditorStore from "@/stores/examEditor";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const ReadingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const {
|
||||
setExam,
|
||||
setExerciseIndex,
|
||||
setPartIndex,
|
||||
setQuestionIndex,
|
||||
} = usePersistentExamStore();
|
||||
|
||||
const { currentModule, title } = useExamEditorStore();
|
||||
const {
|
||||
focusedSection,
|
||||
difficulty,
|
||||
sections,
|
||||
minTimer,
|
||||
isPrivate,
|
||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||
|
||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
||||
@@ -68,18 +80,75 @@ const ReadingSettings: React.FC = () => {
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
|
||||
const canPreview = sections.some(
|
||||
const canPreviewOrSubmit = sections.some(
|
||||
(s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0
|
||||
);
|
||||
|
||||
const submitReading = () => {
|
||||
const exam: ReadingExam = {
|
||||
parts: sections.map((s) => {
|
||||
const exercise = s.state as ReadingPart;
|
||||
return {
|
||||
...exercise,
|
||||
intro: localSettings.currentIntro,
|
||||
category: localSettings.category
|
||||
};
|
||||
}),
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "reading",
|
||||
id: title,
|
||||
type: "academic",
|
||||
variant: sections.length === 3 ? "full" : "partial",
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
axios.post(`/api/exam/reading`, exam)
|
||||
.then((result) => {
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
|
||||
})
|
||||
}
|
||||
|
||||
const preview = () => {
|
||||
setExam({
|
||||
parts: sections.map((s) => {
|
||||
const exercise = s.state as ReadingPart;
|
||||
return {
|
||||
...exercise,
|
||||
intro: localSettings.currentIntro,
|
||||
category: localSettings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "reading",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
} as ReadingExam);
|
||||
setExerciseIndex(0);
|
||||
setQuestionIndex(0);
|
||||
setPartIndex(0);
|
||||
openDetachedTab("popout?type=Exam&module=reading", router)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Passage ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="reading"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={() => { }}
|
||||
canPreview={canPreview}
|
||||
preview={preview}
|
||||
canPreview={canPreviewOrSubmit}
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitReading}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Passage"
|
||||
|
||||
@@ -87,6 +87,7 @@ const SpeakingSettings: React.FC = () => {
|
||||
}
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSettings, difficulty]);
|
||||
|
||||
const onTopicChange = useCallback((topic: string) => {
|
||||
@@ -105,6 +106,8 @@ const SpeakingSettings: React.FC = () => {
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={() => { }}
|
||||
canPreview={false}
|
||||
canSubmit={false}
|
||||
submitModule={()=> {}}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Script"
|
||||
|
||||
@@ -11,13 +11,17 @@ import useExamEditorStore from "@/stores/examEditor";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePersistentExamStore } from "@/stores/examStore";
|
||||
import openDetachedTab from "@/utils/popout";
|
||||
import { WritingExercise } from "@/interfaces/exam";
|
||||
import { WritingExam, WritingExercise } from "@/interfaces/exam";
|
||||
import { v4 } from "uuid";
|
||||
import axios from "axios";
|
||||
import { playSound } from "@/utils/sound";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const WritingSettings: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [preview, setPreview] = useState({canPreview: false, openTab: () => {}});
|
||||
const { currentModule } = useExamEditorStore();
|
||||
const [canPreviewOrSubmit, setCanPreviewOrSubmit] = useState(false);
|
||||
|
||||
const { currentModule, title } = useExamEditorStore();
|
||||
const {
|
||||
minTimer,
|
||||
difficulty,
|
||||
@@ -26,7 +30,7 @@ const WritingSettings: React.FC = () => {
|
||||
focusedSection,
|
||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||
|
||||
const states = sections.flatMap((s)=> s.state) as WritingExercise[];
|
||||
const states = sections.flatMap((s) => s.state) as WritingExercise[];
|
||||
|
||||
const {
|
||||
setExam,
|
||||
@@ -73,34 +77,73 @@ const WritingSettings: React.FC = () => {
|
||||
}, [updateLocalAndScheduleGlobal]);
|
||||
|
||||
useEffect(() => {
|
||||
const openTab = () => {
|
||||
setExam({
|
||||
isDiagnostic: false,
|
||||
minTimer,
|
||||
module: "writing",
|
||||
exercises: states.filter((s) => s.prompt && s.prompt !== ""),
|
||||
id: v4(),
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
});
|
||||
setExerciseIndex(0);
|
||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||
}
|
||||
setPreview({
|
||||
canPreview: states.some((s) => s.prompt && s.prompt !== ""),
|
||||
openTab
|
||||
})
|
||||
setCanPreviewOrSubmit(states.some((s) => s.prompt !== ""))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [states.some((s) => s.prompt !== "")])
|
||||
|
||||
const openTab = () => {
|
||||
setExam({
|
||||
exercises: sections.map((s) => {
|
||||
const exercise = s.state as WritingExercise;
|
||||
return {
|
||||
...exercise,
|
||||
intro: s.settings.currentIntro,
|
||||
category: s.settings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "writing",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
});
|
||||
setExerciseIndex(0);
|
||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||
}
|
||||
|
||||
const submitWriting = () => {
|
||||
const exam: WritingExam = {
|
||||
exercises: sections.map((s) => {
|
||||
const exercise = s.state as WritingExercise;
|
||||
return {
|
||||
...exercise,
|
||||
intro: localSettings.currentIntro,
|
||||
category: localSettings.category
|
||||
};
|
||||
}),
|
||||
minTimer,
|
||||
module: "writing",
|
||||
id: title,
|
||||
isDiagnostic: false,
|
||||
variant: undefined,
|
||||
difficulty,
|
||||
private: isPrivate,
|
||||
};
|
||||
|
||||
axios
|
||||
.post(`/api/exam/reading`, exam)
|
||||
.then((result) => {
|
||||
playSound("sent");
|
||||
toast.success(`Submitted Exam ID: ${result.data.id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEditor
|
||||
sectionLabel={`Task ${focusedSection}`}
|
||||
sectionId={focusedSection}
|
||||
module="writing"
|
||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||
preview={preview.openTab}
|
||||
canPreview={preview.canPreview}
|
||||
preview={openTab}
|
||||
canPreview={canPreviewOrSubmit}
|
||||
canSubmit={canPreviewOrSubmit}
|
||||
submitModule={submitWriting}
|
||||
>
|
||||
<Dropdown
|
||||
title="Generate Instructions"
|
||||
|
||||
@@ -111,7 +111,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
||||
exercises: data.exercises
|
||||
}]
|
||||
);
|
||||
|
||||
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "selectedExercises", value: []}})
|
||||
setPickerOpen(false);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user