feat: Default questions

This commit is contained in:
IlyaDoronin 2023-12-01 17:33:55 +03:00
parent eed127a236
commit fe641fabae
8 changed files with 791 additions and 383 deletions

@ -0,0 +1,290 @@
import type { AnyTypedQuizQuestion } from "../model/questionTypes/shared";
export const QUESTIONS_DUMMY: AnyTypedQuizQuestion[] = [
{
id: "1",
title: "",
type: "variant",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1111,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "1",
},
back: "",
originalBack: "",
autofill: false,
largeCheck: false,
multi: false,
own: false,
innerNameCheck: false,
required: false,
innerName: "",
variants: [],
},
},
{
id: "2",
title: "Вы идёте в школу",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1112,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "1",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "3",
title: "Вы немного опоздали, как поступите?",
type: "variant",
expanded: true,
required: true,
deleted: false,
deleteTimeoutId: 0,
backendId: 1113,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "2",
},
back: "",
originalBack: "",
autofill: false,
largeCheck: false,
multi: false,
own: false,
innerNameCheck: false,
required: false,
innerName: "",
variants: [
{
id: "answer1",
answer: "Извинюсь за опоздание и займу своё место",
extendedText: "",
hints: "",
},
{
id: "answer2",
answer:
"Вынесу дверь с ноги и распинав всех направлюсь к месту у розетки",
extendedText: "",
hints: "",
},
{
id: "answer3",
answer: "Влечу в кабинет, пробегу его и выпрыгну в окно",
extendedText: "",
hints: "",
},
],
},
},
{
id: "4",
title: "Вы открываете дверь кабинета, приготовившись исполнить задуманное",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1114,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [
//момент выбора куда мы пойдём
{
next: "7",
or: true,
rules: [
{
question: "2",
answers: ["answer1"],
},
],
},
{
next: "6",
or: true,
rules: [
{
question: "2",
answers: ["answer2"],
},
],
},
{
next: "5",
or: true,
rules: [
{
question: "2",
answers: ["answer3"],
},
],
},
],
default: "2",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "5",
title: "Вы умерли",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1115,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "6",
title: "Вас вызвали к директору",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1116,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "7",
title: "Вы получили отлично",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1117,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
];

@ -48,7 +48,7 @@ export type QuestionVariant = {
/** Дополнительное поле для текста, emoji, ссылки на картинку */ /** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string; extendedText: string;
/** Оригинал изображения (до кропа) */ /** Оригинал изображения (до кропа) */
originalImageUrl: string; originalImageUrl?: string;
}; };
export interface QuizQuestionBase { export interface QuizQuestionBase {

@ -77,7 +77,7 @@ export default function OptionsAndPicture({ question }: Props) {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) return openCropModal( if (variant.extendedText) return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl || ""
); );
openImageUploadModal(); openImageUploadModal();
@ -102,7 +102,7 @@ export default function OptionsAndPicture({ question }: Props) {
setSelectedVariantId(variant.id); setSelectedVariantId(variant.id);
if (variant.extendedText) return openCropModal( if (variant.extendedText) return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl || ""
); );
openImageUploadModal(); openImageUploadModal();

@ -68,7 +68,7 @@ export default function OptionsPicture({ question }: Props) {
if (variant.extendedText) { if (variant.extendedText) {
return openCropModal( return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl || ""
); );
} }
@ -95,7 +95,7 @@ export default function OptionsPicture({ question }: Props) {
if (variant.extendedText) { if (variant.extendedText) {
return openCropModal( return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl || ""
); );
} }

@ -1,100 +1,121 @@
import { useEffect } from "react";
import { import {
Box, Box,
Button, Button,
IconButton, IconButton,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions"; import {
import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions"; collapseAllQuestions,
createUntypedQuestion,
} from "@root/questions/actions";
import {
decrementCurrentStep,
incrementCurrentStep,
} from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import AddPlus from "../../assets/icons/questionsPage/addPlus"; import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import { DraggableList } from "./DraggableList"; import { DraggableList } from "./DraggableList";
import { setDefaultState } from "@root/questions/actions";
export default function QuestionsPage() { export default function QuestionsPage() {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
if (!quiz) return null; useEffect(() => {
const setDefault = ({ code }: KeyboardEvent) => {
if (code === "Backslash") {
setDefaultState(Number(quiz?.id));
}
};
return ( document.addEventListener("keydown", setDefault);
<>
<Box return () => {
sx={{ document.removeEventListener("keydown", setDefault);
maxWidth: "796px", };
width: "100%", }, []);
display: "flex",
justifyContent: "space-between", if (!quiz) return null;
margin: "60px 0 40px 0",
}} return (
> <>
<Typography variant={"h5"}>Заголовок квиза</Typography> <Box
<Button sx={{
sx={{ maxWidth: "796px",
fontSize: "16px", width: "100%",
lineHeight: "19px", display: "flex",
padding: 0, justifyContent: "space-between",
textDecoration: "underline", margin: "60px 0 40px 0",
color: theme.palette.brightPurple.main, }}
textDecorationColor: theme.palette.brightPurple.main, >
}} <Typography variant={"h5"}>Заголовок квиза</Typography>
onClick={collapseAllQuestions} <Button
> sx={{
Свернуть всё fontSize: "16px",
</Button> lineHeight: "19px",
</Box> padding: 0,
<DraggableList /> textDecoration: "underline",
<Box color: theme.palette.brightPurple.main,
sx={{ textDecorationColor: theme.palette.brightPurple.main,
display: "flex", }}
justifyContent: "space-between", onClick={collapseAllQuestions}
maxWidth: "796px", >
}} Свернуть всё
> </Button>
<IconButton </Box>
onClick={() => { <DraggableList />
createUntypedQuestion(quiz.backendId); <Box
}} sx={{
sx={{ display: "flex",
position: "fixed", justifyContent: "space-between",
left: isMobile ? "20px" : "250px", maxWidth: "796px",
bottom: "20px", }}
}} >
data-cy="create-question" <IconButton
> onClick={() => {
<AddPlus /> createUntypedQuestion(quiz.backendId);
</IconButton> }}
<Box sx={{ display: "flex", gap: "8px", marginLeft: "auto" }}> sx={{
<Button position: "fixed",
variant="outlined" left: isMobile ? "20px" : "250px",
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }} bottom: "20px",
data-cy="back-button" }}
onClick={decrementCurrentStep} data-cy="create-question"
> >
<ArrowLeft /> <AddPlus />
</Button> </IconButton>
<Button <Box sx={{ display: "flex", gap: "8px", marginLeft: "auto" }}>
variant="contained" <Button
sx={{ variant="outlined"
height: "44px", sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
padding: "10px 20px", data-cy="back-button"
borderRadius: "8px", onClick={decrementCurrentStep}
background: theme.palette.brightPurple.main, >
fontSize: "18px", <ArrowLeft />
}} </Button>
onClick={incrementCurrentStep} <Button
> variant="contained"
Следующий шаг sx={{
</Button> height: "44px",
</Box> padding: "10px 20px",
</Box> borderRadius: "8px",
{createPortal(<QuizPreview />, document.body)} background: theme.palette.brightPurple.main,
</> fontSize: "18px",
); }}
onClick={incrementCurrentStep}
>
Следующий шаг
</Button>
</Box>
</Box>
{createPortal(<QuizPreview />, document.body)}
</>
);
} }

@ -1,4 +1,3 @@
import { useState } from "react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
@ -14,7 +13,6 @@ type DateProps = {
}; };
export const Date = ({ stepNumber, question }: DateProps) => { export const Date = ({ stepNumber, question }: DateProps) => {
const [startDate, setStartDate] = useState<Date | null>(new window.Date());
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = answers.find(({ step }) => step === stepNumber) ?? {}; const { answer } = answers.find(({ step }) => step === stepNumber) ?? {};
@ -30,8 +28,8 @@ export const Date = ({ stepNumber, question }: DateProps) => {
}} }}
> >
<DatePicker <DatePicker
selected={startDate} selected={answer ? new window.Date(answer) : new window.Date()}
onChange={(date) => setStartDate(date)} onChange={(date) => updateAnswer(stepNumber, String(date))}
/> />
</Box> </Box>
</Box> </Box>

@ -79,7 +79,7 @@ export const questionStore = create<QuestionStore>()(
if (variant.extendedText.startsWith("blob:")) { if (variant.extendedText.startsWith("blob:")) {
variant.extendedText = ""; variant.extendedText = "";
} }
if (variant.originalImageUrl.startsWith("blob:")) { if (variant.originalImageUrl?.startsWith("blob:")) {
variant.originalImageUrl = ""; variant.originalImageUrl = "";
} }
}); });
@ -89,7 +89,7 @@ export const questionStore = create<QuestionStore>()(
if (variant.extendedText.startsWith("blob:")) { if (variant.extendedText.startsWith("blob:")) {
variant.extendedText = ""; variant.extendedText = "";
} }
if (variant.originalImageUrl.startsWith("blob:")) { if (variant.originalImageUrl?.startsWith("blob:")) {
variant.originalImageUrl = ""; variant.originalImageUrl = "";
} }
}); });
@ -181,7 +181,9 @@ export const setVariantOriginalImageUrl = (
if (variant.originalImageUrl === url) return; if (variant.originalImageUrl === url) return;
URL.revokeObjectURL(variant.originalImageUrl); if (variant.originalImageUrl) {
URL.revokeObjectURL(variant.originalImageUrl);
}
variant.originalImageUrl = url; variant.originalImageUrl = url;
}, { }, {
type: "setVariantOriginalImageUrl", type: "setVariantOriginalImageUrl",

@ -1,8 +1,17 @@
import { questionApi } from "@api/question"; import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui"; import { devlog } from "@frontend/kitui";
import { questionToEditQuestionRequest } from "@model/question/edit"; import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import {
import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared"; QuestionType,
RawQuestion,
rawQuestionToQuestion,
} from "@model/question/question";
import {
AnyTypedQuizQuestion,
QuestionVariant,
UntypedQuizQuestion,
createQuestionVariant,
} from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default"; import { defaultQuestionByType } from "../../constants/default";
import { produce } from "immer"; import { produce } from "immer";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@ -10,20 +19,39 @@ import { enqueueSnackbar } from "notistack";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue"; import { RequestQueue } from "../../utils/requestQueue";
import { QuestionsStore, useQuestionsStore } from "./store"; import { QuestionsStore, useQuestionsStore } from "./store";
import { QUESTIONS_DUMMY } from "../../constants/questions.dummy";
export const setDefaultState = (quizId: number) =>
setProducedState(
(state) => {
QUESTIONS_DUMMY.forEach((question) => {
state.questions.push(question);
});
},
{
type: "setDefaultState",
quizId,
}
);
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { export const setQuestions = (questions: RawQuestion[] | null) =>
const untypedQuestions = state.questions.filter(q => q.type === null); setProducedState(
(state) => {
const untypedQuestions = state.questions.filter((q) => q.type === null);
state.questions = questions?.map(rawQuestionToQuestion) ?? []; state.questions = questions?.map(rawQuestionToQuestion) ?? [];
state.questions.push(...untypedQuestions); state.questions.push(...untypedQuestions);
}, { },
type: "setQuestions", {
questions, type: "setQuestions",
}); questions,
}
);
export const createUntypedQuestion = (quizId: number) => setProducedState(state => { export const createUntypedQuestion = (quizId: number) =>
state.questions.push({ setProducedState(
(state) => {
state.questions.push({
id: nanoid(), id: nanoid(),
quizId, quizId,
type: null, type: null,
@ -31,396 +59,465 @@ export const createUntypedQuestion = (quizId: number) => setProducedState(state
description: "", description: "",
deleted: false, deleted: false,
expanded: true, expanded: true,
}); });
}, { },
type: "createUntypedQuestion", {
quizId, type: "createUntypedQuestion",
}); quizId,
}
);
const removeQuestion = (questionId: string) => setProducedState(state => { const removeQuestion = (questionId: string) =>
const index = state.questions.findIndex(q => q.id === questionId); setProducedState(
if (index === -1) return; (state) => {
const index = state.questions.findIndex((q) => q.id === questionId);
if (index === -1) return;
state.questions.splice(index, 1); state.questions.splice(index, 1);
}, { },
type: "removeQuestion", {
questionId, type: "removeQuestion",
}); questionId,
}
);
export const updateUntypedQuestion = ( export const updateUntypedQuestion = (
questionId: string, questionId: string,
updateFn: (question: UntypedQuizQuestion) => void, updateFn: (question: UntypedQuizQuestion) => void
) => { ) => {
setProducedState(state => { setProducedState(
const question = state.questions.find(q => q.id === questionId); (state) => {
if (!question) return; const question = state.questions.find((q) => q.id === questionId);
if (question.type !== null) throw new Error("Cannot update typed question, use 'updateQuestion' instead"); if (!question) return;
if (question.type !== null)
throw new Error(
"Cannot update typed question, use 'updateQuestion' instead"
);
updateFn(question); updateFn(question);
}, { },
type: "updateUntypedQuestion", {
questionId, type: "updateUntypedQuestion",
updateFn: updateFn.toString(), questionId,
}); updateFn: updateFn.toString(),
}
);
}; };
export const cleanQuestions = () => setProducedState(state => { export const cleanQuestions = () =>
state.questions = []; setProducedState(
}, { (state) => {
type: "cleanQuestions", state.questions = [];
}); },
{
type: "cleanQuestions",
}
);
const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => { const setQuestionBackendId = (questionId: string, backendId: number) =>
const question = state.questions.find(q => q.id === questionId); setProducedState(
if (!question) return; (state) => {
if (question.type === null) throw new Error("Cannot set backend id for untyped question"); const question = state.questions.find((q) => q.id === questionId);
if (!question) return;
if (question.type === null)
throw new Error("Cannot set backend id for untyped question");
question.backendId = backendId; question.backendId = backendId;
}, { },
type: "setQuestionBackendId", {
questionId: questionId, type: "setQuestionBackendId",
backendId, questionId: questionId,
}); backendId,
}
);
export const reorderQuestions = ( export const reorderQuestions = (
sourceIndex: number, sourceIndex: number,
destinationIndex: number, destinationIndex: number
) => { ) => {
if (sourceIndex === destinationIndex) return; if (sourceIndex === destinationIndex) return;
setProducedState(state => { setProducedState(
const [removed] = state.questions.splice(sourceIndex, 1); (state) => {
state.questions.splice(destinationIndex, 0, removed); const [removed] = state.questions.splice(sourceIndex, 1);
}, { state.questions.splice(destinationIndex, 0, removed);
type: "reorderQuestions", },
sourceIndex, {
destinationIndex, type: "reorderQuestions",
}); sourceIndex,
destinationIndex,
}
);
}; };
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => { export const toggleExpandQuestion = (questionId: string) =>
const question = state.questions.find(q => q.id === questionId); setProducedState(
if (!question) return; (state) => {
const question = state.questions.find((q) => q.id === questionId);
if (!question) return;
question.expanded = !question.expanded; question.expanded = !question.expanded;
}, { },
type: "toggleExpandQuestion", {
questionId, type: "toggleExpandQuestion",
}); questionId,
}
export const collapseAllQuestions = () => setProducedState(state => { );
state.questions.forEach(question => question.expanded = false);
}, "collapseAllQuestions");
export const collapseAllQuestions = () =>
setProducedState((state) => {
state.questions.forEach((question) => (question.expanded = false));
}, "collapseAllQuestions");
const REQUEST_DEBOUNCE = 200; const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue(); const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>; let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = ( export const updateQuestion = (
questionId: string, questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void, updateFn: (question: AnyTypedQuizQuestion) => void
) => { ) => {
setProducedState(state => { setProducedState(
const question = state.questions.find(q => q.id === questionId); (state) => {
if (!question) return; const question = state.questions.find((q) => q.id === questionId);
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead"); if (!question) return;
if (question.type === null)
throw new Error(
"Cannot update untyped question, use 'updateUntypedQuestion' instead"
);
updateFn(question); updateFn(question);
}, { },
type: "updateQuestion", {
questionId, type: "updateQuestion",
updateFn: updateFn.toString(), questionId,
}); updateFn: updateFn.toString(),
}
);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(() => { requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => { requestQueue
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId); .enqueue(async () => {
if (!q) return; const q = useQuestionsStore
if (q.type === null) throw new Error("Cannot send update request for untyped question"); .getState()
.questions.find((q) => q.id === questionId);
if (!q) return;
if (q.type === null)
throw new Error("Cannot send update request for untyped question");
const response = await questionApi.edit(questionToEditQuestionRequest(q)); const response = await questionApi.edit(
questionToEditQuestionRequest(q)
);
setQuestionBackendId(questionId, response.updated); setQuestionBackendId(questionId, response.updated);
}).catch(error => { })
if (isAxiosCanceledError(error)) return; .catch((error) => {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId }); devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос"); enqueueSnackbar("Не удалось сохранить вопрос");
}); });
}, REQUEST_DEBOUNCE); }, REQUEST_DEBOUNCE);
}; };
export const addQuestionVariant = (questionId: string) => { export const addQuestionVariant = (questionId: string) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
switch (question.type) { switch (question.type) {
case "variant": case "variant":
case "emoji": case "emoji":
case "select": case "select":
case "images": case "images":
case "varimg": case "varimg":
question.content.variants.push(createQuestionVariant()); question.content.variants.push(createQuestionVariant());
break; break;
default: throw new Error(`Cannot add variant to question of type "${question.type}"`); default:
} throw new Error(
}); `Cannot add variant to question of type "${question.type}"`
);
}
});
}; };
export const deleteQuestionVariant = (questionId: string, variantId: string) => { export const deleteQuestionVariant = (
updateQuestion(questionId, question => { questionId: string,
if (!("variants" in question.content)) return; variantId: string
) => {
updateQuestion(questionId, (question) => {
if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId); const variantIndex = question.content.variants.findIndex(
if (variantIndex === -1) return; (variant) => variant.id === variantId
);
if (variantIndex === -1) return;
question.content.variants.splice(variantIndex, 1); question.content.variants.splice(variantIndex, 1);
}); });
}; };
export const setQuestionVariantField = ( export const setQuestionVariantField = (
questionId: string, questionId: string,
variantId: string, variantId: string,
field: keyof QuestionVariant, field: keyof QuestionVariant,
value: QuestionVariant[keyof QuestionVariant], value: QuestionVariant[keyof QuestionVariant]
) => { ) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId); const variantIndex = question.content.variants.findIndex(
if (variantIndex === -1) return; (variant) => variant.id === variantId
);
if (variantIndex === -1) return;
const variant = question.content.variants[variantIndex]; const variant = question.content.variants[variantIndex];
variant[field] = value;
}); if (value) {
variant[field] = value;
}
});
}; };
export const reorderQuestionVariants = ( export const reorderQuestionVariants = (
questionId: string, questionId: string,
sourceIndex: number, sourceIndex: number,
destinationIndex: number, destinationIndex: number
) => { ) => {
if (sourceIndex === destinationIndex) return; if (sourceIndex === destinationIndex) return;
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
const [removed] = question.content.variants.splice(sourceIndex, 1); const [removed] = question.content.variants.splice(sourceIndex, 1);
question.content.variants.splice(destinationIndex, 0, removed); question.content.variants.splice(destinationIndex, 0, removed);
});
});
}; };
export const setQuestionBackgroundImage = ( export const setQuestionBackgroundImage = (questionId: string, url: string) => {
questionId: string, updateQuestion(questionId, (question) => {
url: string, if (question.content.back === url) return;
) => {
updateQuestion(questionId, question => {
if (question.content.back === url) return;
if ( if (question.content.back !== question.content.originalBack)
question.content.back !== question.content.originalBack URL.revokeObjectURL(question.content.back);
) URL.revokeObjectURL(question.content.back); question.content.back = url;
question.content.back = url; });
});
}; };
export const setQuestionOriginalBackgroundImage = ( export const setQuestionOriginalBackgroundImage = (
questionId: string, questionId: string,
url: string, url: string
) => { ) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (question.content.originalBack === url) return; if (question.content.originalBack === url) return;
URL.revokeObjectURL(question.content.originalBack); URL.revokeObjectURL(question.content.originalBack);
question.content.originalBack = url; question.content.originalBack = url;
}); });
}; };
export const setVariantImageUrl = ( export const setVariantImageUrl = (
questionId: string, questionId: string,
variantId: string, variantId: string,
url: string, url: string
) => { ) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
const variant = question.content.variants.find(variant => variant.id === variantId); const variant = question.content.variants.find(
if (!variant) return; (variant) => variant.id === variantId
);
if (!variant) return;
if (variant.extendedText === url) return; if (variant.extendedText === url) return;
if (variant.extendedText !== variant.originalImageUrl) URL.revokeObjectURL(variant.extendedText); if (variant.extendedText !== variant.originalImageUrl)
variant.extendedText = url; URL.revokeObjectURL(variant.extendedText);
}); variant.extendedText = url;
});
}; };
export const setVariantOriginalImageUrl = ( export const setVariantOriginalImageUrl = (
questionId: string, questionId: string,
variantId: string, variantId: string,
url: string, url: string
) => { ) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
const variant = question.content.variants.find( const variant = question.content.variants.find(
variant => variant.id === variantId (variant) => variant.id === variantId
) as QuestionVariant | undefined; ) as QuestionVariant | undefined;
if (!variant) return; if (!variant) return;
if (variant.originalImageUrl === url) return; if (variant.originalImageUrl === url) return;
URL.revokeObjectURL(variant.originalImageUrl); if (variant.originalImageUrl) {
variant.originalImageUrl = url; URL.revokeObjectURL(variant.originalImageUrl);
}); }
variant.originalImageUrl = url;
});
}; };
export const setPageQuestionPicture = ( export const setPageQuestionPicture = (questionId: string, url: string) => {
questionId: string, updateQuestion(questionId, (question) => {
url: string, if (question.type !== "page") return;
) => {
updateQuestion(questionId, question => {
if (question.type !== "page") return;
if (question.content.picture === url) return; if (question.content.picture === url) return;
if ( if (question.content.picture !== question.content.originalPicture)
question.content.picture !== question.content.originalPicture URL.revokeObjectURL(question.content.picture);
) URL.revokeObjectURL(question.content.picture); question.content.picture = url;
question.content.picture = url; });
});
}; };
export const setPageQuestionOriginalPicture = ( export const setPageQuestionOriginalPicture = (
questionId: string, questionId: string,
url: string, url: string
) => { ) => {
updateQuestion(questionId, question => { updateQuestion(questionId, (question) => {
if (question.type !== "page") return; if (question.type !== "page") return;
if (question.content.originalPicture === url) return; if (question.content.originalPicture === url) return;
URL.revokeObjectURL(question.content.originalPicture); URL.revokeObjectURL(question.content.originalPicture);
question.content.originalPicture = url; question.content.originalPicture = url;
}); });
}; };
export const setQuestionInnerName = ( export const setQuestionInnerName = (questionId: string, name: string) => {
questionId: string, updateQuestion(questionId, (question) => {
name: string, question.content.innerName = name;
) => { });
updateQuestion(questionId, question => {
question.content.innerName = name;
});
}; };
export const changeQuestionType = ( export const changeQuestionType = (questionId: string, type: QuestionType) => {
questionId: string, updateQuestion(questionId, (question) => {
type: QuestionType, question.type = type;
) => { question.content = defaultQuestionByType[type].content;
updateQuestion(questionId, question => { });
question.type = type;
question.content = defaultQuestionByType[type].content;
});
}; };
export const createTypedQuestion = async ( export const createTypedQuestion = async (
questionId: string, questionId: string,
type: QuestionType, type: QuestionType
) => requestQueue.enqueue(async () => { ) =>
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); requestQueue.enqueue(async () => {
const question = useQuestionsStore
.getState()
.questions.find((q) => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question"); if (question.type !== null)
throw new Error("Cannot upgrade already typed question");
try { try {
const createdQuestion = await questionApi.create({ const createdQuestion = await questionApi.create({
quiz_id: question.quizId, quiz_id: question.quizId,
type, type,
title: question.title, title: question.title,
description: question.description, description: question.description,
page: 0, page: 0,
required: true, required: true,
content: JSON.stringify(defaultQuestionByType[type].content), content: JSON.stringify(defaultQuestionByType[type].content),
}); });
setProducedState(state => { setProducedState(
const questionIndex = state.questions.findIndex(q => q.id === questionId); (state) => {
if (questionIndex !== -1) state.questions.splice( const questionIndex = state.questions.findIndex(
questionIndex, (q) => q.id === questionId
1, );
rawQuestionToQuestion(createdQuestion) if (questionIndex !== -1)
state.questions.splice(
questionIndex,
1,
rawQuestionToQuestion(createdQuestion)
); );
}, { },
type: "createTypedQuestion", {
question, type: "createTypedQuestion",
}); question,
}
);
} catch (error) { } catch (error) {
devlog("Error creating question", error); devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос"); enqueueSnackbar("Не удалось создать вопрос");
} }
}); });
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => { export const deleteQuestion = async (questionId: string) =>
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); requestQueue.enqueue(async () => {
const question = useQuestionsStore
.getState()
.questions.find((q) => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type === null) { if (question.type === null) {
removeQuestion(questionId); removeQuestion(questionId);
return; return;
} }
try { try {
await questionApi.delete(question.backendId); await questionApi.delete(question.backendId);
removeQuestion(questionId); removeQuestion(questionId);
} catch (error) { } catch (error) {
devlog("Error deleting question", error); devlog("Error deleting question", error);
enqueueSnackbar("Не удалось удалить вопрос"); enqueueSnackbar("Не удалось удалить вопрос");
} }
}); });
export const copyQuestion = async (questionId: string, quizId: number) => requestQueue.enqueue(async () => { export const copyQuestion = async (questionId: string, quizId: number) =>
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); requestQueue.enqueue(async () => {
const question = useQuestionsStore
.getState()
.questions.find((q) => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type === null) { if (question.type === null) {
const copiedQuestion = structuredClone(question); const copiedQuestion = structuredClone(question);
copiedQuestion.id = nanoid(); copiedQuestion.id = nanoid();
setProducedState(state => { setProducedState(
state.questions.push(copiedQuestion); (state) => {
}, { state.questions.push(copiedQuestion);
type: "copyQuestion", },
questionId, {
quizId, type: "copyQuestion",
}); questionId,
quizId,
}
);
return; return;
} }
try { try {
const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId); const { updated: newQuestionId } = await questionApi.copy(
question.backendId,
quizId
);
const copiedQuestion = structuredClone(question); const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId; copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = nanoid(); copiedQuestion.id = nanoid();
setProducedState(state => { setProducedState(
state.questions.push(copiedQuestion); (state) => {
}, { state.questions.push(copiedQuestion);
type: "copyQuestion", },
questionId, {
quizId, type: "copyQuestion",
}); questionId,
quizId,
}
);
} catch (error) { } catch (error) {
devlog("Error copying question", error); devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос"); enqueueSnackbar("Не удалось скопировать вопрос");
} }
}); });
function setProducedState<A extends string | { type: unknown; }>( function setProducedState<A extends string | { type: unknown }>(
recipe: (state: QuestionsStore) => void, recipe: (state: QuestionsStore) => void,
action?: A, action?: A
) { ) {
useQuestionsStore.setState(state => produce(state, recipe), false, action); useQuestionsStore.setState((state) => produce(state, recipe), false, action);
} }