Navigation rework, added prompt edit to components that were missing

This commit is contained in:
Carlos-Mesquita
2024-11-25 16:50:46 +00:00
parent e9b7bd14cc
commit 114da173be
105 changed files with 3761 additions and 3728 deletions

180
src/stores/exam/index.ts Normal file
View File

@@ -0,0 +1,180 @@
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { ExamFunctions, ExamState, Navigation, StateFlags } from "./types";
import { rootReducer } from "./reducers";
import axios from "axios";
import { v4 } from "uuid";
import { Stat } from "@/interfaces/user";
import { Exam, Shuffles, UserSolution } from "@/interfaces/exam";
import { Module } from "@/interfaces";
export const initialState: ExamState = {
exams: [],
userSolutions: [],
showSolutions: false,
selectedModules: [],
assignment: undefined,
timeSpent: 0,
timeSpentCurrentModule: 0,
sessionId: "",
exam: undefined,
moduleIndex: 0,
partIndex: 0,
exerciseIndex: 0,
questionIndex: 0,
inactivity: 0,
shuffles: [],
bgColor: "bg-white",
currentSolution: undefined,
user: undefined,
navigation: {
previousDisabled: false,
nextDisabled: false,
previousLoading: false,
nextLoading: false,
},
flags: {
timeIsUp: false,
reviewAll: false,
finalizeModule: false,
finalizeExam: false,
},
};
const useExamStore = create<ExamState & ExamFunctions>((set, get) => ({
...initialState,
setUser: (user: string) => set(() => ({ user })),
setShowSolutions: (showSolutions: boolean) => set(() => ({ showSolutions })),
setExams: (exams: Exam[]) => set(() => ({ exams })),
setExam: (exam?: Exam) => set(() => ({ exam })),
setModuleIndex: (moduleIndex: number) => set(() => ({ moduleIndex })),
setSelectedModules: (modules: Module[]) => set(() => ({ selectedModules: modules })),
setSessionId: (sessionId: string) => set(() => ({ sessionId })),
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({ userSolutions })),
setShuffles: (shuffles: Shuffles[]) => set(() => ({ shuffles })),
setPartIndex: (partIndex: number) => set(() => ({ partIndex })),
setExerciseIndex: (exerciseIndex: number) => set(() => ({ exerciseIndex })),
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
setBgColor: (bgColor: string) => set(() => ({ bgColor })),
setNavigation: (updates: Partial<Navigation>) => set((state) => ({
navigation: {
...state.navigation,
...updates
}
})),
setFlags: (updates: Partial<StateFlags>) => set((state) => ({
flags: {
...state.flags,
...updates
}
})),
setTimeIsUp: (timeIsUp: boolean) => set((state) => ({ flags: { ...state.flags, timeIsUp } })),
reset: () => set(() => initialState),
saveSession: async () => {
console.log("Saving your session...");
const state = get();
await axios.post("/api/sessions", {
id: state.sessionId,
sessionId: state.sessionId,
date: new Date().toISOString(),
userSolutions: state.userSolutions.filter((s) => s.type !== "speaking" && s.type !== "interactiveSpeaking"),
moduleIndex: state.moduleIndex,
selectedModules: state.selectedModules,
assignment: state.assignment,
timeSpent: state.timeSpent,
timeSpentCurrentModule: state.timeSpentCurrentModule,
inactivity: state.inactivity,
exams: state.exams,
exam: state.exam,
partIndex: state.partIndex,
exerciseIndex: state.exerciseIndex,
questionIndex: state.questionIndex,
user: state.user,
});
},
saveStats: async () => {
const state = get();
const newStats: Stat[] = state.userSolutions.map((solution) => ({
...solution,
id: solution.id || v4(),
timeSpent: state.timeSpent,
inactivity: state.inactivity,
session: state.sessionId,
exam: solution.exam!,
module: solution.module!,
user: state.user || "",
date: new Date().getTime(),
isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps,
...(state.assignment ? { assignment: state.assignment.id } : {}),
isPractice: solution.isPractice
}));
await axios.post<{ ok: boolean }>("/api/stats", newStats);
},
dispatch: (action) => set((state) => rootReducer(state, action))
}));
export const usePersistentExamStore = create<ExamState & ExamFunctions>()(
persist(
immer((set) => ({
...initialState,
setUser: (user: string) => set(() => ({ user })),
setShowSolutions: (showSolutions: boolean) => set(() => ({ showSolutions })),
setExams: (exams: Exam[]) => set(() => ({ exams })),
setExam: (exam?: Exam) => set(() => ({ exam })),
setModuleIndex: (moduleIndex: number) => set(() => ({ moduleIndex })),
setSelectedModules: (modules: Module[]) => set(() => ({ selectedModules: modules })),
setSessionId: (sessionId: string) => set(() => ({ sessionId })),
setUserSolutions: (userSolutions: UserSolution[]) => set(() => ({ userSolutions })),
setShuffles: (shuffles: Shuffles[]) => set(() => ({ shuffles })),
setPartIndex: (partIndex: number) => set(() => ({ partIndex })),
setExerciseIndex: (exerciseIndex: number) => set(() => ({ exerciseIndex })),
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
setBgColor: (bgColor: string) => set(() => ({ bgColor })),
setNavigation: (updates: Partial<Navigation>) => set((state) => ({
navigation: {
...state.navigation,
...updates
}
})),
setFlags: (updates: Partial<StateFlags>) => set((state) => ({
flags: {
...state.flags,
...updates
}
})),
setTimeIsUp: (timeIsUp: boolean) => set((state) => ({ flags: { ...state.flags, timeIsUp } })),
saveStats: async () => {},
saveSession: async () => {},
reset: () => set(() => initialState),
dispatch: (action) => set((state) => rootReducer(state, action))
})),
{
name: 'persistent-exam-store',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ ...state }),
}
)
);
export default useExamStore;

View File

@@ -0,0 +1,161 @@
import { Module } from "@/interfaces";
import { ExamState } from "../types";
import { SESSION_ACTIONS, SessionActions, sessionReducer } from "./session";
import { Exam, UserSolution } from "@/interfaces/exam";
import { updateExamWithUserSolutions } from "../utils";
import { defaultExamUserSolutions } from "@/utils/exams";
import { Assignment } from "@/interfaces/results";
import { Stat } from "@/interfaces/user";
import { convertToUserSolutions } from "@/utils/stats";
export type RootActions =
{ type: 'INIT_EXAM'; payload: { exams: Exam[], modules: Module[], assignment?: Assignment } } |
{ type: 'INIT_SOLUTIONS'; payload: { exams: Exam[], modules: Module[], stats: Stat[], timeSpent?: number, inactivity?: number } } |
{ type: 'UPDATE_TIMERS'; payload: { timeSpent: number; inactivity: number; timeSpentCurrentModule: number;} } |
{ type: 'FINALIZE_MODULE'; payload: { updateTimers: boolean } } |
{ type: 'FINALIZE_MODULE_SOLUTIONS' }
export type Action = RootActions | SessionActions;
export const rootReducer = (
state: ExamState,
action: Action
): Partial<ExamState> => {
if (SESSION_ACTIONS.includes(action.type as any)) {
return sessionReducer(action as SessionActions);
}
switch (action.type) {
case 'INIT_EXAM': {
const { exams, modules, assignment } = action.payload;
let examAndSolutions = {}
// A new exam is about to start,
// fill the first module with defaultUserSolutions
let defaultSolutions = exams.map(defaultExamUserSolutions).flat();
examAndSolutions = {
userSolutions: defaultSolutions,
exam: updateExamWithUserSolutions(exams[0], defaultSolutions)
}
if (assignment) {
examAndSolutions = { ...examAndSolutions, assignment }
}
// now all the modules start at 0 since navigation
// is now handled at the module page's and no re-renders
// reset the initial render caused by the timers
// no need to do all that weird chainning with -1
// on some modules and triggering next() to update final solution
// with hasExamEnded flag
return {
moduleIndex: 0,
partIndex: 0,
exerciseIndex: 0,
questionIndex: 0,
exams: exams,
selectedModules: modules,
showSolutions: false,
...examAndSolutions
}
};
case 'INIT_SOLUTIONS': {
const { exams, modules, stats, timeSpent, inactivity } = action.payload;
let time = {}
if (timeSpent) time = { timeSpent }
if (inactivity) time = { ...time, inactivity }
return {
moduleIndex: -1,
partIndex: 0,
exerciseIndex: 0,
questionIndex: 0,
exams: exams,
selectedModules: modules,
showSolutions: true,
userSolutions: convertToUserSolutions(stats),
...time
}
}
case 'UPDATE_TIMERS': {
// Just assigning the timers at once instead of two different calls
const { timeSpent, inactivity, timeSpentCurrentModule } = action.payload;
return {
timeSpentCurrentModule,
timeSpent,
inactivity
}
};
case 'FINALIZE_MODULE': {
const { updateTimers } = action.payload;
// To finalize a module first flag the timers to be updated
if (updateTimers) {
return {
flags: { ...state.flags, finalizeModule: true }
}
} else {
// then check whether there are more modules in the exam, if there are
// setup the next module
if (state.moduleIndex + 1 < state.selectedModules.length) {
return {
moduleIndex: state.moduleIndex + 1,
partIndex: 0,
exerciseIndex: 0,
questionIndex: 0,
exam: updateExamWithUserSolutions(state.exams[state.moduleIndex + 1], state.userSolutions),
flags: {
...state.flags,
finalizeModule: false,
}
}
} else {
// if there are no modules left, flag finalizeExam
// so that the stats are uploaded in ExamPage
// and the Finish view is set there, no need to
// dispatch another init
return {
flags: {
...state.flags,
finalizeModule: false,
finalizeExam: true,
}
}
}
}
}
case 'FINALIZE_MODULE_SOLUTIONS': {
if (state.flags.reviewAll) {
const notLastModule = state.moduleIndex < state.selectedModules.length;
const moduleIndex = notLastModule ? state.moduleIndex + 1 : -1;
if (notLastModule) {
return {
questionIndex: 0,
exerciseIndex: 0,
partIndex: 0,
exam: state.exams[moduleIndex + 1],
moduleIndex: moduleIndex
}
} else {
return {
questionIndex: 0,
exerciseIndex: 0,
partIndex: 0,
moduleIndex: -1
}
}
} else {
return {
moduleIndex: -1
}
}
}
default:
return {};
}
};

View File

@@ -0,0 +1,34 @@
import { Session } from "@/hooks/useSessions";
import { ExamState } from "../types";
export type SessionActions =
{ type: 'SET_SESSION'; payload: { session: Session } }
export const SESSION_ACTIONS = [
'SET_SESSION'
];
export const sessionReducer = (action: SessionActions): Partial<ExamState> => {
switch (action.type) {
case 'SET_SESSION':
const { session } = action.payload;
return {
shuffles: session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })),
selectedModules: session.selectedModules,
exam: session.exam,
exams: session.exams,
sessionId: session.sessionId,
assignment: session.assignment,
exerciseIndex: session.exerciseIndex,
partIndex: session.partIndex,
moduleIndex: session.moduleIndex,
timeSpent: session.timeSpent,
userSolutions: session.userSolutions,
timeSpentCurrentModule: session.timeSpentCurrentModule !== undefined ? session.timeSpentCurrentModule : session.timeSpent,
showSolutions: false,
questionIndex: session.questionIndex
};
default:
return {};
}
}

76
src/stores/exam/types.ts Normal file
View File

@@ -0,0 +1,76 @@
import { Module } from "@/interfaces";
import { Exam, Shuffles, UserSolution } from "@/interfaces/exam";
import { Assignment } from "@/interfaces/results";
import { Action } from "./reducers";
export interface Navigation {
previousDisabled: boolean;
nextDisabled: boolean;
previousLoading: boolean;
nextLoading: boolean;
}
export interface StateFlags {
timeIsUp: boolean;
reviewAll: boolean;
finalizeModule: boolean;
finalizeExam: boolean;
}
export interface ExamState {
exams: Exam[];
userSolutions: UserSolution[];
showSolutions: boolean;
selectedModules: Module[];
assignment?: Assignment;
timeSpent: number;
timeSpentCurrentModule: number;
sessionId: string;
moduleIndex: number;
exam?: Exam;
partIndex: number;
exerciseIndex: number;
questionIndex: number;
inactivity: number;
shuffles: Shuffles[];
bgColor: string;
user: undefined | string;
currentSolution?: UserSolution | undefined;
navigation: Navigation;
flags: StateFlags,
}
export interface ExamFunctions {
setUser: (user: string) => void;
setShowSolutions: (showSolutions: boolean) => void;
setExams: (exams: Exam[]) => void;
setSelectedModules: (modules: Module[]) => void;
setModuleIndex: (moduleIndex: number) => void;
setExam: (exam?: Exam) => void;
setPartIndex: (partIndex: number) => void;
setExerciseIndex: (exerciseIndex: number) => void;
setQuestionIndex: (questionIndex: number) => void;
setBgColor: (bgColor: string) => void;
setShuffles: (shuffles: Shuffles[]) => void;
setSessionId: (sessionId: string) => void;
setUserSolutions: (userSolutions: UserSolution[]) => void;
setTimeIsUp: (timeIsUp: boolean) => void;
saveSession: () => Promise<void>;
saveStats: () => Promise<void>;
setNavigation: (updates: Partial<Navigation>) => void;
setFlags: (updates: Partial<StateFlags>) => void;
reset: () => void;
dispatch: (action: Action) => void;
}

27
src/stores/exam/utils.ts Normal file
View File

@@ -0,0 +1,27 @@
import { Exam, ExerciseOnlyExam, PartExam, UserSolution } from "@/interfaces/exam";
const updateExamWithUserSolutions = (exam: Exam, userSolutions: UserSolution[]): Exam => {
if (["reading", "listening", "level"].includes(exam.module)) {
const parts = (exam as PartExam).parts.map((p) =>
Object.assign(p, {
exercises: p.exercises.map((x) =>
Object.assign(x, {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}),
),
}),
);
return Object.assign(exam, { parts });
}
const exercises = (exam as ExerciseOnlyExam).exercises.map((x) =>
Object.assign(x, {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}),
);
return Object.assign(exam, { exercises });
};
export {
updateExamWithUserSolutions,
}

View File

@@ -1,127 +0,0 @@
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[];
userSolutions: UserSolution[];
showSolutions: boolean;
hasExamEnded: boolean;
selectedModules: Module[];
assignment?: Assignment;
timeSpent: number;
sessionId: string;
moduleIndex: number;
exam?: Exam;
partIndex: number;
exerciseIndex: number;
questionIndex: number;
inactivity: number;
shuffles: Shuffles[];
bgColor: string;
currentSolution?: UserSolution | undefined;
}
export interface ExamFunctions {
setExams: (exams: Exam[]) => void;
setUserSolutions: (userSolutions: UserSolution[]) => void;
setShowSolutions: (showSolutions: boolean) => void;
setHasExamEnded: (hasExamEnded: boolean) => void;
setSelectedModules: (modules: Module[]) => void;
setAssignment: (assignment?: Assignment) => void;
setTimeSpent: (timeSpent: number) => void;
setSessionId: (sessionId: string) => void;
setModuleIndex: (moduleIndex: number) => void;
setExam: (exam?: Exam) => void;
setPartIndex: (partIndex: number) => void;
setExerciseIndex: (exerciseIndex: number) => void;
setQuestionIndex: (questionIndex: number) => void;
setInactivity: (inactivity: number) => void;
setShuffles: (shuffles: Shuffles[]) => void;
setBgColor: (bgColor: string) => void;
setCurrentSolution: (currentSolution: UserSolution | undefined) => void;
reset: () => void;
}
export const initialState: ExamState = {
exams: [],
userSolutions: [],
showSolutions: false,
selectedModules: [],
hasExamEnded: false,
assignment: undefined,
timeSpent: 0,
sessionId: "",
exam: undefined,
moduleIndex: 0,
partIndex: -1,
exerciseIndex: -1,
questionIndex: 0,
inactivity: 0,
shuffles: [],
bgColor: "bg-white",
currentSolution: undefined
};
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 })),
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 }),
}
)
);