import { questionApi } from "@api/question"; import { devlog } from "@frontend/kitui"; import { produce } from "immer"; import { Question } from "model/question/question"; import { enqueueSnackbar } from "notistack"; import { isAxiosCanceledError } from "utils/isAxiosCanceledError"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; type QuestionsStore = { questionsById: Record; }; const initialState: QuestionsStore = { questionsById: {}, }; export const useQuestionsStore = create()( devtools( () => initialState, { name: "QuestionsStore", enabled: process.env.NODE_ENV === "development", } ) ); export const setQuestions = (questions: QuestionsStore["questionsById"]) => useQuestionsStore.setState({ questionsById: questions }); export const setQuestion = (question: Question) => setProducedState(state => { state.questionsById[question.id] = question; }, { type: "setQuestion", question, }); export const setQuestionField = ( questionId: number, field: T, value: Question[T], ) => setProducedState(state => { const question = state.questionsById[questionId]; if (!question) return; question[field] = value; }, { type: "setQuestionField", questionId, field, value, }); let savedOriginalQuestion: Question | null = null; let controller: AbortController | null = null; export const setQuestionFieldOptimistic = async ( questionId: number, field: T, value: Question[T], ) => { const question = useQuestionsStore.getState().questionsById[questionId] ?? null; if (!question) return; const currentUpdatedQuestion = produce(question, draft => { draft[field] = value; }); controller?.abort(); controller = new AbortController(); savedOriginalQuestion ??= question; setQuestion(currentUpdatedQuestion); try { const { updated } = await questionApi.edit(currentUpdatedQuestion, controller.signal); // await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected"))); setQuestionField(question.id, "version", updated); controller = null; savedOriginalQuestion = null; } catch (error) { if (isAxiosCanceledError(error)) return; devlog("Error editing question", { error, question: question, currentUpdatedQuestion }); enqueueSnackbar("Не удалось сохранить вопрос"); if (!savedOriginalQuestion) { devlog("Cannot rollback question"); throw new Error("Cannot rollback question"); } setQuestion(savedOriginalQuestion); controller = null; savedOriginalQuestion = null; } }; export const updateQuestionWithFn = ( questionId: number, updateFn: (question: Question) => void, ) => setProducedState(state => { const question = state.questionsById[questionId]; if (!question) return; updateFn(question); }, { type: "updateQuestion", questionId, updateFn: updateFn.toString(), }); export const createQuestion = async () => { try { const question = await questionApi.create(); setQuestion(question); } catch (error) { devlog("Error creating question", error); enqueueSnackbar("Не удалось создать вопрос"); } }; export const deleteQuestion = async (questionId: number) => { try { await questionApi.delete(questionId); removeQuestion(questionId); } catch (error) { devlog("Error deleting question", error); enqueueSnackbar("Не удалось удалить вопрос"); } }; export const removeQuestion = (questionId: number) => setProducedState(state => { delete state.questionsById[questionId]; }, { type: "removeQuestion", questionId, }); function setProducedState( recipe: (state: QuestionsStore) => void, action?: A, ) { useQuestionsStore.setState(state => produce(state, recipe), false, action); }