196 lines
5.3 KiB
TypeScript
196 lines
5.3 KiB
TypeScript
![]() |
import { quizApi } from "@api/quiz";
|
|||
|
import { devlog, getMessageFromFetchError } from "@frontend/kitui";
|
|||
|
import { quizToEditQuizRequest } from "@model/quiz/edit";
|
|||
|
import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz";
|
|||
|
import { QuizConfig, QuizSetupStep, maxQuizSetupSteps } from "@model/quizSettings";
|
|||
|
import { produce } from "immer";
|
|||
|
import { enqueueSnackbar } from "notistack";
|
|||
|
import { NavigateFunction } from "react-router-dom";
|
|||
|
import { QuizStore, useQuizStore } from "./store";
|
|||
|
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
|||
|
|
|||
|
|
|||
|
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
|
|||
|
state.quizById = {};
|
|||
|
if (quizes === null) return;
|
|||
|
|
|||
|
quizes.forEach(quiz => state.quizById[quiz.id] = rawQuizToQuiz(quiz));
|
|||
|
}, {
|
|||
|
type: "setQuizes",
|
|||
|
quizes,
|
|||
|
});
|
|||
|
|
|||
|
export const setQuiz = (quiz: Quiz) => setProducedState(state => {
|
|||
|
state.quizById[quiz.id] = quiz;
|
|||
|
}, {
|
|||
|
type: "setQuiz",
|
|||
|
quiz,
|
|||
|
});
|
|||
|
|
|||
|
export const removeQuiz = (quizId: number) => setProducedState(state => {
|
|||
|
delete state.quizById[quizId];
|
|||
|
}, {
|
|||
|
type: "removeQuiz",
|
|||
|
quizId,
|
|||
|
});
|
|||
|
|
|||
|
export const setQuizField = <T extends keyof Quiz>(
|
|||
|
quizId: number,
|
|||
|
field: T,
|
|||
|
value: Quiz[T],
|
|||
|
) => setProducedState(state => {
|
|||
|
const quiz = state.quizById[quizId];
|
|||
|
if (!quiz) return;
|
|||
|
|
|||
|
quiz[field] = value;
|
|||
|
}, {
|
|||
|
type: "setQuizField",
|
|||
|
quizId,
|
|||
|
field,
|
|||
|
value,
|
|||
|
});
|
|||
|
|
|||
|
export const updateQuiz = (
|
|||
|
quizId: number,
|
|||
|
updateFn: (quiz: Quiz) => void,
|
|||
|
) => setProducedState(state => {
|
|||
|
const quiz = state.quizById[quizId];
|
|||
|
if (!quiz) return;
|
|||
|
|
|||
|
updateFn(quiz);
|
|||
|
}, {
|
|||
|
type: "updateQuiz",
|
|||
|
quizId,
|
|||
|
updateFn: updateFn.toString(),
|
|||
|
});
|
|||
|
|
|||
|
export const incrementCurrentStep = () => setProducedState(state => {
|
|||
|
state.currentStep = Math.min(
|
|||
|
maxQuizSetupSteps, state.currentStep + 1
|
|||
|
) as QuizSetupStep;
|
|||
|
});
|
|||
|
|
|||
|
export const decrementCurrentStep = () => setProducedState(state => {
|
|||
|
state.currentStep = Math.max(
|
|||
|
1, state.currentStep - 1
|
|||
|
) as QuizSetupStep;
|
|||
|
});
|
|||
|
|
|||
|
export const setCurrentStep = (step: number) => setProducedState(state => {
|
|||
|
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps, step)) as QuizSetupStep;
|
|||
|
});
|
|||
|
|
|||
|
export const createQuiz = async (navigate: NavigateFunction) => {
|
|||
|
try {
|
|||
|
const quiz = await quizApi.create({
|
|||
|
name: "Quiz name",
|
|||
|
description: "Quiz description",
|
|||
|
});
|
|||
|
|
|||
|
setQuiz(rawQuizToQuiz(quiz));
|
|||
|
navigate(`/setting/${quiz.id}`);
|
|||
|
} catch (error) {
|
|||
|
devlog("Error creating quiz", error);
|
|||
|
|
|||
|
const message = getMessageFromFetchError(error) ?? "";
|
|||
|
enqueueSnackbar(`Не удалось создать квиз. ${message}`);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
export const deleteQuiz = async (quizId: number) => {
|
|||
|
try {
|
|||
|
await quizApi.delete(quizId);
|
|||
|
|
|||
|
removeQuiz(quizId);
|
|||
|
} catch (error) {
|
|||
|
devlog("Error deleting quiz", error);
|
|||
|
|
|||
|
const message = getMessageFromFetchError(error) ?? "";
|
|||
|
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
export const setQuizType = (
|
|||
|
quizId: number,
|
|||
|
quizType: QuizConfig["type"],
|
|||
|
navigate: NavigateFunction,
|
|||
|
) => {
|
|||
|
updateQuizWithFnOptimistic(
|
|||
|
quizId,
|
|||
|
quiz => {
|
|||
|
quiz.config.type = quizType;
|
|||
|
},
|
|||
|
navigate,
|
|||
|
);
|
|||
|
incrementCurrentStep();
|
|||
|
};
|
|||
|
|
|||
|
export const setQuizStartpageType = (
|
|||
|
quizId: number,
|
|||
|
startpageType: QuizConfig["startpageType"],
|
|||
|
navigate: NavigateFunction,
|
|||
|
) => {
|
|||
|
updateQuizWithFnOptimistic(
|
|||
|
quizId,
|
|||
|
quiz => {
|
|||
|
quiz.config.startpageType = startpageType;
|
|||
|
},
|
|||
|
navigate,
|
|||
|
);
|
|||
|
incrementCurrentStep();
|
|||
|
};
|
|||
|
|
|||
|
let savedOriginalQuiz: Quiz | null = null;
|
|||
|
let controller: AbortController | null = null;
|
|||
|
|
|||
|
export const updateQuizWithFnOptimistic = async (
|
|||
|
quizId: number,
|
|||
|
updateFn: (quiz: Quiz) => void,
|
|||
|
navigate: NavigateFunction,
|
|||
|
rollbackOnError = true,
|
|||
|
) => {
|
|||
|
const quiz = useQuizStore.getState().quizById[quizId] ?? null;
|
|||
|
if (!quiz) return;
|
|||
|
|
|||
|
const currentUpdatedQuiz = produce(quiz, updateFn);
|
|||
|
|
|||
|
controller?.abort();
|
|||
|
controller = new AbortController();
|
|||
|
savedOriginalQuiz ??= quiz;
|
|||
|
|
|||
|
setQuiz(currentUpdatedQuiz);
|
|||
|
try {
|
|||
|
const { updated } = await quizApi.edit(quizToEditQuizRequest(currentUpdatedQuiz), controller.signal);
|
|||
|
// await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected")));
|
|||
|
|
|||
|
setQuizField(quiz.id, "version", updated);
|
|||
|
navigate(`/setting/${quizId}`, { replace: true });
|
|||
|
|
|||
|
controller = null;
|
|||
|
savedOriginalQuiz = null;
|
|||
|
} catch (error) {
|
|||
|
if (isAxiosCanceledError(error)) return;
|
|||
|
|
|||
|
devlog("Error editing quiz", { error, quiz, currentUpdatedQuiz });
|
|||
|
enqueueSnackbar("Не удалось сохранить настройки квиза");
|
|||
|
|
|||
|
if (rollbackOnError) {
|
|||
|
if (!savedOriginalQuiz) {
|
|||
|
devlog("Cannot rollback quiz");
|
|||
|
throw new Error("Cannot rollback quiz");
|
|||
|
}
|
|||
|
setQuiz(savedOriginalQuiz);
|
|||
|
}
|
|||
|
|
|||
|
controller = null;
|
|||
|
savedOriginalQuiz = null;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
function setProducedState<A extends string | { type: unknown; }>(
|
|||
|
recipe: (state: QuizStore) => void,
|
|||
|
action?: A,
|
|||
|
) {
|
|||
|
useQuizStore.setState(state => produce(state, recipe), false, action);
|
|||
|
}
|