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 FillBlanksWord from "./FillBlanksWord";
|
||||||
import { FaPlus } from "react-icons/fa";
|
import { FaPlus } from "react-icons/fa";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
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 useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
|
||||||
import { AlertItem } from "../../Shared/Alert";
|
import { AlertItem } from "../../Shared/Alert";
|
||||||
import validateBlanks from "../validateBlanks";
|
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: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setSelectedBlankId(null);
|
setSelectedBlankId(null);
|
||||||
@@ -103,7 +102,6 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
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(() => {
|
useEffect(() => {
|
||||||
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, blanksState.blanks, blanksState.textMode])
|
}, [answers, blanksState.blanks, blanksState.textMode])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
setEditingAlert(editing, setAlerts);
|
setEditingAlert(editing, setAlerts);
|
||||||
}, [editing])
|
}, [editing])
|
||||||
@@ -232,6 +249,7 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num
|
|||||||
module={currentModule}
|
module={currentModule}
|
||||||
showBlankBank={true}
|
showBlankBank={true}
|
||||||
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
||||||
|
onBlankRemove={handleBlankRemove}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onDiscard={handleDiscard}
|
onDiscard={handleDiscard}
|
||||||
onDelete={modeHandle}
|
onDelete={modeHandle}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect, useReducer, useState } from "react";
|
|||||||
import BlanksEditor from "..";
|
import BlanksEditor from "..";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
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 useSectionEdit from "@/components/ExamEditor/Hooks/useSectionEdit";
|
||||||
import { AlertItem } from "../../Shared/Alert";
|
import { AlertItem } from "../../Shared/Alert";
|
||||||
import validateBlanks from "../validateBlanks";
|
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: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setSelectedBlankId(null);
|
setSelectedBlankId(null);
|
||||||
@@ -99,7 +98,6 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
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(() => {
|
useEffect(() => {
|
||||||
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
validateBlanks(blanksState.blanks, answers, alerts, setAlerts);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [answers, blanksState.blanks, blanksState.textMode]);
|
}, [answers, blanksState.blanks, blanksState.textMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -216,8 +215,28 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
|||||||
solution
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<BlanksEditor
|
<BlanksEditor
|
||||||
@@ -234,6 +253,7 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }
|
|||||||
onDiscard={handleDiscard}
|
onDiscard={handleDiscard}
|
||||||
onDelete={modeHandle}
|
onDelete={modeHandle}
|
||||||
setEditing={setEditing}
|
setEditing={setEditing}
|
||||||
|
onBlankRemove={handleBlankRemove}
|
||||||
>
|
>
|
||||||
{!blanksState.textMode && selectedBlankId && (
|
{!blanksState.textMode && selectedBlankId && (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { toast } from "react-toastify";
|
|||||||
import BlanksEditor from "..";
|
import BlanksEditor from "..";
|
||||||
import { AlertItem } from "../../Shared/Alert";
|
import { AlertItem } from "../../Shared/Alert";
|
||||||
import setEditingAlert from "../../Shared/setEditingAlert";
|
import setEditingAlert from "../../Shared/setEditingAlert";
|
||||||
import { blanksReducer } from "../FillBlanksReducer";
|
import { blanksReducer } from "../BlanksReducer";
|
||||||
import { validateWriteBlanks } from "./validation";
|
import { validateWriteBlanks } from "./validation";
|
||||||
import AlternativeSolutions from "./AlternativeSolutions";
|
import AlternativeSolutions from "./AlternativeSolutions";
|
||||||
import clsx from "clsx";
|
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(() => {
|
useEffect(() => {
|
||||||
validateWriteBlanks(local.solutions, local.maxWords, setAlerts);
|
validateWriteBlanks(local.solutions, local.maxWords, setAlerts);
|
||||||
}, [local.solutions, local.maxWords]);
|
}, [local.solutions, local.maxWords]);
|
||||||
@@ -147,6 +157,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb
|
|||||||
module={currentModule}
|
module={currentModule}
|
||||||
showBlankBank={true}
|
showBlankBank={true}
|
||||||
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
onBlankSelect={(blankId) => setSelectedBlankId(blankId?.toString() || null)}
|
||||||
|
onBlankRemove={handleBlankRemove}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onDiscard={handleDiscard}
|
onDiscard={handleDiscard}
|
||||||
onDelete={modeHandle}
|
onDelete={modeHandle}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertItem } from "../../Shared/Alert";
|
import { AlertItem } from "../../Shared/Alert";
|
||||||
import { BlankState } from "../FillBlanksReducer";
|
import { BlankState } from "../BlanksReducer";
|
||||||
|
|
||||||
|
|
||||||
export const validateWriteBlanks = (
|
export const validateWriteBlanks = (
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import Alert, { AlertItem } from "../Shared/Alert";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Blank, DropZone } from "./DragNDrop";
|
import { Blank, DropZone } from "./DragNDrop";
|
||||||
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./FillBlanksReducer";
|
import { getTextSegments, BlankState, BlanksState, BlanksAction } from "./BlanksReducer";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -33,6 +33,7 @@ interface Props {
|
|||||||
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
blanksDispatcher: React.Dispatch<BlanksAction>
|
blanksDispatcher: React.Dispatch<BlanksAction>
|
||||||
onBlankSelect?: (blankId: number | null) => void;
|
onBlankSelect?: (blankId: number | null) => void;
|
||||||
|
onBlankRemove: (blankId: number) => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onDiscard: () => void;
|
onDiscard: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -51,6 +52,7 @@ const BlanksEditor: React.FC<Props> = ({
|
|||||||
alerts,
|
alerts,
|
||||||
blanksDispatcher,
|
blanksDispatcher,
|
||||||
onBlankSelect,
|
onBlankSelect,
|
||||||
|
onBlankRemove,
|
||||||
onSave,
|
onSave,
|
||||||
onDiscard,
|
onDiscard,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -99,9 +101,24 @@ const BlanksEditor: React.FC<Props> = ({
|
|||||||
const handleTextChange = useCallback(
|
const handleTextChange = useCallback(
|
||||||
(newText: string) => {
|
(newText: string) => {
|
||||||
const processedText = newText.replace(/\[(\d+)\]/g, "{{$1}}");
|
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({ type: "SET_TEXT", payload: processedText });
|
||||||
},
|
},
|
||||||
[blanksDispatcher]
|
[blanksDispatcher, state.text, onBlankRemove]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -116,8 +133,9 @@ const BlanksEditor: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBlankRemove = useCallback((blankId: number) => {
|
const handleBlankRemove = useCallback((blankId: number) => {
|
||||||
|
onBlankRemove(blankId);
|
||||||
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId });
|
||||||
}, [blanksDispatcher]);
|
}, [blanksDispatcher, onBlankRemove]);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertItem } from "../Shared/Alert";
|
import { AlertItem } from "../Shared/Alert";
|
||||||
import { BlankState } from "./FillBlanksReducer";
|
import { BlankState } from "./BlanksReducer";
|
||||||
|
|
||||||
|
|
||||||
const validateBlanks = (
|
const validateBlanks = (
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
|||||||
const newState = { ...section };
|
const newState = { ...section };
|
||||||
newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? local : ex);
|
newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? local : ex);
|
||||||
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
@@ -61,7 +60,6 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -206,7 +204,7 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
|||||||
</SortableQuestion>
|
</SortableQuestion>
|
||||||
))}
|
))}
|
||||||
</QuestionsList>
|
</QuestionsList>
|
||||||
|
{(section.text.content.split("\n\n").length - 1) === local.sentences.length && (
|
||||||
<button
|
<button
|
||||||
onClick={addHeading}
|
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"
|
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"
|
||||||
@@ -214,8 +212,8 @@ const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: nu
|
|||||||
<MdAdd size={18} />
|
<MdAdd size={18} />
|
||||||
Add New Match
|
Add New Match
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ReferenceViewer
|
<ReferenceViewer
|
||||||
headings={exercise.variant !== "ideaMatch"}
|
headings={exercise.variant !== "ideaMatch"}
|
||||||
showReference={showReference}
|
showReference={showReference}
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
@@ -98,7 +97,6 @@ const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, secti
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
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)
|
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" });
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
@@ -176,7 +175,6 @@ const MultipleChoice: React.FC<MultipleChoiceProps> = ({ exercise, sectionId, op
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
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)
|
exercises: section.exercises.map((ex) => ex.id === local.id ? local : ex)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
||||||
dispatch({ type: "REORDER_EXERCISES" })
|
|
||||||
},
|
},
|
||||||
onDiscard: () => {
|
onDiscard: () => {
|
||||||
setLocal(exercise);
|
setLocal(exercise);
|
||||||
@@ -108,7 +107,6 @@ const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> =
|
|||||||
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
exercises: section.exercises.filter((ex) => ex.id !== local.id)
|
||||||
};
|
};
|
||||||
dispatch({ type: "UPDATE_SECTION_STATE", payload: { sectionId, update: newSection } });
|
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) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
console.log("ASOJNFOAI+SHJOIPFAS");
|
||||||
setLocal(handleWriteBlanksReorder(event, local));
|
setLocal(handleWriteBlanksReorder(event, local));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
const Writing: React.FC<Props> = ({ sectionId }) => {
|
const Writing: React.FC<Props> = ({ sectionId }) => {
|
||||||
const { currentModule, dispatch } = useExamEditorStore();
|
const { currentModule, dispatch } = useExamEditorStore();
|
||||||
const {edit } = useExamEditorStore((store) => store.modules[currentModule]);
|
const { edit } = useExamEditorStore((store) => store.modules[currentModule]);
|
||||||
const { generating, genResult, state } = useExamEditorStore(
|
const { generating, genResult, state } = useExamEditorStore(
|
||||||
(state) => state.modules[currentModule].sections.find((section) => section.sectionId === sectionId)!
|
(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") {
|
if (genResult !== undefined && generating === "context") {
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setPrompt(genResult[0].prompt);
|
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]);
|
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
|||||||
sectionId,
|
sectionId,
|
||||||
mode: "edit",
|
mode: "edit",
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
const newState = {...state} as ReadingPart;
|
let newState = {...state} as ReadingPart;
|
||||||
newState.text.title = title;
|
newState.text.title = title;
|
||||||
newState.text.content = content;
|
newState.text.content = content;
|
||||||
dispatch({type: 'UPDATE_SECTION_STATE', payload: {sectionId, update: newState}})
|
dispatch({type: 'UPDATE_SECTION_STATE', payload: {sectionId, update: newState}})
|
||||||
@@ -45,6 +45,7 @@ const ReadingContext: React.FC<{sectionId: number;}> = ({sectionId}) => {
|
|||||||
setContent(genResult[0].text)
|
setContent(genResult[0].text)
|
||||||
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]);
|
}, [genResult, dispatch, sectionId, setEditing, currentModule]);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,17 +40,16 @@ const SectionExercises: React.FC<{ sectionId: number; }> = ({ sectionId }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (genResult !== undefined && generating === "exercises") {
|
if (genResult !== undefined && generating === "exercises") {
|
||||||
const newExercises = genResult[0].exercises;
|
const newExercises = genResult[0].exercises;
|
||||||
const newState = state as ExamPart;
|
|
||||||
newState.exercises = [...newState.exercises, ...newExercises]
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "UPDATE_SECTION_STATE", payload: {
|
type: "UPDATE_SECTION_STATE", payload: {
|
||||||
sectionId, update: {
|
sectionId, update: {
|
||||||
exercises: newExercises
|
exercises: [...(state as ExamPart).exercises, ...newExercises]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
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, currentModule]);
|
}, [genResult, dispatch, sectionId, currentModule]);
|
||||||
|
|
||||||
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
const currentSection = sections.find((s) => s.sectionId === sectionId)!;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
|||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<ExerciseLabel
|
||||||
label={`Fill Blanks Question #${firstWordId} - #${lastWordId}`}
|
label={`Fill Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||||
preview={
|
preview={
|
||||||
<>
|
<>
|
||||||
"{previewLabel(exercise.prompt)}..."
|
"{previewLabel(exercise.prompt)}..."
|
||||||
@@ -41,7 +41,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
|||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<ExerciseLabel
|
||||||
label={`Write Blanks Question #${firstWordId} - #${lastWordId}`}
|
label={`Write Blanks Question #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||||
preview={
|
preview={
|
||||||
<>
|
<>
|
||||||
"{previewLabel(exercise.prompt)}..."
|
"{previewLabel(exercise.prompt)}..."
|
||||||
@@ -59,7 +59,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
|||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<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={
|
preview={
|
||||||
<>
|
<>
|
||||||
"{previewLabel(exercise.prompt)}..."
|
"{previewLabel(exercise.prompt)}..."
|
||||||
@@ -77,7 +77,7 @@ const getExerciseItems = (exercises: ReadingExercise[], sectionId: number): Exer
|
|||||||
sectionId,
|
sectionId,
|
||||||
label: (
|
label: (
|
||||||
<ExerciseLabel
|
<ExerciseLabel
|
||||||
label={`True/False/Not Given #${firstWordId} - #${lastWordId}`}
|
label={`True/False/Not Given #${firstWordId} ${firstWordId === lastWordId ? '' : `- #${lastWordId}`}`}
|
||||||
preview={
|
preview={
|
||||||
<>
|
<>
|
||||||
"{previewLabel(exercise.prompt)}..."
|
"{previewLabel(exercise.prompt)}..."
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { ReactNode, useCallback, useEffect, useMemo, useState, useRef } from "react";
|
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 clsx from "clsx";
|
||||||
import Select from "@/components/Low/Select";
|
import Select from "@/components/Low/Select";
|
||||||
import Input from "@/components/Low/Input";
|
import Input from "@/components/Low/Input";
|
||||||
@@ -18,6 +18,8 @@ interface SettingsEditorProps {
|
|||||||
introPresets: Option[];
|
introPresets: Option[];
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
canPreview: boolean;
|
canPreview: boolean;
|
||||||
|
canSubmit: boolean;
|
||||||
|
submitModule: () => void;
|
||||||
preview: () => void;
|
preview: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +30,9 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
introPresets,
|
introPresets,
|
||||||
children,
|
children,
|
||||||
preview,
|
preview,
|
||||||
|
submitModule,
|
||||||
canPreview,
|
canPreview,
|
||||||
|
canSubmit
|
||||||
}) => {
|
}) => {
|
||||||
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
const examLabel = useExamEditorStore((state) => state.modules[module].examLabel) || '';
|
||||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SectionSettings>(
|
||||||
@@ -76,6 +80,10 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
});
|
});
|
||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
|
const submitExam = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-8 border bg-ielts-${module}/20 rounded-3xl p-8 w-1/3 h-fit`}>
|
<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>
|
<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>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{children}
|
{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
|
<button
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center px-4 py-2 text-white rounded-xl transition-colors duration-300",
|
"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}
|
introPresets={defaultPresets}
|
||||||
preview={()=>{}}
|
preview={()=>{}}
|
||||||
canPreview={false}
|
canPreview={false}
|
||||||
|
canSubmit={false}
|
||||||
|
submitModule={()=> {}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Dropdown title="Add Exercises" className={
|
<Dropdown title="Add Exercises" className={
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ const ListeningSettings: React.FC = () => {
|
|||||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||||
preview={()=> {}}
|
preview={()=> {}}
|
||||||
canPreview={false}
|
canPreview={false}
|
||||||
|
canSubmit={false}
|
||||||
|
submitModule={()=> {}}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Audio Context"
|
title="Audio Context"
|
||||||
|
|||||||
@@ -7,21 +7,33 @@ import ExercisePicker from "../Shared/ExercisePicker";
|
|||||||
import { generate } from "./Shared/Generate";
|
import { generate } from "./Shared/Generate";
|
||||||
import GenerateBtn from "./Shared/GenerateBtn";
|
import GenerateBtn from "./Shared/GenerateBtn";
|
||||||
import useSettingsState from "../Hooks/useSettingsState";
|
import useSettingsState from "../Hooks/useSettingsState";
|
||||||
import { ReadingPart } from "@/interfaces/exam";
|
import { ReadingExam, ReadingPart } from "@/interfaces/exam";
|
||||||
import { ReadingSectionSettings } from "@/stores/examEditor/types";
|
import { ReadingSectionSettings } from "@/stores/examEditor/types";
|
||||||
import useExamEditorStore from "@/stores/examEditor";
|
import useExamEditorStore from "@/stores/examEditor";
|
||||||
import openDetachedTab from "@/utils/popout";
|
import openDetachedTab from "@/utils/popout";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { usePersistentExamStore } from "@/stores/examStore";
|
import { usePersistentExamStore } from "@/stores/examStore";
|
||||||
|
import axios from "axios";
|
||||||
|
import { playSound } from "@/utils/sound";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
const ReadingSettings: React.FC = () => {
|
const ReadingSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { currentModule } = useExamEditorStore();
|
const {
|
||||||
|
setExam,
|
||||||
|
setExerciseIndex,
|
||||||
|
setPartIndex,
|
||||||
|
setQuestionIndex,
|
||||||
|
} = usePersistentExamStore();
|
||||||
|
|
||||||
|
const { currentModule, title } = useExamEditorStore();
|
||||||
const {
|
const {
|
||||||
focusedSection,
|
focusedSection,
|
||||||
difficulty,
|
difficulty,
|
||||||
sections,
|
sections,
|
||||||
|
minTimer,
|
||||||
|
isPrivate,
|
||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<ReadingSectionSettings>(
|
||||||
@@ -68,18 +80,75 @@ const ReadingSettings: React.FC = () => {
|
|||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
|
|
||||||
const canPreview = sections.some(
|
const canPreviewOrSubmit = sections.some(
|
||||||
(s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0
|
(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 (
|
return (
|
||||||
<SettingsEditor
|
<SettingsEditor
|
||||||
sectionLabel={`Passage ${focusedSection}`}
|
sectionLabel={`Passage ${focusedSection}`}
|
||||||
sectionId={focusedSection}
|
sectionId={focusedSection}
|
||||||
module="reading"
|
module="reading"
|
||||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||||
preview={() => { }}
|
preview={preview}
|
||||||
canPreview={canPreview}
|
canPreview={canPreviewOrSubmit}
|
||||||
|
canSubmit={canPreviewOrSubmit}
|
||||||
|
submitModule={submitReading}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Generate Passage"
|
title="Generate Passage"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const SpeakingSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [localSettings, difficulty]);
|
}, [localSettings, difficulty]);
|
||||||
|
|
||||||
const onTopicChange = useCallback((topic: string) => {
|
const onTopicChange = useCallback((topic: string) => {
|
||||||
@@ -105,6 +106,8 @@ const SpeakingSettings: React.FC = () => {
|
|||||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||||
preview={() => { }}
|
preview={() => { }}
|
||||||
canPreview={false}
|
canPreview={false}
|
||||||
|
canSubmit={false}
|
||||||
|
submitModule={()=> {}}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Generate Script"
|
title="Generate Script"
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ import useExamEditorStore from "@/stores/examEditor";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { usePersistentExamStore } from "@/stores/examStore";
|
import { usePersistentExamStore } from "@/stores/examStore";
|
||||||
import openDetachedTab from "@/utils/popout";
|
import openDetachedTab from "@/utils/popout";
|
||||||
import { WritingExercise } from "@/interfaces/exam";
|
import { WritingExam, WritingExercise } from "@/interfaces/exam";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
|
import axios from "axios";
|
||||||
|
import { playSound } from "@/utils/sound";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
const WritingSettings: React.FC = () => {
|
const WritingSettings: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [preview, setPreview] = useState({canPreview: false, openTab: () => {}});
|
const [canPreviewOrSubmit, setCanPreviewOrSubmit] = useState(false);
|
||||||
const { currentModule } = useExamEditorStore();
|
|
||||||
|
const { currentModule, title } = useExamEditorStore();
|
||||||
const {
|
const {
|
||||||
minTimer,
|
minTimer,
|
||||||
difficulty,
|
difficulty,
|
||||||
@@ -26,7 +30,7 @@ const WritingSettings: React.FC = () => {
|
|||||||
focusedSection,
|
focusedSection,
|
||||||
} = useExamEditorStore((store) => store.modules["writing"]);
|
} = useExamEditorStore((store) => store.modules["writing"]);
|
||||||
|
|
||||||
const states = sections.flatMap((s)=> s.state) as WritingExercise[];
|
const states = sections.flatMap((s) => s.state) as WritingExercise[];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setExam,
|
setExam,
|
||||||
@@ -73,13 +77,24 @@ const WritingSettings: React.FC = () => {
|
|||||||
}, [updateLocalAndScheduleGlobal]);
|
}, [updateLocalAndScheduleGlobal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setCanPreviewOrSubmit(states.some((s) => s.prompt !== ""))
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [states.some((s) => s.prompt !== "")])
|
||||||
|
|
||||||
const openTab = () => {
|
const openTab = () => {
|
||||||
setExam({
|
setExam({
|
||||||
isDiagnostic: false,
|
exercises: sections.map((s) => {
|
||||||
|
const exercise = s.state as WritingExercise;
|
||||||
|
return {
|
||||||
|
...exercise,
|
||||||
|
intro: s.settings.currentIntro,
|
||||||
|
category: s.settings.category
|
||||||
|
};
|
||||||
|
}),
|
||||||
minTimer,
|
minTimer,
|
||||||
module: "writing",
|
module: "writing",
|
||||||
exercises: states.filter((s) => s.prompt && s.prompt !== ""),
|
id: title,
|
||||||
id: v4(),
|
isDiagnostic: false,
|
||||||
variant: undefined,
|
variant: undefined,
|
||||||
difficulty,
|
difficulty,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
@@ -87,11 +102,37 @@ const WritingSettings: React.FC = () => {
|
|||||||
setExerciseIndex(0);
|
setExerciseIndex(0);
|
||||||
openDetachedTab("popout?type=Exam&module=writing", router)
|
openDetachedTab("popout?type=Exam&module=writing", router)
|
||||||
}
|
}
|
||||||
setPreview({
|
|
||||||
canPreview: states.some((s) => s.prompt && s.prompt !== ""),
|
const submitWriting = () => {
|
||||||
openTab
|
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}`);
|
||||||
})
|
})
|
||||||
}, [states.some((s) => s.prompt !== "")])
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(error.response.data.error || "Something went wrong while submitting, please try again later.");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsEditor
|
<SettingsEditor
|
||||||
@@ -99,8 +140,10 @@ const WritingSettings: React.FC = () => {
|
|||||||
sectionId={focusedSection}
|
sectionId={focusedSection}
|
||||||
module="writing"
|
module="writing"
|
||||||
introPresets={[defaultPresets[focusedSection - 1]]}
|
introPresets={[defaultPresets[focusedSection - 1]]}
|
||||||
preview={preview.openTab}
|
preview={openTab}
|
||||||
canPreview={preview.canPreview}
|
canPreview={canPreviewOrSubmit}
|
||||||
|
canSubmit={canPreviewOrSubmit}
|
||||||
|
submitModule={submitWriting}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
title="Generate Instructions"
|
title="Generate Instructions"
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ const ExercisePicker: React.FC<ExercisePickerProps> = ({
|
|||||||
exercises: data.exercises
|
exercises: data.exercises
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
dispatch({type: "UPDATE_SECTION_SINGLE_FIELD", payload: {sectionId, module: currentModule, field: "selectedExercises", value: []}})
|
||||||
setPickerOpen(false);
|
setPickerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import {MultipleChoiceExercise, ReadingExam, ReadingPart, UserSolution} from "@/interfaces/exam";
|
import { MultipleChoiceExercise, ReadingExam, ReadingPart, UserSolution } from "@/interfaces/exam";
|
||||||
import {Fragment, useEffect, useState} from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import Icon from "@mdi/react";
|
|
||||||
import {mdiArrowRight, mdiNotebook} from "@mdi/js";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import { convertCamelCaseToReadable } from "@/utils/string";
|
||||||
import {convertCamelCaseToReadable} from "@/utils/string";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {Dialog, Transition} from "@headlessui/react";
|
import { renderExercise } from "@/components/Exercises";
|
||||||
import {renderExercise} from "@/components/Exercises";
|
import { renderSolution } from "@/components/Solutions";
|
||||||
import {renderSolution} from "@/components/Solutions";
|
import { BsChevronDown, BsChevronUp } from "react-icons/bs";
|
||||||
import {Panel} from "primereact/panel";
|
|
||||||
import {Steps} from "primereact/steps";
|
|
||||||
import {BsAlarm, BsBook, BsChevronDown, BsChevronUp, BsClock, BsStopwatch} from "react-icons/bs";
|
|
||||||
import ProgressBar from "@/components/Low/ProgressBar";
|
|
||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
import {Divider} from "primereact/divider";
|
|
||||||
import Button from "@/components/Low/Button";
|
import Button from "@/components/Low/Button";
|
||||||
import BlankQuestionsModal from "@/components/QuestionsModal";
|
import BlankQuestionsModal from "@/components/QuestionsModal";
|
||||||
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
import useExamStore, { usePersistentExamStore } from "@/stores/examStore";
|
||||||
import {defaultUserSolutions} from "@/utils/exams";
|
import { countExercises } from "@/utils/moduleUtils";
|
||||||
import {countExercises} from "@/utils/moduleUtils";
|
import PartDivider from "./Navigation/SectionDivider";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
exam: ReadingExam;
|
exam: ReadingExam;
|
||||||
@@ -29,7 +22,7 @@ interface Props {
|
|||||||
|
|
||||||
const numberToLetter = (number: number) => (number + 9).toString(36).toUpperCase();
|
const numberToLetter = (number: number) => (number + 9).toString(36).toUpperCase();
|
||||||
|
|
||||||
function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: string; content: string; onClose: () => void}) {
|
function TextModal({ isOpen, title, content, onClose }: { isOpen: boolean; title: string; content: string; onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||||
@@ -83,7 +76,7 @@ function TextModal({isOpen, title, content, onClose}: {isOpen: boolean; title: s
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: string}) {
|
function TextComponent({ part, exerciseType }: { part: ReadingPart; exerciseType: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<h3 className="text-xl font-semibold">{part.text.title}</h3>
|
<h3 className="text-xl font-semibold">{part.text.title}</h3>
|
||||||
@@ -106,13 +99,18 @@ function TextComponent({part, exerciseType}: {part: ReadingPart; exerciseType: s
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Reading({exam, showSolutions = false, preview = false, onFinish}: Props) {
|
export default function Reading({ exam, showSolutions = false, preview = false, onFinish }: Props) {
|
||||||
|
const readingBgColor = "bg-ielts-reading-light";
|
||||||
|
|
||||||
const [showTextModal, setShowTextModal] = useState(false);
|
const [showTextModal, setShowTextModal] = useState(false);
|
||||||
const [showBlankModal, setShowBlankModal] = useState(false);
|
const [showBlankModal, setShowBlankModal] = useState(false);
|
||||||
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{id: string; amount: number}[]>([]);
|
const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]);
|
||||||
const [isTextMinimized, setIsTextMinimzed] = useState(false);
|
const [isTextMinimized, setIsTextMinimzed] = useState(false);
|
||||||
const [exerciseType, setExerciseType] = useState("");
|
const [exerciseType, setExerciseType] = useState("");
|
||||||
|
|
||||||
|
const [seenParts, setSeenParts] = useState<Set<number>>(new Set(showSolutions ? exam.parts.map((_, index) => index) : []));
|
||||||
|
const [showPartDivider, setShowPartDivider] = useState<boolean>(typeof exam.parts[0].intro === "string" && exam.parts[0].intro !== "");
|
||||||
|
|
||||||
const examState = useExamStore((state) => state);
|
const examState = useExamStore((state) => state);
|
||||||
const persistentExamState = usePersistentExamStore((state) => state);
|
const persistentExamState = usePersistentExamStore((state) => state);
|
||||||
|
|
||||||
@@ -133,6 +131,14 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
|
|
||||||
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(exerciseIndex)) {
|
||||||
|
setShowPartDivider(true);
|
||||||
|
setBgColor(readingBgColor);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [partIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showSolutions) setExerciseIndex(-1);
|
if (showSolutions) setExerciseIndex(-1);
|
||||||
}, [setExerciseIndex, showSolutions]);
|
}, [setExerciseIndex, showSolutions]);
|
||||||
@@ -148,7 +154,7 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice];
|
previousMultipleChoice = [...previousMultipleChoice, ...partMultipleChoice];
|
||||||
}
|
}
|
||||||
|
|
||||||
setMultipleChoicesDone(previousMultipleChoice.map((x) => ({id: x.id, amount: x.questions.length - 1})));
|
setMultipleChoicesDone(previousMultipleChoice.map((x) => ({ id: x.id, amount: x.questions.length - 1 })));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -184,12 +190,12 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
const nextExercise = (solution?: UserSolution) => {
|
const nextExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
if (solution) {
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]);
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "reading", exam: exam.id }]);
|
||||||
}
|
}
|
||||||
if (storeQuestionIndex > 0) {
|
if (storeQuestionIndex > 0) {
|
||||||
const exercise = getExercise();
|
const exercise = getExercise();
|
||||||
setExerciseType(exercise.type);
|
setExerciseType(exercise.type);
|
||||||
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), {id: exercise.id, amount: storeQuestionIndex}]);
|
setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== exercise.id), { id: exercise.id, amount: storeQuestionIndex }]);
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -219,7 +225,7 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
setHasExamEnded(false);
|
setHasExamEnded(false);
|
||||||
|
|
||||||
if (solution) {
|
if (solution) {
|
||||||
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]);
|
onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "reading", exam: exam.id }]);
|
||||||
} else {
|
} else {
|
||||||
onFinish(userSolutions);
|
onFinish(userSolutions);
|
||||||
}
|
}
|
||||||
@@ -228,7 +234,7 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
const previousExercise = (solution?: UserSolution) => {
|
const previousExercise = (solution?: UserSolution) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
if (solution) {
|
if (solution) {
|
||||||
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]);
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "reading", exam: exam.id }]);
|
||||||
}
|
}
|
||||||
setStoreQuestionIndex(0);
|
setStoreQuestionIndex(0);
|
||||||
|
|
||||||
@@ -297,6 +303,16 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{(showPartDivider) ?
|
||||||
|
<PartDivider
|
||||||
|
module="reading"
|
||||||
|
sectionLabel="Part"
|
||||||
|
defaultTitle="Reading exam"
|
||||||
|
section={exam.parts[partIndex]}
|
||||||
|
sectionIndex={partIndex}
|
||||||
|
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
|
||||||
|
/> : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full w-full gap-8">
|
<div className="flex flex-col h-full w-full gap-8">
|
||||||
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
|
||||||
@@ -363,5 +379,7 @@ export default function Reading({exam, showSolutions = false, preview = false, o
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ export const rootReducer = (
|
|||||||
state: ExamEditorStore,
|
state: ExamEditorStore,
|
||||||
action: Action
|
action: Action
|
||||||
): Partial<ExamEditorStore> => {
|
): Partial<ExamEditorStore> => {
|
||||||
|
console.log(action.type);
|
||||||
|
|
||||||
if (MODULE_ACTIONS.includes(action.type as any)) {
|
if (MODULE_ACTIONS.includes(action.type as any)) {
|
||||||
if (action.type === "REORDER_EXERCISES" && "payload" in action && "event" in action.payload) {
|
if (action.type === "REORDER_EXERCISES") {
|
||||||
const updatedState = sectionReducer(state, action as SectionActions);
|
const updatedState = sectionReducer(state, action as SectionActions);
|
||||||
if (!updatedState.modules) return state;
|
if (!updatedState.modules) return state;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { defaultSectionSettings } from "../defaults";
|
import { defaultSectionSettings } from "../defaults";
|
||||||
import { reorderExercises } from "../reorder/global";
|
import { reorderModule } from "../reorder/global";
|
||||||
import ExamEditorStore, { ModuleState } from "../types";
|
import ExamEditorStore, { ModuleState } from "../types";
|
||||||
|
|
||||||
export type ModuleActions =
|
export type ModuleActions =
|
||||||
@@ -97,7 +97,7 @@ export const moduleReducer = (
|
|||||||
...state,
|
...state,
|
||||||
modules: {
|
modules: {
|
||||||
...state.modules,
|
...state.modules,
|
||||||
[currentModule]: reorderExercises(currentModuleState)
|
[currentModule]: reorderModule(currentModuleState)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from "@/interfaces";
|
|||||||
import ExamEditorStore, { Generating, ReadingSectionSettings, Section, SectionSettings, SectionState } from "../types";
|
import ExamEditorStore, { Generating, ReadingSectionSettings, Section, SectionSettings, SectionState } from "../types";
|
||||||
import { DragEndEvent } from "@dnd-kit/core";
|
import { DragEndEvent } from "@dnd-kit/core";
|
||||||
import { LevelPart, ListeningPart, ReadingPart } from "@/interfaces/exam";
|
import { LevelPart, ListeningPart, ReadingPart } from "@/interfaces/exam";
|
||||||
|
import { reorderSection } from "../reorder/global";
|
||||||
|
|
||||||
export type SectionActions =
|
export type SectionActions =
|
||||||
| { type: 'UPDATE_SECTION_SINGLE_FIELD'; payload: { module: Module; sectionId: number; field: string; value: any } }
|
| { type: 'UPDATE_SECTION_SINGLE_FIELD'; payload: { module: Module; sectionId: number; field: string; value: any } }
|
||||||
@@ -80,7 +81,7 @@ export const sectionReducer = (
|
|||||||
...modules[currentModule],
|
...modules[currentModule],
|
||||||
sections: sections.map(section =>
|
sections: sections.map(section =>
|
||||||
section.sectionId === sectionId
|
section.sectionId === sectionId
|
||||||
? { ...section, state: {...section.state, ...updatedState} }
|
? { ...section, state: { ...section.state, ...updatedState } }
|
||||||
: section
|
: section
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,10 +94,18 @@ export const sectionReducer = (
|
|||||||
const oldIndex = active.id as number;
|
const oldIndex = active.id as number;
|
||||||
const newIndex = over.id as number;
|
const newIndex = over.id as number;
|
||||||
|
|
||||||
const currentSectionState = sections.find((s) => s.sectionId = sectionId)!.state as ReadingPart | ListeningPart | LevelPart;
|
const currentSectionState = sections.find((s) => s.sectionId === sectionId)!.state as ReadingPart | ListeningPart | LevelPart;
|
||||||
|
const exercises = [...currentSectionState.exercises];
|
||||||
|
const [removed] = exercises.splice(oldIndex, 1);
|
||||||
|
exercises.splice(newIndex, 0, removed);
|
||||||
|
|
||||||
|
const { exercises: reorderedExercises } = reorderSection(exercises, 1);
|
||||||
|
|
||||||
|
const newSectionState = {
|
||||||
|
...currentSectionState,
|
||||||
|
exercises: reorderedExercises
|
||||||
|
};
|
||||||
|
|
||||||
const [removed] = currentSectionState.exercises.splice(oldIndex, 1);
|
|
||||||
currentSectionState.exercises.splice(newIndex, 0, removed);
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
modules: {
|
modules: {
|
||||||
@@ -105,7 +114,7 @@ export const sectionReducer = (
|
|||||||
...modules[currentModule],
|
...modules[currentModule],
|
||||||
sections: sections.map(section =>
|
sections: sections.map(section =>
|
||||||
section.sectionId === sectionId
|
section.sectionId === sectionId
|
||||||
? { ...section, state: currentSectionState }
|
? { ...section, state: newSectionState }
|
||||||
: section
|
: section
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { FillBlanksExercise, LevelPart, ListeningPart, MatchSentencesExercise, MultipleChoiceExercise, ReadingPart, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
|
import { Exercise, FillBlanksExercise, LevelPart, ListeningPart, MatchSentencesExercise, MultipleChoiceExercise, ReadingPart, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
|
||||||
import { ModuleState } from "../types";
|
import { ModuleState } from "../types";
|
||||||
import ReorderResult from "./types";
|
import ReorderResult from "./types";
|
||||||
|
|
||||||
const reorderFillBlanks = (exercise: FillBlanksExercise, startId: number): ReorderResult<FillBlanksExercise> => {
|
const reorderFillBlanks = (exercise: FillBlanksExercise, startId: number): ReorderResult<FillBlanksExercise> => {
|
||||||
|
console.log();
|
||||||
const newSolutions = exercise.solutions
|
const newSolutions = exercise.solutions
|
||||||
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
|
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
|
||||||
.map((solution, index) => ({
|
.map((solution, index) => ({
|
||||||
@@ -48,33 +49,39 @@ const reorderFillBlanks = (exercise: FillBlanksExercise, startId: number): Reord
|
|||||||
words: newWords,
|
words: newWords,
|
||||||
userSolutions: newUserSolutions
|
userSolutions: newUserSolutions
|
||||||
},
|
},
|
||||||
lastId: startId + newSolutions.length - 1
|
lastId: startId + newSolutions.length
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderWriteBlanks = (exercise: WriteBlanksExercise, startId: number): ReorderResult<WriteBlanksExercise> => {
|
const reorderWriteBlanks = (exercise: WriteBlanksExercise, startId: number): ReorderResult<WriteBlanksExercise> => {
|
||||||
const newSolutions = exercise.solutions
|
const oldIds = exercise.solutions.map(s => s.id);
|
||||||
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
|
const newIds = oldIds.map((_, index) => (startId + index).toString());
|
||||||
.map((solution, index) => ({
|
|
||||||
...solution,
|
const newSolutions = exercise.solutions.map((solution, index) => ({
|
||||||
id: (startId + index).toString()
|
id: newIds[index],
|
||||||
|
solution: [...solution.solution]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
let newText = exercise.text;
|
||||||
|
oldIds.forEach((oldId, index) => {
|
||||||
|
newText = newText.replace(
|
||||||
|
`{{${oldId}}}`,
|
||||||
|
`{{${newIds[index]}}}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {
|
||||||
exercise: {
|
exercise: {
|
||||||
...exercise,
|
...exercise,
|
||||||
solutions: newSolutions,
|
solutions: newSolutions,
|
||||||
text: newSolutions.reduce((text, solution, index) => {
|
text: newText
|
||||||
return text.replace(
|
|
||||||
new RegExp(`\\{\\{${solution.id}\\}\\}`),
|
|
||||||
`{{${startId + index}}}`
|
|
||||||
);
|
|
||||||
}, exercise.text)
|
|
||||||
},
|
},
|
||||||
lastId: startId + newSolutions.length
|
lastId: startId + newSolutions.length
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const reorderTrueFalse = (exercise: TrueFalseExercise, startId: number): ReorderResult<TrueFalseExercise> => {
|
const reorderTrueFalse = (exercise: TrueFalseExercise, startId: number): ReorderResult<TrueFalseExercise> => {
|
||||||
const newQuestions = exercise.questions
|
const newQuestions = exercise.questions
|
||||||
@@ -129,45 +136,63 @@ const reorderMultipleChoice = (exercise: MultipleChoiceExercise, startId: number
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const reorderSection = (exercises: Exercise[], startId: number): { exercises: Exercise[], lastId: number } => {
|
||||||
|
let currentId = startId;
|
||||||
const reorderExercises = (moduleState: ModuleState) => {
|
const reorderedExercises = exercises.map(exercise => {
|
||||||
let currentId = 1;
|
|
||||||
|
|
||||||
const reorderedSections = moduleState.sections.map(section => {
|
|
||||||
const currentSection = section.state as ReadingPart | ListeningPart |LevelPart;
|
|
||||||
const reorderedExercises = currentSection.exercises.map(exercise => {
|
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case 'fillBlanks':
|
case 'fillBlanks':
|
||||||
|
console.log("Reordering FillBlanks");
|
||||||
result = reorderFillBlanks(exercise, currentId);
|
result = reorderFillBlanks(exercise, currentId);
|
||||||
currentId = result.lastId;
|
currentId = result.lastId;
|
||||||
return result.exercise;
|
return result.exercise;
|
||||||
|
|
||||||
case 'writeBlanks':
|
case 'writeBlanks':
|
||||||
result = reorderWriteBlanks(exercise, currentId);
|
result = reorderWriteBlanks(exercise, currentId);
|
||||||
currentId = result.lastId;
|
currentId = result.lastId;
|
||||||
return result.exercise;
|
return result.exercise;
|
||||||
|
|
||||||
case 'trueFalse':
|
case 'trueFalse':
|
||||||
result = reorderTrueFalse(exercise, currentId);
|
result = reorderTrueFalse(exercise, currentId);
|
||||||
currentId = result.lastId;
|
currentId = result.lastId;
|
||||||
return result.exercise;
|
return result.exercise;
|
||||||
|
|
||||||
case 'matchSentences':
|
case 'matchSentences':
|
||||||
result = reorderMatchSentences(exercise, currentId);
|
result = reorderMatchSentences(exercise, currentId);
|
||||||
currentId = result.lastId;
|
currentId = result.lastId;
|
||||||
return result.exercise;
|
return result.exercise;
|
||||||
|
|
||||||
case 'multipleChoice':
|
case 'multipleChoice':
|
||||||
result = reorderMultipleChoice(exercise, currentId);
|
result = reorderMultipleChoice(exercise, currentId);
|
||||||
currentId = result.lastId
|
currentId = result.lastId;
|
||||||
return result.exercise;
|
return result.exercise;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return exercise;
|
return exercise;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
exercises: reorderedExercises,
|
||||||
|
lastId: currentId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const reorderModule = (moduleState: ModuleState) => {
|
||||||
|
let currentId = 1;
|
||||||
|
const reorderedSections = moduleState.sections.map(section => {
|
||||||
|
const currentSection = section.state as ReadingPart | ListeningPart | LevelPart;
|
||||||
|
console.log(currentSection.exercises);
|
||||||
|
const result = reorderSection(currentSection.exercises, currentId);
|
||||||
|
currentId = result.lastId;
|
||||||
|
console.log(result);
|
||||||
return {
|
return {
|
||||||
...section,
|
...section,
|
||||||
state: {
|
state: {
|
||||||
...currentSection,
|
...currentSection,
|
||||||
exercises: reorderedExercises
|
exercises: result.exercises
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -176,13 +201,14 @@ const reorderExercises = (moduleState: ModuleState) => {
|
|||||||
...moduleState,
|
...moduleState,
|
||||||
sections: reorderedSections
|
sections: reorderedSections
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
reorderFillBlanks,
|
reorderFillBlanks,
|
||||||
reorderWriteBlanks,
|
reorderWriteBlanks,
|
||||||
reorderTrueFalse,
|
reorderTrueFalse,
|
||||||
reorderMatchSentences,
|
reorderMatchSentences,
|
||||||
reorderExercises,
|
reorderSection,
|
||||||
|
reorderModule,
|
||||||
reorderMultipleChoice,
|
reorderMultipleChoice,
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user