184 lines
5.1 KiB
TypeScript
184 lines
5.1 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, maxQuizSetupSteps } from "@model/quizSettings";
|
||
import { createQuestion } from "@root/questions/actions";
|
||
import { produce } from "immer";
|
||
import { enqueueSnackbar } from "notistack";
|
||
import { NavigateFunction } from "react-router-dom";
|
||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||
import { RequestQueue } from "../../utils/requestQueue";
|
||
import { QuizStore, useQuizStore } from "./store";
|
||
|
||
|
||
export const setEditQuizId = (quizId: number | null) => setProducedState(state => {
|
||
state.editQuizId = quizId;
|
||
}, {
|
||
type: "setEditQuizId",
|
||
quizId,
|
||
});
|
||
|
||
export const resetEditConfig = () => setProducedState(state => {
|
||
state.editQuizId = null;
|
||
state.currentStep = 0;
|
||
});
|
||
|
||
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
|
||
state.quizes = quizes?.map(rawQuizToQuiz) ?? [];
|
||
}, {
|
||
type: "setQuizes",
|
||
quizes,
|
||
});
|
||
|
||
const addQuiz = (quiz: Quiz) => setProducedState(state => {
|
||
state.quizes.push(quiz);
|
||
}, {
|
||
type: "addQuiz",
|
||
quiz,
|
||
});
|
||
|
||
const removeQuiz = (quizId: string) => setProducedState(state => {
|
||
const index = state.quizes.findIndex(q => q.id === quizId);
|
||
if (index === -1) return;
|
||
|
||
state.quizes.splice(index, 1);
|
||
}, {
|
||
type: "removeQuiz",
|
||
quizId,
|
||
});
|
||
|
||
const setQuizBackendId = (quizId: string, backendId: number) => setProducedState(state => {
|
||
const quiz = state.quizes.find(q => q.id === quizId);
|
||
if (!quiz) return;
|
||
|
||
quiz.backendId = backendId;
|
||
}, {
|
||
type: "setQuizBackendId",
|
||
quizId,
|
||
backendId,
|
||
});
|
||
|
||
export const incrementCurrentStep = () => setProducedState(state => {
|
||
state.currentStep = Math.min(maxQuizSetupSteps - 1, state.currentStep + 1);
|
||
}, {
|
||
type: "incrementCurrentStep",
|
||
});
|
||
|
||
export const decrementCurrentStep = () => setProducedState(state => {
|
||
state.currentStep = Math.max(0, state.currentStep - 1);
|
||
}, {
|
||
type: "decrementCurrentStep",
|
||
});
|
||
|
||
export const setCurrentStep = (step: number) => setProducedState(state => {
|
||
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step));
|
||
});
|
||
|
||
export const setQuizType = (
|
||
quizId: string,
|
||
quizType: QuizConfig["type"],
|
||
) => {
|
||
updateQuiz(
|
||
quizId,
|
||
quiz => {
|
||
quiz.config.type = quizType;
|
||
},
|
||
);
|
||
};
|
||
|
||
export const setQuizStartpageType = (
|
||
quizId: string,
|
||
startpageType: QuizConfig["startpageType"],
|
||
) => {
|
||
updateQuiz(
|
||
quizId,
|
||
quiz => {
|
||
quiz.config.startpageType = startpageType;
|
||
},
|
||
);
|
||
};
|
||
|
||
const REQUEST_DEBOUNCE = 200;
|
||
const requestQueue = new RequestQueue();
|
||
let requestTimeoutId: ReturnType<typeof setTimeout>;
|
||
|
||
export const updateQuiz = async (
|
||
quizId: string | null | undefined,
|
||
updateFn: (quiz: Quiz) => void,
|
||
) => {
|
||
if (!quizId) return;
|
||
|
||
setProducedState(state => {
|
||
const quiz = state.quizes.find(q => q.id === quizId);
|
||
if (!quiz) return;
|
||
|
||
updateFn(quiz);
|
||
}, {
|
||
type: "updateQuiz",
|
||
quizId,
|
||
updateFn: updateFn.toString(),
|
||
});
|
||
|
||
clearTimeout(requestTimeoutId);
|
||
requestTimeoutId = setTimeout(async () => {
|
||
requestQueue.enqueue(async () => {
|
||
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
|
||
if (!quiz) return;
|
||
|
||
const response = await quizApi.edit(quizToEditQuizRequest(quiz));
|
||
|
||
setQuizBackendId(quizId, response.updated);
|
||
setEditQuizId(response.updated);
|
||
}).catch(error => {
|
||
if (isAxiosCanceledError(error)) return;
|
||
|
||
devlog("Error editing quiz", error, quizId);
|
||
enqueueSnackbar("Не удалось сохранить настройки квиза");
|
||
});
|
||
}, REQUEST_DEBOUNCE);
|
||
};
|
||
|
||
export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enqueue(async () => {
|
||
try {
|
||
const rawQuiz = await quizApi.create();
|
||
const quiz = rawQuizToQuiz(rawQuiz);
|
||
|
||
addQuiz(quiz);
|
||
setEditQuizId(quiz.backendId);
|
||
navigate("/edit");
|
||
|
||
await createQuestion(rawQuiz.id);
|
||
} catch (error) {
|
||
devlog("Error creating quiz", error);
|
||
|
||
const message = getMessageFromFetchError(error) ?? "";
|
||
enqueueSnackbar(`Не удалось создать квиз. ${message}`);
|
||
}
|
||
});
|
||
|
||
export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async () => {
|
||
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
|
||
if (!quiz) return;
|
||
|
||
try {
|
||
await quizApi.delete(quiz.backendId);
|
||
|
||
removeQuiz(quizId);
|
||
} catch (error) {
|
||
devlog("Error deleting quiz", error);
|
||
|
||
const message = getMessageFromFetchError(error) ?? "";
|
||
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
||
}
|
||
});
|
||
|
||
// TODO copy quiz
|
||
|
||
function setProducedState<A extends string | { type: unknown; }>(
|
||
recipe: (state: QuizStore) => void,
|
||
action?: A,
|
||
) {
|
||
useQuizStore.setState(state => produce(state, recipe), false, action);
|
||
}
|