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:
Carlos-Mesquita
2024-11-05 17:53:55 +00:00
parent 15c9c4d4bd
commit ffa2045a2d
28 changed files with 519 additions and 264 deletions

View File

@@ -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}

View File

@@ -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">

View File

@@ -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}

View File

@@ -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 = (

View File

@@ -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, {

View File

@@ -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 = (

View File

@@ -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,16 +204,16 @@ 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"
> >
<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}

View File

@@ -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" });
}, },
}); });

View File

@@ -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" });
}, },
}); });

View File

@@ -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" })
}, },
}); });

View File

@@ -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));
} }

View File

@@ -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(() => {

View File

@@ -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]);

View File

@@ -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)!;

View File

@@ -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={
<> <>
&quot;{previewLabel(exercise.prompt)}...&quot; &quot;{previewLabel(exercise.prompt)}...&quot;
@@ -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={
<> <>
&quot;{previewLabel(exercise.prompt)}...&quot; &quot;{previewLabel(exercise.prompt)}...&quot;
@@ -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={
<> <>
&quot;{previewLabel(exercise.prompt)}...&quot; &quot;{previewLabel(exercise.prompt)}...&quot;
@@ -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={
<> <>
&quot;{previewLabel(exercise.prompt)}...&quot; &quot;{previewLabel(exercise.prompt)}...&quot;

View File

@@ -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",

View File

@@ -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={

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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,34 +77,73 @@ const WritingSettings: React.FC = () => {
}, [updateLocalAndScheduleGlobal]); }, [updateLocalAndScheduleGlobal]);
useEffect(() => { useEffect(() => {
const openTab = () => { setCanPreviewOrSubmit(states.some((s) => s.prompt !== ""))
setExam({ // eslint-disable-next-line react-hooks/exhaustive-deps
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
})
}, [states.some((s) => s.prompt !== "")]) }, [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 ( return (
<SettingsEditor <SettingsEditor
sectionLabel={`Task ${focusedSection}`} sectionLabel={`Task ${focusedSection}`}
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"

View File

@@ -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);
}; };

View File

@@ -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,12 +99,17 @@ 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);
@@ -298,70 +304,82 @@ export default function Reading({exam, showSolutions = false, preview = false, o
return ( return (
<> <>
<div className="flex flex-col h-full w-full gap-8"> {(showPartDivider) ?
<BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} /> <PartDivider
{partIndex > -1 && <TextModal {...exam.parts[partIndex].text} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />}
<ModuleTitle
minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()}
module="reading" module="reading"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))} sectionLabel="Part"
disableTimer={showSolutions} defaultTitle="Reading exam"
label={exerciseIndex === -1 ? undefined : convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)} section={exam.parts[partIndex]}
/> sectionIndex={partIndex}
<div onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts((prev) => new Set(prev).add(exerciseIndex)) }}
className={clsx( /> : (
"mb-20 w-full", <>
exerciseIndex > -1 && !isTextMinimized && "grid grid-cols-2 gap-4", <div className="flex flex-col h-full w-full gap-8">
exerciseIndex > -1 && isTextMinimized && "flex flex-col gap-2", <BlankQuestionsModal isOpen={showBlankModal} onClose={confirmFinishModule} />
)}> {partIndex > -1 && <TextModal {...exam.parts[partIndex].text} isOpen={showTextModal} onClose={() => setShowTextModal(false)} />}
{partIndex > -1 && renderText()} <ModuleTitle
minTimer={exam.minTimer}
exerciseIndex={calculateExerciseIndex()}
module="reading"
totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))}
disableTimer={showSolutions}
label={exerciseIndex === -1 ? undefined : convertCamelCaseToReadable(exam.parts[partIndex].exercises[exerciseIndex].type)}
/>
<div
className={clsx(
"mb-20 w-full",
exerciseIndex > -1 && !isTextMinimized && "grid grid-cols-2 gap-4",
exerciseIndex > -1 && isTextMinimized && "flex flex-col gap-2",
)}>
{partIndex > -1 && renderText()}
{exerciseIndex > -1 && {exerciseIndex > -1 &&
partIndex > -1 && partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length && exerciseIndex < exam.parts[partIndex].exercises.length &&
!showSolutions && !showSolutions &&
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, undefined, preview)} renderExercise(getExercise(), exam.id, nextExercise, previousExercise, undefined, preview)}
{exerciseIndex > -1 && {exerciseIndex > -1 &&
partIndex > -1 && partIndex > -1 &&
exerciseIndex < exam.parts[partIndex].exercises.length && exerciseIndex < exam.parts[partIndex].exercises.length &&
showSolutions && showSolutions &&
renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)} renderSolution(exam.parts[partIndex].exercises[exerciseIndex], nextExercise, previousExercise)}
</div> </div>
{exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && ( {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (
<Button <Button
color="purple" color="purple"
variant="outline" variant="outline"
onClick={() => setShowTextModal(true)} onClick={() => setShowTextModal(true)}
className="max-w-[200px] self-end w-full absolute bottom-[31px] right-64"> className="max-w-[200px] self-end w-full absolute bottom-[31px] right-64">
Read text Read text
</Button> </Button>
)}
</div>
{exerciseIndex === -1 && partIndex > 0 && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => {
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex(partIndex - 1);
}}
className="max-w-[200px] w-full">
Back
</Button>
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)}
{exerciseIndex === -1 && partIndex === 0 && (
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Start now
</Button>
)}
</>
)} )}
</div>
{exerciseIndex === -1 && partIndex > 0 && (
<div className="self-end flex justify-between w-full gap-8 absolute bottom-8 left-0 px-8">
<Button
color="purple"
variant="outline"
onClick={() => {
setExerciseIndex(exam.parts[partIndex - 1].exercises.length - 1);
setPartIndex(partIndex - 1);
}}
className="max-w-[200px] w-full">
Back
</Button>
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Next
</Button>
</div>
)}
{exerciseIndex === -1 && partIndex === 0 && (
<Button color="purple" onClick={() => nextExercise()} className="max-w-[200px] self-end w-full">
Start now
</Button>
)}
</> </>
); );
} }

View File

@@ -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;
@@ -26,7 +28,7 @@ export const rootReducer = (
if (SECTION_ACTIONS.includes(action.type as any)) { if (SECTION_ACTIONS.includes(action.type as any)) {
if (action.type === "UPDATE_SECTION_STATE") { if (action.type === "UPDATE_SECTION_STATE") {
const updatedState = sectionReducer(state, action as SectionActions); const updatedState = sectionReducer(state, action as SectionActions);
if (!updatedState.modules) return state; if (!updatedState.modules) return state;
return moduleReducer({ return moduleReducer({
...state, ...state,

View File

@@ -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)
} }
}; };
} }

View File

@@ -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
) )
} }

View File

@@ -1,59 +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> => {
const newSolutions = exercise.solutions console.log();
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.map((solution, index) => ({
...solution,
id: (startId + index).toString()
}));
const idMapping = exercise.solutions
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.reduce((acc, solution, index) => {
acc[solution.id] = (startId + index).toString();
return acc;
}, {} as Record<string, string>);
let newText = exercise.text;
Object.entries(idMapping).forEach(([oldId, newId]) => {
const regex = new RegExp(`\\{\\{${oldId}\\}\\}`, 'g');
newText = newText.replace(regex, `{{${newId}}}`);
});
const newWords = exercise.words.map(word => {
if (typeof word === 'string') {
return word;
} else if ('letter' in word && 'word' in word) {
return word;
} else if ('options' in word) {
return word;
}
return word;
});
const newUserSolutions = exercise.userSolutions?.map(solution => ({
...solution,
id: idMapping[solution.id] || solution.id
}));
return {
exercise: {
...exercise,
solutions: newSolutions,
text: newText,
words: newWords,
userSolutions: newUserSolutions
},
lastId: startId + newSolutions.length - 1
};
};
const reorderWriteBlanks = (exercise: WriteBlanksExercise, startId: number): ReorderResult<WriteBlanksExercise> => {
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) => ({
@@ -61,21 +11,78 @@ const reorderWriteBlanks = (exercise: WriteBlanksExercise, startId: number): Reo
id: (startId + index).toString() id: (startId + index).toString()
})); }));
const idMapping = exercise.solutions
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.reduce((acc, solution, index) => {
acc[solution.id] = (startId + index).toString();
return acc;
}, {} as Record<string, string>);
let newText = exercise.text;
Object.entries(idMapping).forEach(([oldId, newId]) => {
const regex = new RegExp(`\\{\\{${oldId}\\}\\}`, 'g');
newText = newText.replace(regex, `{{${newId}}}`);
});
const newWords = exercise.words.map(word => {
if (typeof word === 'string') {
return word;
} else if ('letter' in word && 'word' in word) {
return word;
} else if ('options' in word) {
return word;
}
return word;
});
const newUserSolutions = exercise.userSolutions?.map(solution => ({
...solution,
id: idMapping[solution.id] || solution.id
}));
return { return {
exercise: { exercise: {
...exercise, ...exercise,
solutions: newSolutions, solutions: newSolutions,
text: newSolutions.reduce((text, solution, index) => { text: newText,
return text.replace( words: newWords,
new RegExp(`\\{\\{${solution.id}\\}\\}`), userSolutions: newUserSolutions
`{{${startId + index}}}`
);
}, exercise.text)
}, },
lastId: startId + newSolutions.length lastId: startId + newSolutions.length
}; };
}; };
const reorderWriteBlanks = (exercise: WriteBlanksExercise, startId: number): ReorderResult<WriteBlanksExercise> => {
const oldIds = exercise.solutions.map(s => s.id);
const newIds = oldIds.map((_, index) => (startId + index).toString());
const newSolutions = exercise.solutions.map((solution, index) => ({
id: newIds[index],
solution: [...solution.solution]
}));
let newText = exercise.text;
oldIds.forEach((oldId, index) => {
newText = newText.replace(
`{{${oldId}}}`,
`{{${newIds[index]}}}`
);
});
const result = {
exercise: {
...exercise,
solutions: newSolutions,
text: newText
},
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
.sort((a, b) => parseInt(a.id) - parseInt(b.id)) .sort((a, b) => parseInt(a.id) - parseInt(b.id))
@@ -129,60 +136,79 @@ const reorderMultipleChoice = (exercise: MultipleChoiceExercise, startId: number
}; };
const reorderSection = (exercises: Exercise[], startId: number): { exercises: Exercise[], lastId: number } => {
let currentId = startId;
const reorderedExercises = exercises.map(exercise => {
let result;
switch (exercise.type) {
case 'fillBlanks':
console.log("Reordering FillBlanks");
result = reorderFillBlanks(exercise, currentId);
currentId = result.lastId;
return result.exercise;
const reorderExercises = (moduleState: ModuleState) => { case 'writeBlanks':
let currentId = 1; result = reorderWriteBlanks(exercise, currentId);
currentId = result.lastId;
return result.exercise;
const reorderedSections = moduleState.sections.map(section => { case 'trueFalse':
const currentSection = section.state as ReadingPart | ListeningPart |LevelPart; result = reorderTrueFalse(exercise, currentId);
const reorderedExercises = currentSection.exercises.map(exercise => { currentId = result.lastId;
let result; return result.exercise;
switch (exercise.type) {
case 'fillBlanks': case 'matchSentences':
result = reorderFillBlanks(exercise, currentId); result = reorderMatchSentences(exercise, currentId);
currentId = result.lastId; currentId = result.lastId;
return result.exercise; return result.exercise;
case 'writeBlanks':
result = reorderWriteBlanks(exercise, currentId); case 'multipleChoice':
currentId = result.lastId; result = reorderMultipleChoice(exercise, currentId);
return result.exercise; currentId = result.lastId;
case 'trueFalse': return result.exercise;
result = reorderTrueFalse(exercise, currentId);
currentId = result.lastId; default:
return result.exercise; return exercise;
case 'matchSentences': }
result = reorderMatchSentences(exercise, currentId);
currentId = result.lastId;
return result.exercise;
case 'multipleChoice':
result = reorderMultipleChoice(exercise, currentId);
currentId = result.lastId
return result.exercise;
default:
return exercise;
}
});
return {
...section,
state: {
...currentSection,
exercises: reorderedExercises
}
};
}); });
return { return {
...moduleState, exercises: reorderedExercises,
sections: reorderedSections 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 {
...section,
state: {
...currentSection,
exercises: result.exercises
}
};
});
return {
...moduleState,
sections: reorderedSections
};
};
export { export {
reorderFillBlanks, reorderFillBlanks,
reorderWriteBlanks, reorderWriteBlanks,
reorderTrueFalse, reorderTrueFalse,
reorderMatchSentences, reorderMatchSentences,
reorderExercises, reorderSection,
reorderModule,
reorderMultipleChoice, reorderMultipleChoice,
}; };