diff --git a/package-lock.json b/package-lock.json index 059e1355..93600cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,7 @@ "@types/react-datepicker": "^4.15.1", "@types/uuid": "^9.0.1", "@types/wavesurfer.js": "^6.0.6", + "@welldone-software/why-did-you-render": "^8.0.3", "@wixc3/react-board": "^2.2.0", "autoprefixer": "^10.4.13", "husky": "^8.0.3", @@ -3615,6 +3616,18 @@ "react": ">= 16.8.0" } }, + "node_modules/@welldone-software/why-did-you-render": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-8.0.3.tgz", + "integrity": "sha512-bb5bKPMStYnocyTBVBu4UTegZdBqzV1mPhxc0UIV/S43KFUSRflux9gvzJfu2aM4EWLJ3egTvdjOi+viK+LKGA==", + "dev": true, + "dependencies": { + "lodash": "^4" + }, + "peerDependencies": { + "react": "^18" + } + }, "node_modules/@wixc3/board-core": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@wixc3/board-core/-/board-core-2.2.0.tgz", @@ -14582,6 +14595,15 @@ "@use-gesture/core": "10.3.1" } }, + "@welldone-software/why-did-you-render": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-8.0.3.tgz", + "integrity": "sha512-bb5bKPMStYnocyTBVBu4UTegZdBqzV1mPhxc0UIV/S43KFUSRflux9gvzJfu2aM4EWLJ3egTvdjOi+viK+LKGA==", + "dev": true, + "requires": { + "lodash": "^4" + } + }, "@wixc3/board-core": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@wixc3/board-core/-/board-core-2.2.0.tgz", diff --git a/package.json b/package.json index 6e705f2a..d26e7571 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "@types/react-datepicker": "^4.15.1", "@types/uuid": "^9.0.1", "@types/wavesurfer.js": "^6.0.6", + "@welldone-software/why-did-you-render": "^8.0.3", "@wixc3/react-board": "^2.2.0", "autoprefixer": "^10.4.13", "husky": "^8.0.3", diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx index 142436a0..1ca52b73 100644 --- a/src/components/Diagnostic.tsx +++ b/src/components/Diagnostic.tsx @@ -1,7 +1,7 @@ import {infoButtonStyle} from "@/constants/buttonStyles"; import {Module} from "@/interfaces"; import {User} from "@/interfaces/user"; -import useExamStore from "@/stores/examStore"; +import useExamStore from "@/stores/exam"; import {getExam, getExamById} from "@/utils/exams"; import {MODULE_ARRAY} from "@/utils/moduleUtils"; import {writingMarking} from "@/utils/score"; @@ -28,8 +28,7 @@ export default function Diagnostic({onFinish}: Props) { const router = useRouter(); - const setExams = useExamStore((state) => state.setExams); - const setSelectedModules = useExamStore((state) => state.setSelectedModules); + const dispatch = useExamStore((state) => state.dispatch); const isNextDisabled = () => { if (!focus) return true; @@ -41,8 +40,7 @@ export default function Diagnostic({onFinish}: Props) { Promise.all(examPromises).then((exams) => { if (exams.every((x) => !!x)) { - setExams(exams.map((x) => x!)); - setSelectedModules(exams.map((x) => x!.module)); + dispatch({type: 'INIT_EXAM', payload: {exams: exams.map((x) => x!), modules: exams.map((x) => x!.module)}}) router.push("/exam"); } }); diff --git a/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx b/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx index d4e1ad76..1a518b65 100644 --- a/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/Letters/index.tsx @@ -12,6 +12,7 @@ import { AlertItem } from "../../Shared/Alert"; import validateBlanks from "../validateBlanks"; import { toast } from "react-toastify"; import setEditingAlert from "../../Shared/setEditingAlert"; +import PromptEdit from "../../Shared/PromptEdit"; interface Word { letter: string; @@ -38,6 +39,12 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num const [editing, setEditing] = useState(false); + const updateLocal = (exercise: FillBlanksExercise) => { + setLocal(exercise); + setEditingAlert(true, setAlerts); + setEditing(true); + }; + const [blanksState, blanksDispatcher] = useReducer(blanksReducer, { text: exercise.text || "", blanks: [], @@ -266,6 +273,8 @@ const FillBlanksLetters: React.FC<{ exercise: FillBlanksExercise; sectionId: num setEditing={setEditing} onPractice={handlePractice} isEvaluationEnabled={!local.isPractice} + prompt={local.prompt} + updatePrompt={(prompt: string) => updateLocal({...local, prompt})} > <> {!blanksState.textMode && diff --git a/src/components/ExamEditor/Exercises/Blanks/MultipleChoice/index.tsx b/src/components/ExamEditor/Exercises/Blanks/MultipleChoice/index.tsx index 35b87f74..826ed848 100644 --- a/src/components/ExamEditor/Exercises/Blanks/MultipleChoice/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/MultipleChoice/index.tsx @@ -36,6 +36,12 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number } const [isEditMode, setIsEditMode] = useState(false); const [editing, setEditing] = useState(false); + const updateLocal = (exercise: FillBlanksExercise) => { + setLocal(exercise); + setEditingAlert(true, setAlerts); + setEditing(true); + }; + const [blanksState, blanksDispatcher] = useReducer(blanksReducer, { text: exercise.text || "", blanks: [], @@ -268,6 +274,8 @@ const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number } setEditing={setEditing} onBlankRemove={handleBlankRemove} isEvaluationEnabled={!local.isPractice} + prompt={local.prompt} + updatePrompt={(prompt: string) => updateLocal({...local, prompt})} > {!blanksState.textMode && selectedBlankId && ( diff --git a/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/index.tsx b/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/index.tsx index 833747b0..1566ecb5 100644 --- a/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/WriteBlankFill/index.tsx @@ -24,6 +24,12 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb const [selectedBlankId, setSelectedBlankId] = useState(null); const [editing, setEditing] = useState(false); + const updateLocal = (exercise: WriteBlanksExercise) => { + setLocal(exercise); + setEditingAlert(true, setAlerts); + setEditing(true); + }; + const [blanksState, blanksDispatcher] = useReducer(blanksReducer, { text: exercise.text || "", blanks: [], @@ -79,7 +85,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb newState.exercises = newState.exercises.map((ex) => ex.id === exercise.id ? updatedExercise : ex ); - setLocal((prev) => ({...prev, isPractice: !local.isPractice})) + setLocal((prev) => ({ ...prev, isPractice: !local.isPractice })) dispatch({ type: 'UPDATE_SECTION_STATE', payload: { sectionId, update: newState, module: currentModule } }); } }); @@ -143,7 +149,7 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb ...prev, solutions: prev.solutions.filter(s => s.id !== blankId.toString()) })); - + blanksDispatcher({ type: "REMOVE_BLANK", payload: blankId }); }; @@ -175,6 +181,8 @@ const WriteBlanksFill: React.FC<{ exercise: WriteBlanksExercise; sectionId: numb onPractice={handlePractice} setEditing={setEditing} isEvaluationEnabled={!local.isPractice} + prompt={local.prompt} + updatePrompt={(prompt: string) => updateLocal({ ...local, prompt })} > {!blanksState.textMode && ( diff --git a/src/components/ExamEditor/Exercises/Blanks/index.tsx b/src/components/ExamEditor/Exercises/Blanks/index.tsx index 9b728a8b..21e40c30 100644 --- a/src/components/ExamEditor/Exercises/Blanks/index.tsx +++ b/src/components/ExamEditor/Exercises/Blanks/index.tsx @@ -19,6 +19,7 @@ import clsx from "clsx"; import { Card, CardContent } from "@/components/ui/card"; import { Blank, DropZone } from "./DragNDrop"; import { getTextSegments, BlankState, BlanksState, BlanksAction, BlankToken } from "./BlanksReducer"; +import PromptEdit from "../Shared/PromptEdit"; interface Props { @@ -30,6 +31,8 @@ interface Props { editing: boolean; showBlankBank: boolean; alerts: AlertItem[]; + prompt: string; + updatePrompt: (prompt: string) => void; setEditing: React.Dispatch>; blanksDispatcher: React.Dispatch onBlankSelect?: (blankId: number | null) => void; @@ -42,25 +45,27 @@ interface Props { children: ReactNode; } -const BlanksEditor: React.FC = ({ +const BlanksEditor: React.FC = ({ title = "Fill Blanks", initialText, description, state, editing, module, - children, + children, showBlankBank = true, alerts, - blanksDispatcher, + blanksDispatcher, onBlankSelect, onBlankRemove, - onSave, - onDiscard, + onSave, + onDiscard, onDelete, onPractice, isEvaluationEnabled, - setEditing + setEditing, + prompt, + updatePrompt }) => { useEffect(() => { @@ -105,21 +110,21 @@ const BlanksEditor: React.FC = ({ const handleTextChange = useCallback( (newText: string) => { const processedText = newText.replace(/\[(\d+)\]/g, "{{$1}}"); - + const existingBlankIds = getTextSegments(state.text) .filter(token => token.type === 'blank') .map(token => (token as BlankToken).id); - + const newBlankIds = getTextSegments(processedText) .filter(token => token.type === 'blank') .map(token => (token as BlankToken).id); - + const removedBlankIds = existingBlankIds.filter(id => !newBlankIds.includes(id)); - + removedBlankIds.forEach(id => { onBlankRemove(id); }); - + blanksDispatcher({ type: "SET_TEXT", payload: processedText }); }, [blanksDispatcher, state.text, onBlankRemove] @@ -171,10 +176,11 @@ const BlanksEditor: React.FC = ({ isEvaluationEnabled={isEvaluationEnabled} /> {alerts.length > 0 && } + updatePrompt(text)} />