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 = ( quizId: number, field: T, value: Quiz[T], ) => setProducedState(state => { const quiz = state.quizById[quizId]; if (!quiz) return; const oldId = quiz.id; quiz[field] = value; if (field === "id") { delete state.quizById[oldId]; state.quizById[value as number] = quiz; } }, { 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; }, { type: "incrementCurrentStep", }); export const decrementCurrentStep = () => setProducedState(state => { state.currentStep = Math.max( 1, state.currentStep - 1 ) as QuizSetupStep; }, { type: "decrementCurrentStep", }); 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 | undefined, 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 | undefined, updateFn: (quiz: Quiz) => void, navigate: NavigateFunction, rollbackOnError = true, ) => { if (!quizId) return; 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: newId } = await quizApi.edit(quizToEditQuizRequest(currentUpdatedQuiz), controller.signal); // await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected"))); setQuizField(quiz.id, "id", newId); navigate(`/setting/${newId}`, { 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( recipe: (state: QuizStore) => void, action?: A, ) { useQuizStore.setState(state => produce(state, recipe), false, action); }