Exam generation rework, batch user tables, fastapi endpoint switch

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

View File

@@ -0,0 +1,119 @@
import { Module } from "@/interfaces"
import { Difficulty} from "@/interfaces/exam"
import { sample } from "lodash"
import { ExamPart, ModuleState, SectionState } from "./types"
import { levelPart, listeningSection, readingPart, speakingTask, writingTask } from "@/stores/examEditor/sections"
const defaultSettings = (module: Module) => {
const baseSettings = {
category: '',
introOption: { label: 'None', value: 'None' },
customIntro: '',
currentIntro: '',
topic: '',
isCategoryDropdownOpen: false,
isIntroDropdownOpen: false,
isExerciseDropdownOpen: false,
}
switch (module) {
case 'reading':
return {
...baseSettings,
isPassageOpen: false,
}
case 'listening':
return {
...baseSettings,
isAudioContextOpen: false
}
default:
return baseSettings;
}
}
const sectionLabels = (module: Module) => {
switch (module) {
case 'reading':
return Array.from({ length: 3 }, (_, index) => ({
id: index + 1,
label: `Passage ${index + 1}`
}));
case 'writing':
return [{id: 1, label: "Task 1"}, {id: 2, label: "Task 2"}];
case 'speaking':
return [{id: 1, label: "Speaking 1"}, {id: 2, label: "Speaking 2"}, {id: 3, label: "Interactive Speaking"}];
case 'listening':
return Array.from({ length: 4 }, (_, index) => ({
id: index + 1,
label: `Section ${index + 1}`
}));
case 'level':
return [{id: 1, label: "Part 1"}];
}
}
const defaultExamLabel = (module: Module) => {
switch (module) {
case 'reading':
return "Reading Exam";
case 'writing':
return "Writing Exam";
case 'speaking':
return "Speaking Exam";
case 'listening':
return "Listening Exam";
case 'level':
return "Placement Test";
}
}
const defaultSection = (module: Module, sectionId: number) => {
switch (module) {
case 'reading':
return readingPart(sectionId);
case 'writing':
return writingTask(sectionId);
case 'listening':
return listeningSection(sectionId)
case 'speaking':
return speakingTask(sectionId)
case 'level':
return levelPart(sectionId)
}
}
export const defaultSectionSettings = (module: Module, sectionId: number, part?: ExamPart) => {
return {
sectionId: sectionId,
sectionLabel: "",
settings: defaultSettings(module),
state: part !== undefined ? part : defaultSection(module, sectionId),
generating: undefined,
genResult: undefined,
expandedSubSections: [],
exercisePickerState: [],
selectedExercises: [],
}
}
const defaultModuleSettings = (module: Module, minTimer: number, states?: SectionState[]): ModuleState => {
const state: ModuleState = {
examLabel: defaultExamLabel(module),
minTimer,
difficulty: sample(["easy", "medium", "hard"] as Difficulty[])!,
isPrivate: false,
sectionLabels: sectionLabels(module),
expandedSections: [1],
focusedSection: 1,
sections: [defaultSectionSettings(module, 1)],
importModule: true,
importing: false,
edit: [],
};
return state;
}
export default defaultModuleSettings;

View File

@@ -0,0 +1,24 @@
import defaultModuleSettings from "./defaults";
import { Action, rootReducer } from "./reducers";
import ExamEditorStore from "./types";
import { create } from "zustand";
const useExamEditorStore = create<
ExamEditorStore & {
dispatch: (action: Action) => void;
}>((set) => ({
title: "",
globalEdit: [],
currentModule: "reading",
modules: {
reading: defaultModuleSettings("reading", 60),
writing: defaultModuleSettings("writing", 60),
speaking: defaultModuleSettings("speaking", 14),
listening: defaultModuleSettings("listening", 30),
level: defaultModuleSettings("level", 60)
},
dispatch: (action) => set((state) => rootReducer(state, action))
}));
export default useExamEditorStore;

View File

@@ -0,0 +1,49 @@
import ExamEditorStore from "../types";
import { MODULE_ACTIONS, ModuleActions, moduleReducer } from "./moduleReducer";
import { SECTION_ACTIONS, SectionActions, sectionReducer } from "./sectionReducer";
export type Action = ModuleActions | SectionActions | { type: 'UPDATE_ROOT'; payload: { updates: Partial<ExamEditorStore> } };
export const rootReducer = (
state: ExamEditorStore,
action: Action
): Partial<ExamEditorStore> => {
if (MODULE_ACTIONS.includes(action.type as any)) {
if (action.type === "REORDER_EXERCISES" && "payload" in action && "event" in action.payload) {
const updatedState = sectionReducer(state, action as SectionActions);
if (!updatedState.modules) return state;
return moduleReducer({
...state,
modules: updatedState.modules
}, { type: "REORDER_EXERCISES" });
}
return moduleReducer(state, action as ModuleActions);
}
if (SECTION_ACTIONS.includes(action.type as any)) {
if (action.type === "UPDATE_SECTION_STATE") {
const updatedState = sectionReducer(state, action as SectionActions);
if (!updatedState.modules) return state;
return moduleReducer({
...state,
modules: updatedState.modules
}, { type: "REORDER_EXERCISES" });
}
return sectionReducer(state, action as SectionActions);
}
switch (action.type) {
case 'UPDATE_ROOT':
const { updates } = action.payload;
return {
...state,
...updates
};
default:
return {};
}
};

View File

@@ -0,0 +1,107 @@
import { Module } from "@/interfaces";
import { defaultSectionSettings } from "../defaults";
import { reorderExercises } from "../reorder/global";
import ExamEditorStore, { ModuleState } from "../types";
export type ModuleActions =
| { type: 'UPDATE_MODULE'; payload: { updates: Partial<ModuleState>, module?: Module } }
| { type: 'TOGGLE_SECTION'; payload: { sectionId: number; } }
| { type: 'RESET_FOCUS'; }
| { type: 'REORDER_EXERCISES' };
export const MODULE_ACTIONS = [
'UPDATE_MODULE',
'TOGGLE_SECTION',
'RESET_FOCUS',
'REORDER_EXERCISES'
];
export const moduleReducer = (
state: ExamEditorStore,
action: ModuleActions
): Partial<ExamEditorStore> => {
const currentModule = state.currentModule;
const currentModuleState = state.modules[currentModule];
const expandedSections = currentModuleState.expandedSections;
switch (action.type) {
case 'UPDATE_MODULE':
const { updates, module } = action.payload;
if (module === undefined) {
return {
modules: {
...state.modules,
[currentModule]: {
...currentModuleState,
...updates
}
}
};
} else {
return {
modules: {
...state.modules,
[module]: {
...state.modules[module],
...updates
}
}
};
}
case 'TOGGLE_SECTION':
const { sectionId } = action.payload;
const prev = currentModuleState.sections;
const updatedSections = prev.some(section => section.sectionId === sectionId)
? prev.filter(section => section.sectionId !== sectionId)
: [
...prev,
defaultSectionSettings(currentModule, sectionId)
];
const updatedCheckedSections = expandedSections.includes(sectionId)
? expandedSections.filter(i => i !== sectionId)
: [...expandedSections, sectionId];
return {
modules: {
...state.modules,
[currentModule]: {
...currentModuleState,
sections: updatedSections,
expandedSections: updatedCheckedSections
}
}
};
case 'RESET_FOCUS': {
const currentFocus = currentModuleState.focusedSection;
const updatedSections = expandedSections?.filter(id => id != currentFocus);
return {
modules: {
...state.modules,
[currentModule]: {
...currentModuleState,
expandedSections: updatedSections,
focusedSection: updatedSections[0]
}
}
}
}
case 'REORDER_EXERCISES': {
if (currentModule === "writing" || currentModule === "speaking") return state;
return {
...state,
modules: {
...state.modules,
[currentModule]: reorderExercises(currentModuleState)
}
};
}
default:
return {};
}
};

View File

@@ -0,0 +1,118 @@
import { Module } from "@/interfaces";
import ExamEditorStore, { Generating, ReadingSectionSettings, Section, SectionSettings, SectionState } from "../types";
import { DragEndEvent } from "@dnd-kit/core";
import { LevelPart, ListeningPart, ReadingPart } from "@/interfaces/exam";
export type SectionActions =
| { type: 'UPDATE_SECTION_SINGLE_FIELD'; payload: { module: Module; sectionId: number; field: string; value: any } }
| { type: 'UPDATE_SECTION_SETTINGS'; payload: { sectionId: number; update: Partial<SectionSettings | ReadingSectionSettings>; } }
| { type: 'UPDATE_SECTION_STATE'; payload: { sectionId: number; update: Partial<Section>; } }
| { type: 'REORDER_EXERCISES'; payload: { event: DragEndEvent, sectionId: number; } };
export const SECTION_ACTIONS = [
'UPDATE_SECTION_SETTINGS',
'UPDATE_SECTION_STATE',
'UPDATE_SECTION_SINGLE_FIELD',
];
export const sectionReducer = (
state: ExamEditorStore,
action: SectionActions
): Partial<ExamEditorStore> => {
const currentModule = state.currentModule;
const modules = state.modules;
const sections = state.modules[currentModule].sections;
let sectionId: number;
if (action.payload && 'sectionId' in action.payload) {
sectionId = action.payload.sectionId;
}
switch (action.type) {
case 'UPDATE_SECTION_SINGLE_FIELD':
const { module, field, value } = action.payload;
return {
modules: {
...modules,
[module]: {
...modules[module],
sections: sections.map((section: SectionState) =>
section.sectionId === sectionId
? {
...section,
[field]: value
}
: section
)
}
}
};
case 'UPDATE_SECTION_SETTINGS':
let updatedSettings = action.payload.update;
return {
modules: {
...modules,
[currentModule]: {
...modules[currentModule],
sections: sections.map((section: SectionState) =>
section.sectionId === sectionId
? {
...section,
settings: {
...section.settings,
...updatedSettings
}
}
: section
)
}
}
};
case 'UPDATE_SECTION_STATE':
const updatedState = action.payload.update;
return {
modules: {
...modules,
[currentModule]: {
...modules[currentModule],
sections: sections.map(section =>
section.sectionId === sectionId
? { ...section, state: {...section.state, ...updatedState} }
: section
)
}
}
};
case 'REORDER_EXERCISES': {
const { active, over } = action.payload.event;
if (!over) return state;
const oldIndex = active.id as number;
const newIndex = over.id as number;
const currentSectionState = sections.find((s) => s.sectionId = sectionId)!.state as ReadingPart | ListeningPart | LevelPart;
const [removed] = currentSectionState.exercises.splice(oldIndex, 1);
currentSectionState.exercises.splice(newIndex, 0, removed);
return {
...state,
modules: {
...state.modules,
[currentModule]: {
...modules[currentModule],
sections: sections.map(section =>
section.sectionId === sectionId
? { ...section, state: currentSectionState }
: section
)
}
}
};
}
default:
return {};
}
};

View File

@@ -0,0 +1,188 @@
import { FillBlanksExercise, LevelPart, ListeningPart, MatchSentencesExercise, MultipleChoiceExercise, ReadingPart, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
import { ModuleState } from "../types";
import ReorderResult from "./types";
const reorderFillBlanks = (exercise: FillBlanksExercise, startId: number): ReorderResult<FillBlanksExercise> => {
const newSolutions = exercise.solutions
.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
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.map((solution, index) => ({
...solution,
id: (startId + index).toString()
}));
return {
exercise: {
...exercise,
solutions: newSolutions,
text: newSolutions.reduce((text, solution, index) => {
return text.replace(
new RegExp(`\\{\\{${solution.id}\\}\\}`),
`{{${startId + index}}}`
);
}, exercise.text)
},
lastId: startId + newSolutions.length
};
};
const reorderTrueFalse = (exercise: TrueFalseExercise, startId: number): ReorderResult<TrueFalseExercise> => {
const newQuestions = exercise.questions
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.map((question, index) => ({
...question,
id: (startId + index).toString()
}));
return {
exercise: {
...exercise,
questions: newQuestions
},
lastId: startId + newQuestions.length
};
};
const reorderMatchSentences = (exercise: MatchSentencesExercise, startId: number): ReorderResult<MatchSentencesExercise> => {
const newSentences = exercise.sentences
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.map((sentence, index) => ({
...sentence,
id: (startId + index).toString()
}));
return {
exercise: {
...exercise,
sentences: newSentences
},
lastId: startId + newSentences.length
};
};
const reorderMultipleChoice = (exercise: MultipleChoiceExercise, startId: number): ReorderResult<MultipleChoiceExercise> => {
const newQuestions = exercise.questions
.sort((a, b) => parseInt(a.id) - parseInt(b.id))
.map((question, index) => ({
...question,
id: (startId + index).toString()
}));
return {
exercise: {
...exercise,
questions: newQuestions
},
lastId: startId + newQuestions.length
};
};
const reorderExercises = (moduleState: ModuleState) => {
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;
switch (exercise.type) {
case 'fillBlanks':
result = reorderFillBlanks(exercise, currentId);
currentId = result.lastId;
return result.exercise;
case 'writeBlanks':
result = reorderWriteBlanks(exercise, currentId);
currentId = result.lastId;
return result.exercise;
case 'trueFalse':
result = reorderTrueFalse(exercise, currentId);
currentId = result.lastId;
return result.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 {
...moduleState,
sections: reorderedSections
};
};
export {
reorderFillBlanks,
reorderWriteBlanks,
reorderTrueFalse,
reorderMatchSentences,
reorderExercises,
reorderMultipleChoice,
};

View File

@@ -0,0 +1,130 @@
import { DragEndEvent } from "@dnd-kit/core";
import { WriteBlanksExercise, TrueFalseExercise, MatchSentencesExercise, MultipleChoiceExercise } from "@/interfaces/exam";
import getMinExerciseId from "./minIds";
import { parseText, reconstructText } from "@/components/ExamEditor/Exercises/WriteBlanks/parsing";
import { arrayMove } from "@dnd-kit/sortable";
const handleWriteBlanksReorder = (event: DragEndEvent, exercise: WriteBlanksExercise) => {
const { active, over } = event;
if (!over || active.id === over.id) return exercise;
const oldIndex = exercise.solutions.findIndex(s => s.id === active.id);
const newIndex = exercise.solutions.findIndex(s => s.id === over.id);
const newSolutions = [...exercise.solutions];
const [movedItem] = newSolutions.splice(oldIndex, 1);
newSolutions.splice(newIndex, 0, movedItem);
const questions = parseText(exercise.text);
const reorderedQuestions = questions.map((q, i) => {
const solutionIndex = questions.findIndex(origQ => origQ.id === newSolutions[i].id);
return questions[solutionIndex];
});
const minId = getMinExerciseId(exercise);
const finalSolutions = newSolutions.map((solution, index) => ({
...solution,
id: (minId + index).toString()
}));
const finalQuestions = reorderedQuestions.map((q, index) => ({
...q,
id: (minId + index).toString()
}));
const newText = reconstructText(finalQuestions);
return {
...exercise,
solutions: finalSolutions,
text: newText
};
};
const handleTrueFalseReorder = (event: DragEndEvent, exercise: TrueFalseExercise) => {
const { active, over } = event;
if (!over || active.id === over.id) return exercise;
const oldIndex = exercise.questions.findIndex(q => q.id === active.id);
const newIndex = exercise.questions.findIndex(q => q.id === over.id);
const newQuestions = [...exercise.questions];
const [movedItem] = newQuestions.splice(oldIndex, 1);
newQuestions.splice(newIndex, 0, movedItem);
const minId = getMinExerciseId(exercise);
const reorderedQuestions = newQuestions.map((question, index) => ({
...question,
id: (minId + index).toString()
}));
return {
...exercise,
questions: reorderedQuestions
};
};
const handleMatchSentencesReorder = (event: DragEndEvent, exercise: MatchSentencesExercise) => {
const { active, over } = event;
if (!over || active.id === over.id) return exercise;
const oldIndex = exercise.sentences.findIndex(s => s.id === active.id);
const newIndex = exercise.sentences.findIndex(s => s.id === over.id);
const newSentences = [...exercise.sentences];
const [movedItem] = newSentences.splice(oldIndex, 1);
newSentences.splice(newIndex, 0, movedItem);
const minId = getMinExerciseId(exercise);
const reorderedSentences = newSentences.map((sentence, index) => ({
...sentence,
id: (minId + index).toString()
}));
return {
...exercise,
sentences: reorderedSentences
};
};
const handleMultipleChoiceReorder = (event: DragEndEvent, exercise: MultipleChoiceExercise): MultipleChoiceExercise => {
const { active, over } = event;
if (!over || active.id === over.id) {
return exercise;
}
const oldIndex = exercise.questions.findIndex(
question => question.id === active.id
);
const overIndex = exercise.questions.findIndex(
question => question.id === over.id
);
if (oldIndex === -1 || overIndex === -1) {
return exercise;
}
const reorderedQuestions = arrayMove(exercise.questions, oldIndex, overIndex);
const minId = Math.min(...exercise.questions.map(q => parseInt(q.id)));
const updatedQuestions = reorderedQuestions.map((question, index) => ({
...question,
id: (minId + index).toString()
}));
return {
...exercise,
questions: updatedQuestions
};
};
export {
handleWriteBlanksReorder,
handleTrueFalseReorder,
handleMatchSentencesReorder,
handleMultipleChoiceReorder,
};

View File

@@ -0,0 +1,34 @@
import { FillBlanksExercise, MatchSentencesExercise, TrueFalseExercise, WriteBlanksExercise } from "@/interfaces/exam";
const getMinFillBlanksId = (exercise: FillBlanksExercise): number => {
return Math.min(...exercise.solutions.map(s => parseInt(s.id)));
};
const getMinWriteBlanksId = (exercise: WriteBlanksExercise): number => {
return Math.min(...exercise.solutions.map(s => parseInt(s.id)));
};
const getMinTrueFalseId = (exercise: TrueFalseExercise): number => {
return Math.min(...exercise.questions.map(q => parseInt(q.id)));
};
const getMinMatchSentencesId = (exercise: MatchSentencesExercise): number => {
return Math.min(...exercise.sentences.map(s => parseInt(s.id)));
};
const getMinExerciseId = (exercise: FillBlanksExercise | WriteBlanksExercise | TrueFalseExercise | MatchSentencesExercise): number => {
switch (exercise.type) {
case 'fillBlanks':
return getMinFillBlanksId(exercise);
case 'writeBlanks':
return getMinWriteBlanksId(exercise);
case 'trueFalse':
return getMinTrueFalseId(exercise);
case 'matchSentences':
return getMinMatchSentencesId(exercise);
default:
return 1;
}
};
export default getMinExerciseId;

View File

@@ -0,0 +1,4 @@
export default interface ReorderResult<T> {
exercise: T;
lastId: number;
}

View File

@@ -0,0 +1,62 @@
import { InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
import { v4 } from "uuid";
export const writingTask = (task: number) => ({
id: v4(),
type: "writing",
prefix: `You should spend about ${task == 1 ? "20" : "40"} minutes on this task.`,
prompt: "",
userSolutions: [],
suffix: `You should write at least ${task == 1 ? "150" : "250"} words.`,
wordCounter: {
limit: task == 1 ? 150 : 250,
type: "min",
}
} as WritingExercise);
export const readingPart = (task: number) => {
return {
text: {
title: "",
content: ""
},
exercises: []
} as ReadingPart;
};
export const listeningSection = (task: number) => {
return {
audio: undefined,
script: undefined,
exercises: [],
} as ListeningPart;
};
export const speakingTask = (task: number) => {
if (task === 3) {
return {
id: v4(),
type: "interactiveSpeaking",
title: "",
text: "",
prompts: [],
userSolutions: []
} as InteractiveSpeakingExercise;
}
return {
id: v4(),
type: "speaking",
title: "",
text: "",
prompts: [],
video_url: "",
userSolutions: [],
} as SpeakingExercise;
}
export const levelPart = (task : number) => {
return {
exercises: []
} as LevelPart;
}

View File

@@ -0,0 +1,70 @@
import { Difficulty, InteractiveSpeakingExercise, LevelPart, ListeningPart, ReadingPart, SpeakingExercise, WritingExercise } from "@/interfaces/exam";
import { Module } from "@/interfaces";
import Option from "@/interfaces/option";
import { ExerciseConfig } from "@/components/ExamEditor/Shared/ExercisePicker/ExerciseWizard";
export interface GeneratedExercises {
exercises: Record<string, string>[];
sectionId: number;
module: string;
}
export interface SectionSettings {
category: string;
introOption: Option;
customIntro: string;
currentIntro: string | undefined;
isCategoryDropdownOpen: boolean;
isIntroDropdownOpen: boolean;
isExerciseDropdownOpen: boolean;
topic?: string;
}
export interface SpeakingSectionSettings extends SectionSettings {
secondTopic?: string;
}
export interface ReadingSectionSettings extends SectionSettings {
isPassageOpen: boolean;
}
export interface ListeningSectionSettings extends SectionSettings {
isAudioContextOpen: boolean;
}
export type Generating = "context" | "exercises" | "media" | undefined;
export type Section = LevelPart | ReadingPart | ListeningPart | WritingExercise | SpeakingExercise | InteractiveSpeakingExercise;
export type ExamPart = ListeningPart | ReadingPart | LevelPart;
export interface SectionState {
sectionId: number;
settings: SectionSettings | ReadingSectionSettings;
state: Section;
expandedSubSections: number[];
generating: Generating;
genResult: Record<string, any>[] | undefined;
exercisePickerState: ExerciseConfig[];
selectedExercises: string[];
}
export interface ModuleState {
examLabel: string;
sections: SectionState[];
minTimer: number;
difficulty: Difficulty;
isPrivate: boolean;
sectionLabels: {id: number; label: string;}[];
expandedSections: number[];
focusedSection: number;
importModule: boolean;
importing: boolean;
edit: number[];
}
export default interface ExamEditorStore {
title: string;
currentModule: Module;
modules: {
[K in Module]: ModuleState
};
}

View File

@@ -1,7 +1,9 @@
import {Module} from "@/interfaces";
import {Exam, ShuffleMap, Shuffles, UserSolution} from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results";
import {create} from "zustand";
import { Module } from "@/interfaces";
import { Exam, Shuffles, UserSolution } from "@/interfaces/exam";
import { Assignment } from "@/interfaces/results";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
export interface ExamState {
exams: Exam[];
@@ -67,25 +69,59 @@ export const initialState: ExamState = {
const useExamStore = create<ExamState & ExamFunctions>((set) => ({
...initialState,
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({userSolutions})),
setExams: (exams: Exam[]) => set(() => ({exams})),
setShowSolutions: (showSolutions: boolean) => set(() => ({showSolutions})),
setSelectedModules: (modules: Module[]) => set(() => ({selectedModules: modules})),
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({hasExamEnded})),
setAssignment: (assignment?: Assignment) => set(() => ({assignment})),
setTimeSpent: (timeSpent) => set(() => ({timeSpent})),
setSessionId: (sessionId: string) => set(() => ({sessionId})),
setExam: (exam?: Exam) => set(() => ({exam})),
setModuleIndex: (moduleIndex: number) => set(() => ({moduleIndex})),
setPartIndex: (partIndex: number) => set(() => ({partIndex})),
setExerciseIndex: (exerciseIndex: number) => set(() => ({exerciseIndex})),
setQuestionIndex: (questionIndex: number) => set(() => ({questionIndex})),
setInactivity: (inactivity: number) => set(() => ({inactivity})),
setShuffles: (shuffles: Shuffles[]) => set(() => ({shuffles})),
setBgColor: (bgColor) => set(() => ({bgColor})),
setCurrentSolution: (currentSolution: UserSolution | undefined) => set(() => ({currentSolution})),
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({ userSolutions })),
setExams: (exams: Exam[]) => set(() => ({ exams })),
setShowSolutions: (showSolutions: boolean) => set(() => ({ showSolutions })),
setSelectedModules: (modules: Module[]) => set(() => ({ selectedModules: modules })),
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({ hasExamEnded })),
setAssignment: (assignment?: Assignment) => set(() => ({ assignment })),
setTimeSpent: (timeSpent) => set(() => ({ timeSpent })),
setSessionId: (sessionId: string) => set(() => ({ sessionId })),
setExam: (exam?: Exam) => set(() => ({ exam })),
setModuleIndex: (moduleIndex: number) => set(() => ({ moduleIndex })),
setPartIndex: (partIndex: number) => set(() => ({ partIndex })),
setExerciseIndex: (exerciseIndex: number) => set(() => ({ exerciseIndex })),
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
setInactivity: (inactivity: number) => set(() => ({ inactivity })),
setShuffles: (shuffles: Shuffles[]) => set(() => ({ shuffles })),
setBgColor: (bgColor) => set(() => ({ bgColor })),
setCurrentSolution: (currentSolution: UserSolution | undefined) => set(() => ({ currentSolution })),
reset: () => set(() => initialState),
}));
export default useExamStore;
export const usePersistentExamStore = create<ExamState & ExamFunctions>()(
persist(
immer((set) => ({
...initialState,
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({ userSolutions })),
setExams: (exams: Exam[]) => set(() => ({ exams })),
setShowSolutions: (showSolutions: boolean) => set(() => ({ showSolutions })),
setSelectedModules: (modules: Module[]) => set(() => ({ selectedModules: modules })),
setHasExamEnded: (hasExamEnded: boolean) => set(() => ({ hasExamEnded })),
setAssignment: (assignment?: Assignment) => set(() => ({ assignment })),
setTimeSpent: (timeSpent) => set(() => ({ timeSpent })),
setSessionId: (sessionId: string) => set(() => ({ sessionId })),
setExam: (exam?: Exam) => set(() => ({ exam })),
setModuleIndex: (moduleIndex: number) => set(() => ({ moduleIndex })),
setPartIndex: (partIndex: number) => set(() => ({ partIndex })),
setExerciseIndex: (exerciseIndex: number) => set(() => ({ exerciseIndex })),
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
setInactivity: (inactivity: number) => set(() => ({ inactivity })),
setShuffles: (shuffles: Shuffles[]) => set(() => ({ shuffles })),
setBgColor: (bgColor) => set(() => ({ bgColor })),
setCurrentSolution: (currentSolution: UserSolution | undefined) => set(() => ({ currentSolution })),
reset: () => set(() => initialState),
})),
{
name: 'persistent-exam-store',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ ...state }),
}
)
);