fix edit requests logic & refactor

This commit is contained in:
nflnkr 2023-11-28 02:07:24 +03:00
parent 0ba8472220
commit cbfa4a13d8
74 changed files with 493 additions and 505 deletions

@ -71,7 +71,7 @@ function addQuizImages(quizId: number, image: Blob) {
return makeRequest<FormData, never>({ return makeRequest<FormData, never>({
url: `${baseUrl}/quiz/putImages`, url: `${baseUrl}/quiz/putImages`,
body: formData, body: formData,
method: "POST", method: "PUT",
}); });
} }
@ -100,7 +100,6 @@ const defaultCreateQuizBody: CreateQuizRequest = {
"due_to": 0, "due_to": 0,
"time_of_passing": 0, "time_of_passing": 0,
"pausable": false, "pausable": false,
"question_cnt": 0,
"super": false, "super": false,
"group_id": 0, "group_id": 0,
}; };

@ -1,7 +1,7 @@
import type { QuizQuestionBase } from "../model/questionTypes/shared"; import type { QuizQuestionBase } from "../model/questionTypes/shared";
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "fixedId"> = { export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
quizId: 0, quizId: 0,
description: "", description: "",
page: 0, page: 0,

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionDate } from "../model/questionTypes/date"; import type { QuizQuestionDate } from "../model/questionTypes/date";
export const QUIZ_QUESTION_DATE: Omit<QuizQuestionDate, "id" | "fixedId"> = { export const QUIZ_QUESTION_DATE: Omit<QuizQuestionDate, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "date", type: "date",
content: { content: {

@ -13,7 +13,7 @@ import { QUIZ_QUESTION_VARIANT } from "./variant";
import { QUIZ_QUESTION_VARIMG } from "./varimg"; import { QUIZ_QUESTION_VARIMG } from "./varimg";
export const defaultQuestionByType: Record<QuestionType, Omit<AnyQuizQuestion, "id" | "fixedId">> = { export const defaultQuestionByType: Record<QuestionType, Omit<AnyQuizQuestion, "id" | "backendId">> = {
"date": QUIZ_QUESTION_DATE, "date": QUIZ_QUESTION_DATE,
"emoji": QUIZ_QUESTION_EMOJI, "emoji": QUIZ_QUESTION_EMOJI,
"file": QUIZ_QUESTION_FILE, "file": QUIZ_QUESTION_FILE,

@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionEmoji } from "../model/questionTypes/emoji"; import type { QuizQuestionEmoji } from "../model/questionTypes/emoji";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id" | "fixedId"> = { export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "emoji", type: "emoji",
content: { content: {

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionFile } from "../model/questionTypes/file"; import type { QuizQuestionFile } from "../model/questionTypes/file";
export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id" | "fixedId"> = { export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "file", type: "file",
content: { content: {

@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionImages } from "../model/questionTypes/images"; import type { QuizQuestionImages } from "../model/questionTypes/images";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id" | "fixedId"> = { export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "images", type: "images",
content: { content: {

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionNumber } from "../model/questionTypes/number"; import type { QuizQuestionNumber } from "../model/questionTypes/number";
export const QUIZ_QUESTION_NUMBER: Omit<QuizQuestionNumber, "id" | "fixedId"> = { export const QUIZ_QUESTION_NUMBER: Omit<QuizQuestionNumber, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "number", type: "number",
content: { content: {

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionPage } from "../model/questionTypes/page"; import type { QuizQuestionPage } from "../model/questionTypes/page";
export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id" | "fixedId"> = { export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "page", type: "page",
content: { content: {

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionRating } from "../model/questionTypes/rating"; import type { QuizQuestionRating } from "../model/questionTypes/rating";
export const QUIZ_QUESTION_RATING: Omit<QuizQuestionRating, "id" | "fixedId"> = { export const QUIZ_QUESTION_RATING: Omit<QuizQuestionRating, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "rating", type: "rating",
content: { content: {

@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionSelect } from "../model/questionTypes/select"; import type { QuizQuestionSelect } from "../model/questionTypes/select";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id" | "fixedId"> = { export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "select", type: "select",
content: { content: {

@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionText } from "../model/questionTypes/text"; import type { QuizQuestionText } from "../model/questionTypes/text";
export const QUIZ_QUESTION_TEXT: Omit<QuizQuestionText, "id" | "fixedId"> = { export const QUIZ_QUESTION_TEXT: Omit<QuizQuestionText, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "text", type: "text",
content: { content: {

@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVariant } from "../model/questionTypes/variant"; import type { QuizQuestionVariant } from "../model/questionTypes/variant";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id" | "fixedId"> = { export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "variant", type: "variant",
content: { content: {

@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVarImg } from "../model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "../model/questionTypes/varimg";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id" | "fixedId"> = { export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE, ...QUIZ_QUESTION_BASE,
type: "varimg", type: "varimg",
content: { content: {

@ -16,9 +16,9 @@ export interface EditQuestionResponse {
updated: number; updated: number;
} }
export function questionToEditQuestionRequest(question: AnyQuizQuestion, newId?: number): EditQuestionRequest { export function questionToEditQuestionRequest(question: AnyQuizQuestion): EditQuestionRequest {
return { return {
id: newId ?? question.id, id: question.backendId,
title: question.title, title: question.title,
desc: question.description, desc: question.description,
type: question.type, type: question.type,

@ -1,5 +1,6 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyQuizQuestion } from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default"; import { defaultQuestionByType } from "../../constants/default";
import { nanoid } from "nanoid";
export type QuestionType = export type QuestionType =
@ -53,8 +54,8 @@ export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion
} }
return { return {
id: rawQuestion.id, backendId: rawQuestion.id,
fixedId: rawQuestion.id, id: nanoid(),
description: rawQuestion.description, description: rawQuestion.description,
page: rawQuestion.page, page: rawQuestion.page,
quizId: rawQuestion.quiz_id, quizId: rawQuestion.quiz_id,

@ -48,9 +48,9 @@ export interface ImageQuestionVariant extends QuestionVariant {
} }
export interface QuizQuestionBase { export interface QuizQuestionBase {
id: number; backendId: number;
/** fixed id for using it as a key prop */ /** Stable id, generated on client */
fixedId: number; id: string;
quizId: number; quizId: number;
title: string; title: string;
description: string; description: string;

@ -26,7 +26,7 @@ export interface CreateQuizRequest {
/** true if it is allowed for pause quiz */ /** true if it is allowed for pause quiz */
pausable: boolean; pausable: boolean;
/** count of questions */ /** count of questions */
question_cnt: number; question_cnt?: number;
/** set true if squiz realize group functionality */ /** set true if squiz realize group functionality */
super: boolean; super: boolean;
/** group of new quiz */ /** group of new quiz */

@ -43,9 +43,9 @@ export interface EditQuizResponse {
updated: number; updated: number;
} }
export function quizToEditQuizRequest(quiz: Quiz, newId?: number): EditQuizRequest { export function quizToEditQuizRequest(quiz: Quiz): EditQuizRequest {
return { return {
id: newId ?? quiz.id, id: quiz.backendId,
fp: quiz.fingerprinting, fp: quiz.fingerprinting,
rep: quiz.repeatable, rep: quiz.repeatable,
note_prevented: quiz.note_prevented, note_prevented: quiz.note_prevented,

@ -1,9 +1,12 @@
import { QuizConfig, defaultQuizConfig } from "@model/quizSettings"; import { QuizConfig, defaultQuizConfig } from "@model/quizSettings";
import { nanoid } from "nanoid";
export interface Quiz { export interface Quiz {
/** Stable id, generated on client */
id: string;
/** Id of created quiz */ /** Id of created quiz */
id: number; backendId: number;
/** string id for customers */ /** string id for customers */
qid: string; qid: string;
/** true if quiz deleted */ /** true if quiz deleted */
@ -112,13 +115,6 @@ export interface RawQuiz {
group_id: number; group_id: number;
} }
export function quizToRawQuiz(quiz: Quiz): RawQuiz {
return {
...quiz,
config: JSON.stringify(quiz.config),
};
}
export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz { export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz {
let config = defaultQuizConfig; let config = defaultQuizConfig;
@ -131,5 +127,7 @@ export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz {
return { return {
...rawQuiz, ...rawQuiz,
config, config,
backendId: rawQuiz.id,
id: nanoid(),
}; };
} }

@ -1,31 +1,38 @@
export const quizSetupSteps = { import ChartPieIcon from "@icons/ChartPieIcon";
1: { displayStep: 1, text: "Настройка стартовой страницы" }, import ContactBookIcon from "@icons/ContactBookIcon";
2: { displayStep: 1, text: "Настройка стартовой страницы" }, import FlowArrowIcon from "@icons/FlowArrowIcon";
3: { displayStep: 1, text: "Настройка стартовой страницы" }, import LayoutIcon from "@icons/LayoutIcon";
4: { displayStep: 2, text: "Задайте вопросы" }, import MegaphoneIcon from "@icons/MegaphoneIcon";
5: { displayStep: 3, text: "Настройте авторезультаты" }, import QuestionIcon from "@icons/QuestionIcon";
6: { displayStep: 3, text: "Настройте авторезультаты" }, import QuestionsMapIcon from "@icons/QuestionsMapIcon";
7: { displayStep: 4, text: "Оценка графа карты вопросов" },
8: { displayStep: 5, text: "Настройте форму контактов" },
9: { displayStep: 6, text: "Установите квиз" },
10: { displayStep: 7, text: "Запустите рекламу" },
} as const;
export const maxQuizSetupSteps = Math.max(...Object.keys(quizSetupSteps).map(n => parseInt(n)));
export const maxDisplayQuizSetupSteps = Math.max(...Object.values(quizSetupSteps).map(v => v.displayStep)); export const quizSetupSteps = [
{ stepperText: "Настройка стартовой страницы", sidebarText: "Стартовая страница", sidebarIcon: LayoutIcon },
{ stepperText: "Задайте вопросы", sidebarText: "Вопросы", sidebarIcon: QuestionIcon },
{ stepperText: "Настройте авторезультаты", sidebarText: "Результаты", sidebarIcon: ChartPieIcon },
{ stepperText: "Оценка графа карты вопросов", sidebarText: "Карта вопросов", sidebarIcon: QuestionsMapIcon },
{ stepperText: "Настройте форму контактов", sidebarText: "Форма контактов", sidebarIcon: ContactBookIcon },
{ stepperText: "Установите квиз", sidebarText: "Установка квиза", sidebarIcon: FlowArrowIcon },
{ stepperText: "Запустите рекламу", sidebarText: "Запуск рекламы", sidebarIcon: MegaphoneIcon },
] as const;
export type QuizSetupStep = keyof typeof quizSetupSteps; export const maxQuizSetupSteps = quizSetupSteps.length;
export type QuizStartpageType = "standard" | "expanded" | "centered"; export type QuizStartpageType = "standard" | "expanded" | "centered" | null;
export type QuizStartpageAlignType = "left" | "right" | "center"; export type QuizStartpageAlignType = "left" | "right" | "center";
export type QuizType = "quiz" | "form" | null;
export type QuizResultsType = true | null;
export interface QuizConfig { export interface QuizConfig {
type: "quiz" | "form"; type: QuizType;
logo: string; logo: string;
noStartPage: boolean; noStartPage: boolean;
startpageType: QuizStartpageType; startpageType: QuizStartpageType;
results: QuizResultsType;
startpage: { startpage: {
description: string; description: string;
button: string; button: string;
@ -49,19 +56,20 @@ export interface QuizConfig {
} }
export const defaultQuizConfig: QuizConfig = { export const defaultQuizConfig: QuizConfig = {
type: "quiz", type: null,
logo: "", logo: "",
noStartPage: false, noStartPage: false,
startpageType: "standard", startpageType: null,
results: null,
startpage: { startpage: {
description: "", description: "",
button: "", button: "",
position: "left", position: "left",
background: { background: {
type: null, type: null,
desktop: "", desktop: "https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg",
mobile: "", mobile: "https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png",
video: "", video: "https://youtu.be/dbaPkCiLPKQ",
cycle: false, cycle: false,
}, },
}, },

@ -22,7 +22,7 @@ import type { ImageQuestionVariant, QuestionVariant } from "../../../model/quest
type AnswerItemProps = { type AnswerItemProps = {
index: number; index: number;
questionId: number; questionId: string;
variant: QuestionVariant | ImageQuestionVariant; variant: QuestionVariant | ImageQuestionVariant;
largeCheck: boolean; largeCheck: boolean;
additionalContent?: ReactNode; additionalContent?: ReactNode;
@ -40,11 +40,7 @@ export const AnswerItem = ({
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(790)); const isTablet = useMediaQuery(theme.breakpoints.down(790));
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const setQuestionVariantAnswer = useDebouncedCallback((value) => {
setQuestionVariantField(questionId, variant.id, "answer", value);
}, 1000);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@ -76,7 +72,9 @@ export const AnswerItem = ({
focused={false} focused={false}
placeholder={"Добавьте ответ"} placeholder={"Добавьте ответ"}
multiline={largeCheck} multiline={largeCheck}
onChange={({ target }) => setQuestionVariantAnswer(target.value)} onChange={({ target }) => {
setQuestionVariantField(questionId, variant.id, "answer", target.value)
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (event.code === "Enter" && !largeCheck) { if (event.code === "Enter" && !largeCheck) {
addQuestionVariant(questionId); addQuestionVariant(questionId);

@ -11,7 +11,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { copyQuestion, deleteQuestion, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { copyQuestion, deleteQuestion, updateQuestion } from "@root/questions/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting"; import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon"; import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching"; import Branching from "../../assets/icons/questionsPage/branching";
@ -38,7 +38,7 @@ export default function ButtonsOptions({
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const openedModal = () => { const openedModal = () => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.openedModalSettings = true; question.openedModalSettings = true;
}); });
}; };
@ -279,6 +279,7 @@ export default function ButtonsOptions({
deleteQuestion(question.id); deleteQuestion(question.id);
}} }}
data-cy="delete-question"
> >
<DeleteIcon color={"#4D4D4D"} /> <DeleteIcon color={"#4D4D4D"} />
</IconButton> </IconButton>

@ -10,7 +10,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { copyQuestion, deleteQuestion, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { copyQuestion, deleteQuestion, updateQuestion } from "@root/questions/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting"; import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal"; import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -184,7 +184,7 @@ export default function ButtonsOptionsAndPict({
onMouseLeave={() => setButtonHover("")} onMouseLeave={() => setButtonHover("")}
onClick={() => { onClick={() => {
SSHC("branching"); SSHC("branching");
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.openedModalSettings = true; question.openedModalSettings = true;
}); });
}} }}
@ -320,6 +320,7 @@ export default function ButtonsOptionsAndPict({
deleteQuestion(question.id); deleteQuestion(question.id);
}} }}
data-cy="delete-question"
> >
<DeleteIcon style={{ color: "#4D4D4D" }} /> <DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton> </IconButton>

@ -1,5 +1,5 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -19,7 +19,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -48,7 +48,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Выбор диапазона дат"} label={"Выбор диапазона дат"}
checked={question.content.dateRange} checked={question.content.dateRange}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "date") return; if (question.type !== "date") return;
question.content.dateRange = target.checked; question.content.dateRange = target.checked;
@ -60,7 +60,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Выбор времени"} label={"Выбор времени"}
checked={question.content.time} checked={question.content.time}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "date") return; if (question.type !== "date") return;
question.content.time = target.checked; question.content.time = target.checked;
@ -88,7 +88,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.required = !target.checked; question.required = !target.checked;
}); });
}} }}
@ -109,7 +109,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : ""; question.content.innerName = target.checked ? question.content.innerName : "";
}); });

@ -29,7 +29,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { copyQuestion, createQuestion, deleteQuestion, toggleExpandQuestion, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { copyQuestion, createQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion } from "@root/questions/actions";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -54,7 +54,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
const anchorRef = useRef(null); const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.title = title; question.title = title;
}); });
}, 200); }, 200);
@ -250,6 +250,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
deleteQuestion(question.id); deleteQuestion(question.id);
}} }}
data-cy="delete-question"
> >
<DeleteIcon <DeleteIcon
style={{ color: theme.palette.brightPurple.main }} style={{ color: theme.palette.brightPurple.main }}
@ -317,6 +318,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
alignItems: "center", alignItems: "center",
columnGap: "10px", columnGap: "10px",
}} }}
data-cy="create-question"
> >
<Box <Box
sx={{ sx={{

@ -13,8 +13,8 @@ import DraggableListItem from "./DraggableListItem";
export const DraggableList = () => { export const DraggableList = () => {
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), { const { isLoading } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions, onSuccess: setQuestions,
onError: error => { onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : ""; const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
@ -29,6 +29,8 @@ export const DraggableList = () => {
if (destination) reorderQuestions(source.index, destination.index); if (destination) reorderQuestions(source.index, destination.index);
}; };
if (isLoading && !questions) return <Box>Загрузка вопросов...</Box>;
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable-list"> <Droppable droppableId="droppable-list">
@ -36,7 +38,7 @@ export const DraggableList = () => {
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{questions.map((question, index) => ( {questions.map((question, index) => (
<DraggableListItem <DraggableListItem
key={question.fixedId} key={question.id}
question={question} question={question}
isDragging={snapshot.isDraggingOver} isDragging={snapshot.isDraggingOver}
index={index} index={index}

@ -5,7 +5,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -24,15 +24,15 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
const debounceAnswer = useDebouncedCallback((value) => { const debounceAnswer = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "select") return; if (question.type !== "select") return;
question.content.default = value; question.content.default = value;
}); });
}, 1000); }, 200);
return ( return (
<> <>
@ -73,7 +73,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "select") return; if (question.type !== "select") return;
question.content.multi = target.checked; question.content.multi = target.checked;
@ -130,7 +130,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.required = !e.target.checked; question.required = !e.target.checked;
}); });
}} }}
@ -141,7 +141,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : ""; question.content.innerName = target.checked ? question.content.innerName : "";
}); });

@ -9,7 +9,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { addQuestionVariant, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { addQuestionVariant, updateQuestion } from "@root/questions/actions";
import { EmojiPicker } from "@ui_kit/EmojiPicker"; import { EmojiPicker } from "@ui_kit/EmojiPicker";
import { useState } from "react"; import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
@ -181,7 +181,7 @@ export default function Emoji({ question }: Props) {
<EmojiPicker <EmojiPicker
onEmojiSelect={({ native }) => { onEmojiSelect={({ native }) => {
setOpen(false); setOpen(false);
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "emoji") return; if (question.type !== "emoji") return;
const variant = question.content.variants.find(v => v.id === selectedVariant); const variant = question.content.variants.find(v => v.id === selectedVariant);

@ -1,5 +1,5 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -20,7 +20,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -50,7 +50,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
label={"Можно несколько"} label={"Можно несколько"}
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "emoji") return; if (question.type !== "emoji") return;
question.content.multi = target.checked; question.content.multi = target.checked;
@ -60,7 +60,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "emoji") return; if (question.type !== "emoji") return;
question.content.own = target.checked; question.content.own = target.checked;
@ -86,7 +86,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={(e) => updateQuestion(question.id, question => {
if (question.type !== "emoji") return; if (question.type !== "emoji") return;
question.content.required = !e.target.checked; question.content.required = !e.target.checked;
@ -107,7 +107,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : ""; question.content.innerName = target.checked ? question.content.innerName : "";
})} })}

@ -14,7 +14,7 @@ import {
import { useState } from "react"; import { useState } from "react";
import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions"; import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import type { RefObject } from "react"; import type { RefObject } from "react";
import type { AnyQuizQuestion } from "../../../../model/questionTypes/shared"; import type { AnyQuizQuestion } from "../../../../model/questionTypes/shared";
@ -118,7 +118,7 @@ export const ChooseAnswerModal = ({
onClick={() => { onClick={() => {
setOpenModal(false); setOpenModal(false);
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.type = selectedValue; question.type = selectedValue;
}); });
}} }}

@ -1,5 +1,5 @@
import { Box, ListItem, Typography, useTheme } from "@mui/material"; import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import { memo } from "react"; import { memo } from "react";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
import { AnyQuizQuestion, QuizQuestionBase } from "../../../../model/questionTypes/shared"; import { AnyQuizQuestion, QuizQuestionBase } from "../../../../model/questionTypes/shared";
@ -46,7 +46,7 @@ export default memo(
</Typography> </Typography>
<Typography <Typography
onClick={() => { onClick={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.deleted = false; question.deleted = false;
}); });
}} }}

@ -12,7 +12,7 @@ import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating"; import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider"; import Slider from "@icons/questionsPage/slider";
import { Box, InputAdornment, Paper } from "@mui/material"; import { Box, InputAdornment, Paper } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
@ -37,10 +37,10 @@ export default function QuestionsPageCard({
const anchorRef = useRef(null); const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.title = title; question.title = title;
}); });
}, 1000); }, 200);
return ( return (
<> <>

@ -13,8 +13,8 @@ import { enqueueSnackbar } from "notistack";
export const FormDraggableList = () => { export const FormDraggableList = () => {
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), { useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions, onSuccess: setQuestions,
onError: error => { onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : ""; const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
@ -36,7 +36,7 @@ export const FormDraggableList = () => {
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{questions.map((question, index) => ( {questions.map((question, index) => (
<FormDraggableListItem <FormDraggableListItem
key={question.fixedId} key={question.id}
question={question} question={question}
questionIndex={index} questionIndex={index}
questionData={question} questionData={question}

@ -11,7 +11,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
export default function FormQuestionsPage() { export default function FormQuestionsPage() {
const theme = useTheme(); const theme = useTheme();
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
if (!quiz) return null; if (!quiz) return null;
@ -68,8 +68,9 @@ export default function FormQuestionsPage() {
}, },
}} }}
onClick={() => { onClick={() => {
createQuestion(quiz.id); createQuestion(quiz.backendId);
}} }}
data-cy="create-question"
> >
<AddAnswer color="#EEE4FC" /> <AddAnswer color="#EEE4FC" />
<Typography sx={{ color: "#9A9AAF" }}> <Typography sx={{ color: "#9A9AAF" }}>

@ -17,7 +17,7 @@ import type {
AnyQuizQuestion, AnyQuizQuestion,
} from "../../../model/questionTypes/shared"; } from "../../../model/questionTypes/shared";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
type ButtonTypeQuestion = { type ButtonTypeQuestion = {
@ -83,7 +83,7 @@ export default function FormTypeQuestions({ question }: Props) {
<QuestionsMiniButton <QuestionsMiniButton
key={title} key={title}
onClick={() => { onClick={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.type = questionType; question.type = questionType;
}) })
}} }}

@ -1,5 +1,5 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -18,16 +18,16 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
const isMobile = useMediaQuery(theme.breakpoints.down(680)); const isMobile = useMediaQuery(theme.breakpoints.down(680));
const setReplText = useDebouncedCallback((replText) => { const setReplText = useDebouncedCallback((replText) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "varimg") return; if (question.type !== "varimg") return;
question.content.replText = replText; question.content.replText = replText;
}); });
}, 1000); }, 200);
const setDescription = useDebouncedCallback((value) => { const setDescription = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<> <>
@ -59,7 +59,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "varimg") return; if (question.type !== "varimg") return;
question.content.own = target.checked; question.content.own = target.checked;
@ -112,7 +112,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={question.content.required} checked={question.content.required}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "varimg") return; if (question.type !== "varimg") return;
question.content.required = target.checked; question.content.required = target.checked;
@ -126,7 +126,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = ""; question.content.innerName = "";
})} })}

@ -6,7 +6,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -44,10 +44,10 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
const updateProportions = (proportions: Proportion) => { const updateProportions = (proportions: Proportion) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.xy = proportions; question.content.xy = proportions;
@ -115,7 +115,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
label={"Можно несколько"} label={"Можно несколько"}
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.multi = target.checked; question.content.multi = target.checked;
@ -129,7 +129,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
label={"Большие картинки"} label={"Большие картинки"}
checked={question.content.largeCheck} checked={question.content.largeCheck}
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.largeCheck = target.checked; question.content.largeCheck = target.checked;
@ -141,7 +141,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.own = target.checked; question.content.own = target.checked;
@ -183,7 +183,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
Формат Формат
</Typography> </Typography>
<SelectIconButton <SelectIconButton
onClick={() => updateQuestionWithFnOptimistic(question.id, question => { onClick={() => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.format = "carousel"; question.content.format = "carousel";
@ -193,7 +193,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
Icon={FormatIcon2} Icon={FormatIcon2}
/> />
<SelectIconButton <SelectIconButton
onClick={() => updateQuestionWithFnOptimistic(question.id, question => { onClick={() => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.format = "masonry"; question.content.format = "masonry";
@ -212,7 +212,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
sx={{ alignItems: isMobile ? "flex-start" : "" }} sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={question.content.required} checked={question.content.required}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.required = target.checked; question.content.required = target.checked;
@ -233,7 +233,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { handleChange={({ target }) => updateQuestion(question.id, question => {
if (question.type !== "images") return; if (question.type !== "images") return;
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;

@ -1,5 +1,5 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useState } from "react"; import { useState } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -20,12 +20,12 @@ export default function OwnTextField({ question }: Props) {
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const setPlaceholder = useDebouncedCallback((value) => { const setPlaceholder = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "text") return; if (question.type !== "text") return;
question.content.placeholder = value; question.content.placeholder = value;
}); });
}, 1000); }, 200);
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);

@ -9,7 +9,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import CheckedIcon from "@ui_kit/RadioCheck"; import CheckedIcon from "@ui_kit/RadioCheck";
@ -43,7 +43,7 @@ export default function SettingTextField({
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -85,7 +85,7 @@ export default function SettingTextField({
({ value }) => question.content.answerType === value ({ value }) => question.content.answerType === value
)} )}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "text") return; if (question.type !== "text") return;
question.content.answerType = ANSWER_TYPES[Number(target.value)].value; question.content.answerType = ANSWER_TYPES[Number(target.value)].value;
@ -119,7 +119,7 @@ export default function SettingTextField({
label={"Только числа"} label={"Только числа"}
checked={question.content.onlyNumbers} checked={question.content.onlyNumbers}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "text") return; if (question.type !== "text") return;
question.content.onlyNumbers = target.checked; question.content.onlyNumbers = target.checked;
@ -157,7 +157,7 @@ export default function SettingTextField({
label={"Автозаполнение адреса"} label={"Автозаполнение адреса"}
checked={question.content.autofill} checked={question.content.autofill}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.autofill = target.checked; question.content.autofill = target.checked;
}); });
}} }}
@ -171,7 +171,7 @@ export default function SettingTextField({
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.required = !e.target.checked; question.required = !e.target.checked;
}); });
}} }}
@ -193,7 +193,7 @@ export default function SettingTextField({
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked question.content.innerName = target.checked
? question.content.innerName ? question.content.innerName

@ -2,7 +2,7 @@ import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { openCropModal } from "@root/cropModal"; import { openCropModal } from "@root/cropModal";
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
import { setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestion } from "@root/questions/actions";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal } from "@ui_kit/Modal/CropModal";
@ -29,12 +29,12 @@ export default function PageOptions({ disableInput, question }: Props) {
const isMobile = useMediaQuery(theme.breakpoints.down(780)); const isMobile = useMediaQuery(theme.breakpoints.down(780));
const setText = useDebouncedCallback((value) => { const setText = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "page") return; if (question.type !== "page") return;
question.content.text = value; question.content.text = value;
}); });
}, 1000); }, 200);
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
@ -225,7 +225,7 @@ export default function PageOptions({ disableInput, question }: Props) {
onClose={() => setOpenVideoModal(false)} onClose={() => setOpenVideoModal(false)}
video={question.content.video} video={question.content.video}
onUpload={(url) => { onUpload={(url) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "page") return; if (question.type !== "page") return;
question.content.video = url; question.content.video = url;

@ -10,7 +10,7 @@ import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionPage } from "../../../model/questionTypes/page"; import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
type SettingPageOptionsProps = { type SettingPageOptionsProps = {
@ -25,7 +25,7 @@ export default function SettingPageOptions({
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -60,7 +60,7 @@ export default function SettingPageOptions({
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = ""; question.content.innerName = "";
}) })

@ -7,7 +7,7 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { collapseAllQuestions, createQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createQuestion } from "@root/questions/actions";
import { incrementCurrentStep } from "@root/quizes/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";
@ -19,7 +19,7 @@ import { DraggableList } from "./DraggableList";
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; if (!quiz) return null;
@ -59,13 +59,14 @@ export default function QuestionsPage() {
> >
<IconButton <IconButton
onClick={() => { onClick={() => {
createQuestion(quiz.id); createQuestion(quiz.backendId);
}} }}
sx={{ sx={{
position: "fixed", position: "fixed",
left: isMobile ? "20px" : "250px", left: isMobile ? "20px" : "250px",
bottom: "20px", bottom: "20px",
}} }}
data-cy="create-question"
> >
<AddPlus /> <AddPlus />
</IconButton> </IconButton>
@ -73,6 +74,8 @@ export default function QuestionsPage() {
<Button <Button
variant="outlined" variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }} sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
data-cy="back-button"
onClick={decrementCurrentStep}
> >
<ArrowLeft /> <ArrowLeft />
</Button> </Button>

@ -18,7 +18,7 @@ import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon";
import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon"; import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon";
import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini"; import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
interface Props { interface Props {
@ -43,19 +43,19 @@ export default function RatingOptions({ question }: Props) {
const positiveRef = useRef<HTMLDivElement>(null); const positiveRef = useRef<HTMLDivElement>(null);
const debounceNegativeDescription = useDebouncedCallback((value) => { const debounceNegativeDescription = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.ratingNegativeDescription = value.substring(0, 15); question.content.ratingNegativeDescription = value.substring(0, 15);
}); });
}, 500); }, 200);
const debouncePositiveDescription = useDebouncedCallback((value) => { const debouncePositiveDescription = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.ratingPositiveDescription = value.substring(0, 15); question.content.ratingPositiveDescription = value.substring(0, 15);
}); });
}, 500); }, 200);
useEffect(() => { useEffect(() => {
setNegativeText(question.content.ratingNegativeDescription); setNegativeText(question.content.ratingNegativeDescription);
@ -120,7 +120,7 @@ export default function RatingOptions({ question }: Props) {
{...(itemNumber === 0 || itemNumber === question.content.steps - 1 {...(itemNumber === 0 || itemNumber === question.content.steps - 1
? { ? {
onClick: () => { onClick: () => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.ratingExpanded = true; question.content.ratingExpanded = true;

@ -1,6 +1,6 @@
import { QuizQuestionRating } from "@model/questionTypes/rating"; import { QuizQuestionRating } from "@model/questionTypes/rating";
import { Box, ButtonBase, Slider, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, ButtonBase, Slider, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -27,7 +27,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
const buttonRatingForm: ButtonRatingFrom[] = [ const buttonRatingForm: ButtonRatingFrom[] = [
{ name: "star", icon: <StarIconMini color={theme.palette.grey3.main} /> }, { name: "star", icon: <StarIconMini color={theme.palette.grey3.main} /> },
@ -79,7 +79,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<ButtonBase <ButtonBase
key={index} key={index}
onClick={() => { onClick={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.form = name; question.content.form = name;
@ -121,7 +121,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
valueLabelDisplay="auto" valueLabelDisplay="auto"
sx={{ color: theme.palette.brightPurple.main, padding: "0" }} sx={{ color: theme.palette.brightPurple.main, padding: "0" }}
onChange={(_, value) => { onChange={(_, value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.steps = Number(value) || 1; question.content.steps = Number(value) || 1;
@ -150,7 +150,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.required = !e.target.checked; question.required = !e.target.checked;
@ -169,7 +169,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "rating") return; if (question.type !== "rating") return;
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;

@ -4,7 +4,7 @@ import ButtonsOptions from "../ButtonsOptions";
import CustomNumberField from "@ui_kit/CustomNumberField"; import CustomNumberField from "@ui_kit/CustomNumberField";
import SwitchSlider from "./switchSlider"; import SwitchSlider from "./switchSlider";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
interface Props { interface Props {
@ -56,7 +56,7 @@ export default function SliderOptions({ question }: Props) {
max={99} max={99}
value={question.content.range.split("—")[0]} value={question.content.range.split("—")[0]}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.range = `${target.value}${question.content.range.split("—")[1]}`; question.content.range = `${target.value}${question.content.range.split("—")[1]}`;
@ -68,7 +68,7 @@ export default function SliderOptions({ question }: Props) {
const max = Number(question.content.range.split("—")[1]); const max = Number(question.content.range.split("—")[1]);
if (min >= max) { if (min >= max) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.range = `${max - 1 >= 0 ? max - 1 : 0}${question.content.range.split("—")[1]}`; question.content.range = `${max - 1 >= 0 ? max - 1 : 0}${question.content.range.split("—")[1]}`;
@ -76,7 +76,7 @@ export default function SliderOptions({ question }: Props) {
} }
if (start < min) { if (start < min) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.start = min; question.content.start = min;
@ -92,7 +92,7 @@ export default function SliderOptions({ question }: Props) {
max={100} max={100}
value={question.content.range.split("—")[1]} value={question.content.range.split("—")[1]}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.range = `${question.content.range.split("—")[0]}${target.value}`; question.content.range = `${question.content.range.split("—")[0]}${target.value}`;
@ -106,7 +106,7 @@ export default function SliderOptions({ question }: Props) {
const range = max - min; const range = max - min;
if (max <= min) { if (max <= min) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.range = `${min}${min + 1 >= 100 ? 100 : min + 1}`; question.content.range = `${min}${min + 1 >= 100 ? 100 : min + 1}`;
@ -114,7 +114,7 @@ export default function SliderOptions({ question }: Props) {
} }
if (start > max) { if (start > max) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.start = max; question.content.start = max;
@ -122,7 +122,7 @@ export default function SliderOptions({ question }: Props) {
} }
if (step > max) { if (step > max) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.step = min; question.content.step = min;
@ -158,7 +158,7 @@ export default function SliderOptions({ question }: Props) {
max={Number(question.content.range.split("—")[1])} max={Number(question.content.range.split("—")[1])}
value={String(question.content.start)} value={String(question.content.start)}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.start = Number(target.value); question.content.start = Number(target.value);
@ -185,7 +185,7 @@ export default function SliderOptions({ question }: Props) {
error={stepError} error={stepError}
value={String(question.content.step)} value={String(question.content.step)}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.step = Number(target.value); question.content.step = Number(target.value);
@ -198,7 +198,7 @@ export default function SliderOptions({ question }: Props) {
const step = Number(target.value); const step = Number(target.value);
if (step > max) { if (step > max) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.step = max; question.content.step = max;

@ -1,5 +1,5 @@
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -19,7 +19,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -52,7 +52,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
label={"Выбор диапозона (два ползунка)"} label={"Выбор диапозона (два ползунка)"}
checked={question.content.chooseRange} checked={question.content.chooseRange}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.chooseRange = target.checked; question.content.chooseRange = target.checked;
@ -80,7 +80,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.required = !e.target.checked; question.required = !e.target.checked;
@ -103,7 +103,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;

@ -11,7 +11,7 @@ import OptionsPict from "../../assets/icons/questionsPage/options_pict";
import Page from "../../assets/icons/questionsPage/page"; import Page from "../../assets/icons/questionsPage/page";
import RatingIcon from "../../assets/icons/questionsPage/rating"; import RatingIcon from "../../assets/icons/questionsPage/rating";
import Slider from "../../assets/icons/questionsPage/slider"; import Slider from "../../assets/icons/questionsPage/slider";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import type { import type {
AnyQuizQuestion, AnyQuizQuestion,
} from "../../model/questionTypes/shared"; } from "../../model/questionTypes/shared";
@ -42,7 +42,7 @@ export default function TypeQuestions({ question }: Props) {
<QuestionsMiniButton <QuestionsMiniButton
key={title} key={title}
dataCy={`select-questiontype-${value}`} dataCy={`select-questiontype-${value}`}
onClick={() => updateQuestionWithFnOptimistic(question.id, question => { onClick={() => updateQuestion(question.id, question => {
question.type = value; question.type = value;
})} })}
icon={icon} icon={icon}

@ -9,7 +9,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ArrowDown from "../../../assets/icons/ArrowDownIcon"; import ArrowDown from "../../../assets/icons/ArrowDownIcon";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
@ -50,7 +50,7 @@ export default function UploadFile({ question }: Props) {
}; };
const handleChange = ({ target }: SelectChangeEvent) => { const handleChange = ({ target }: SelectChangeEvent) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "file") return; if (question.type !== "file") return;
question.content.type = target.value as UploadFileType; question.content.type = target.value as UploadFileType;
@ -63,7 +63,7 @@ export default function UploadFile({ question }: Props) {
); );
if (!isTypeSetted) { if (!isTypeSetted) {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (question.type !== "file") return; if (question.type !== "file") return;
question.content.type = DESIGN_TYPES[0].value; question.content.type = DESIGN_TYPES[0].value;

@ -5,7 +5,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -23,7 +23,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
const setInnerName = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -48,7 +48,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
label={"Автозаполнение адреса"} label={"Автозаполнение адреса"}
checked={question.content.autofill} checked={question.content.autofill}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.autofill = target.checked; question.content.autofill = target.checked;
}); });
}} }}
@ -61,7 +61,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.required = !e.target.checked; question.required = !e.target.checked;
}); });
}} }}
@ -82,7 +82,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : ""; question.content.innerName = target.checked ? question.content.innerName : "";
}); });

@ -5,7 +5,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -26,7 +26,7 @@ export default function ResponseSettings({ question }: Props) {
const updateQuestionInnerName = useDebouncedCallback((value) => { const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 1000); }, 200);
return ( return (
<Box <Box
@ -63,7 +63,7 @@ export default function ResponseSettings({ question }: Props) {
label={"Длинный текстовый ответ"} label={"Длинный текстовый ответ"}
checked={question.content.largeCheck} checked={question.content.largeCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (!("largeCheck" in question.content)) return; if (!("largeCheck" in question.content)) return;
question.content.largeCheck = target.checked; question.content.largeCheck = target.checked;
@ -76,7 +76,7 @@ export default function ResponseSettings({ question }: Props) {
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (!("multi" in question.content)) return; if (!("multi" in question.content)) return;
question.content.multi = target.checked; question.content.multi = target.checked;
@ -88,7 +88,7 @@ export default function ResponseSettings({ question }: Props) {
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
if (!("own" in question.content)) return; if (!("own" in question.content)) return;
question.content.own = target.checked; question.content.own = target.checked;
@ -124,7 +124,7 @@ export default function ResponseSettings({ question }: Props) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.required = !target.checked; question.required = !target.checked;
}); });
}} }}
@ -145,7 +145,7 @@ export default function ResponseSettings({ question }: Props) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked; question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : ""; question.content.innerName = target.checked ? question.content.innerName : "";
}); });

@ -16,7 +16,7 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@ -48,7 +48,7 @@ export default function BranchingQuestions({
}, [title]); }, [title]);
const handleClose = () => { const handleClose = () => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.openedModalSettings = false; question.openedModalSettings = false;
}); });
}; };
@ -144,7 +144,7 @@ export default function BranchingQuestions({
activeItemIndex={question.content.rule.show ? 0 : 1} activeItemIndex={question.content.rule.show ? 0 : 1}
sx={{ maxWidth: "140px" }} sx={{ maxWidth: "140px" }}
onChange={(action) => { onChange={(action) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.rule.show = action === ACTIONS[0]; question.content.rule.show = action === ACTIONS[0];
}); });
}} }}
@ -177,7 +177,7 @@ export default function BranchingQuestions({
<IconButton <IconButton
sx={{ borderRadius: "6px", padding: "2px" }} sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { onClick={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.rule.reqs.splice(index, 1); question.content.rule.reqs.splice(index, 1);
}); });
}} }}
@ -190,7 +190,7 @@ export default function BranchingQuestions({
activeItemIndex={request.id ? Number(request.id) : -1} activeItemIndex={request.id ? Number(request.id) : -1}
items={STIPULATIONS} items={STIPULATIONS}
onChange={(stipulation) => { onChange={(stipulation) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.rule.reqs[index].id = String( question.content.rule.reqs[index].id = String(
STIPULATIONS.findIndex((item) => item.includes(stipulation)) STIPULATIONS.findIndex((item) => item.includes(stipulation))
); );
@ -222,7 +222,7 @@ export default function BranchingQuestions({
const answerItemIndex = ANSWERS.findIndex( const answerItemIndex = ANSWERS.findIndex(
(answerItem) => answerItem === answer (answerItem) => answerItem === answer
); );
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
const vars = question.content.rule.reqs[index].vars; const vars = question.content.rule.reqs[index].vars;
if (vars.includes(answerItemIndex)) { if (vars.includes(answerItemIndex)) {
vars.push(answerItemIndex); vars.push(answerItemIndex);
@ -249,7 +249,7 @@ export default function BranchingQuestions({
label={ANSWERS[item]} label={ANSWERS[item]}
variant="outlined" variant="outlined"
onDelete={() => { onDelete={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
const vars = question.content.rule.reqs[index].vars; const vars = question.content.rule.reqs[index].vars;
const removedItemIndex = vars.findIndex((varItem) => varItem === item); const removedItemIndex = vars.findIndex((varItem) => varItem === item);
@ -280,7 +280,7 @@ export default function BranchingQuestions({
marginBottom: "10px", marginBottom: "10px",
}} }}
onClick={() => { onClick={() => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.rule.reqs.push({ id: "", vars: [] }); question.content.rule.reqs.push({ id: "", vars: [] });
}); });
}} }}
@ -292,7 +292,7 @@ export default function BranchingQuestions({
aria-labelledby="demo-controlled-radio-buttons-group" aria-labelledby="demo-controlled-radio-buttons-group"
value={question.content.rule.or ? 1 : 0} value={question.content.rule.or ? 1 : 0}
onChange={(_, value) => { onChange={(_, value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.rule.or = Boolean(Number(value)); question.content.rule.or = Boolean(Number(value));
}); });
}} }}

@ -1,6 +1,6 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography } from "@mui/material"; import { Box, ButtonBase, Typography } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton"; import SelectableButton from "@ui_kit/SelectableButton";
import UploadBox from "@ui_kit/UploadBox"; import UploadBox from "@ui_kit/UploadBox";
@ -21,10 +21,10 @@ export default function HelpQuestions({ question }: HelpQuestionsProps) {
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text"); const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
const updateQuestionHint = useDebouncedCallback((value) => { const updateQuestionHint = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { updateQuestion(question.id, question => {
question.content.hint.text = value; question.content.hint.text = value;
}); });
}, 1000); }, 200);
return ( return (
<Box <Box
@ -92,7 +92,7 @@ export default function HelpQuestions({ question }: HelpQuestionsProps) {
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
video={question.content.hint.video} video={question.content.hint.video}
onUpload={url => updateQuestionWithFnOptimistic(question.id, question => { onUpload={url => updateQuestion(question.id, question => {
question.content.hint.video = url; question.content.hint.video = url;
})} })}
/> />

@ -1,34 +1,39 @@
import { Box, Button, Tooltip } from "@mui/material"; import { Box, Button, Tooltip } from "@mui/material";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import image from "../../assets/Rectangle 110.png";
import Info from "../../assets/icons/Info";
import CreationFullCard from "./CreationFullCard"; import CreationFullCard from "./CreationFullCard";
import Info from "../../assets/icons/Info";
import image from "../../assets/Rectangle 110.png";
import { incrementCurrentStep } from "@root/quizes/actions";
export const Result = () => { export const Result = () => {
const quiz = useCurrentQuiz();
return ( if (!quiz) return null;
<Box component="section">
<CreationFullCard return (
text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке." <Box component="section">
text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов." <CreationFullCard
image={image} text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке."
/> text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов."
<Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}> image={image}
<Button />
variant="contained" <Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}>
sx={{ mr: "15px", minWidth: "258px" }} <Button
onClick={incrementCurrentStep} variant="contained"
> sx={{ mr: "15px", minWidth: "258px" }}
Создать результаты onClick={() => updateQuiz(quiz.id, quiz => {
</Button> quiz.config.results = true;
<Tooltip title="Посмотреть справку" placement="top"> })}
<Box> >
<Info /> Создать результаты
</Box> </Button>
</Tooltip> <Tooltip title="Посмотреть справку" placement="top">
</Box> <Box>
</Box> <Info />
); </Box>
</Tooltip>
</Box>
</Box>
);
}; };

@ -146,13 +146,14 @@ export default function SigninDialog() {
onBlur: formik.handleBlur, onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email), error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email, helperText: formik.touched.email && formik.errors.email,
"data-cy": "username",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
id="email" id="email"
label="Email" label="Email"
gap={upMd ? "10px" : "10px"} gap={upMd ? "10px" : "10px"}
/> />
<PasswordInput <PasswordInput
TextfieldProps={{ TextfieldProps={{
value: formik.values.password, value: formik.values.password,
@ -161,6 +162,7 @@ export default function SigninDialog() {
error: formik.touched.password && Boolean(formik.errors.password), error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password, helperText: formik.touched.password && formik.errors.password,
type: "password", type: "password",
"data-cy": "password",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
@ -183,6 +185,7 @@ export default function SigninDialog() {
backgroundColor: "black", backgroundColor: "black",
}, },
}} }}
data-cy="signin"
> >
Войти Войти
</Button> </Button>

@ -155,6 +155,7 @@ export default function SignupDialog() {
onBlur: formik.handleBlur, onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email), error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email, helperText: formik.touched.email && formik.errors.email,
"data-cy": "username",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
@ -170,6 +171,7 @@ export default function SignupDialog() {
error: formik.touched.password && Boolean(formik.errors.password), error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password, helperText: formik.touched.password && formik.errors.password,
autoComplete: "new-password", autoComplete: "new-password",
"data-cy": "password",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
@ -188,6 +190,7 @@ export default function SignupDialog() {
helperText: helperText:
formik.touched.repeatPassword && formik.errors.repeatPassword, formik.touched.repeatPassword && formik.errors.repeatPassword,
autoComplete: "new-password", autoComplete: "new-password",
"data-cy": "repeat-password",
}} }}
onChange={formik.handleChange} onChange={formik.handleChange}
color="#F2F3F7" color="#F2F3F7"
@ -210,6 +213,7 @@ export default function SignupDialog() {
backgroundColor: "black", backgroundColor: "black",
}, },
}} }}
data-cy="signup"
> >
Зарегистрироваться Зарегистрироваться
</Button> </Button>

@ -19,7 +19,7 @@ import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz"; import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard"; import QuizCard from "./QuizCard";
import { setQuizes, createQuiz } from "@root/quizes/actions"; import { setQuizes, createQuiz } from "@root/quizes/actions";
import { useQuizArray } from "@root/quizes/hooks"; import { useQuizStore } from "@root/quizes/store";
interface Props { interface Props {
@ -40,7 +40,7 @@ export default function MyQuizzesFull({
enqueueSnackbar(`Не удалось получить квизы. ${message}`); enqueueSnackbar(`Не удалось получить квизы. ${message}`);
}, },
}); });
const quizArray = useQuizArray(); const quizArray = useQuizStore(state => state.quizes);
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
@ -69,6 +69,7 @@ export default function MyQuizzesFull({
minWidth: "44px", minWidth: "44px",
}} }}
onClick={() => createQuiz(navigate)} onClick={() => createQuiz(navigate)}
data-cy="create-quiz"
> >
{isMobile ? "+" : "Создать +"} {isMobile ? "+" : "Создать +"}
</Button> </Button>

@ -33,7 +33,7 @@ export default function QuizCard({
const navigate = useNavigate(); const navigate = useNavigate();
function handleEditClick() { function handleEditClick() {
setEditQuizId(quiz.id); setEditQuizId(quiz.backendId);
navigate("/edit"); navigate("/edit");
} }
@ -139,6 +139,7 @@ export default function QuizCard({
ml: "auto", ml: "auto",
}} }}
onClick={() => deleteQuiz(quiz.id)} onClick={() => deleteQuiz(quiz.id)}
data-cy="delete-quiz"
> >
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} /> <MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
</IconButton> </IconButton>

@ -43,8 +43,8 @@ export default function StartPage() {
}); });
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const quizId = useQuizStore(state => state.editQuizId); const editQuizId = useQuizStore(state => state.editQuizId);
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const currentStep = useQuizStore(state => state.currentStep); const currentStep = useQuizStore(state => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
@ -53,8 +53,8 @@ export default function StartPage() {
const quizConfig = quiz?.config; const quizConfig = quiz?.config;
useEffect(() => { useEffect(() => {
if (quizId === null) navigate("/list"); if (editQuizId === null) navigate("/list");
}, [navigate, quizId]); }, [navigate, editQuizId]);
useEffect(() => () => resetEditConfig(), []); useEffect(() => () => resetEditConfig(), []);
@ -227,6 +227,8 @@ export default function StartPage() {
<SwitchStepPages <SwitchStepPages
activeStep={currentStep} activeStep={currentStep}
quizType={quizConfig.type} quizType={quizConfig.type}
quizResults={quizConfig.results}
quizStartPageType={quizConfig.startpageType}
/> />
</> </>
} }

@ -22,7 +22,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions"; import { incrementCurrentStep, updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -63,7 +63,7 @@ export default function StartPageSettings() {
const theme = useTheme(); const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1500)); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1500));
const isTablet = useMediaQuery(theme.breakpoints.down(950)); const isTablet = useMediaQuery(theme.breakpoints.down(950));
const { quiz, updateQuiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [formState, setFormState] = useState<"design" | "content">("design"); const [formState, setFormState] = useState<"design" | "content">("design");
const designType = quiz?.config?.startpageType; const designType = quiz?.config?.startpageType;
@ -179,7 +179,7 @@ export default function StartPageSettings() {
variant="outlined" variant="outlined"
value={designType} value={designType}
displayEmpty displayEmpty
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpageType = e.target.value as QuizStartpageType; quiz.config.startpageType = e.target.value as QuizStartpageType;
})} })}
sx={{ sx={{
@ -264,7 +264,7 @@ export default function StartPageSettings() {
> >
<SelectableButton <SelectableButton
isSelected={quiz.config.startpage.background.type === "image"} isSelected={quiz.config.startpage.background.type === "image"}
onClick={() => updateQuiz(quiz => { onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.type = "image"; quiz.config.startpage.background.type = "image";
})} })}
> >
@ -272,7 +272,7 @@ export default function StartPageSettings() {
</SelectableButton> </SelectableButton>
<SelectableButton <SelectableButton
isSelected={quiz.config.startpage.background.type === "video"} isSelected={quiz.config.startpage.background.type === "video"}
onClick={() => updateQuiz(quiz => { onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.type = "video"; quiz.config.startpage.background.type = "video";
})} })}
> >
@ -418,7 +418,7 @@ export default function StartPageSettings() {
<CustomCheckbox <CustomCheckbox
label="Зацикливать видео" label="Зацикливать видео"
checked={quiz.config.startpage.background.cycle} checked={quiz.config.startpage.background.cycle}
handleChange={e => updateQuiz(quiz => { handleChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.cycle = e.target.checked; quiz.config.startpage.background.cycle = e.target.checked;
})} })}
/> />
@ -452,14 +452,14 @@ export default function StartPageSettings() {
}} }}
> >
<SelectableIconButton <SelectableIconButton
onClick={() => updateQuiz(quiz => { onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "left"; quiz.config.startpage.position = "left";
})} })}
isActive={quiz.config.startpage.position === "left"} isActive={quiz.config.startpage.position === "left"}
Icon={AlignLeftIcon} Icon={AlignLeftIcon}
/> />
<SelectableIconButton <SelectableIconButton
onClick={() => updateQuiz(quiz => { onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "center"; quiz.config.startpage.position = "center";
})} })}
isActive={quiz.config.startpage.position === "center"} isActive={quiz.config.startpage.position === "center"}
@ -467,7 +467,7 @@ export default function StartPageSettings() {
sx={{ display: designType === "centered" ? "flex" : "none" }} sx={{ display: designType === "centered" ? "flex" : "none" }}
/> />
<SelectableIconButton <SelectableIconButton
onClick={() => updateQuiz(quiz => { onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right"; quiz.config.startpage.position = "right";
})} })}
isActive={quiz.config.startpage.position === "right"} isActive={quiz.config.startpage.position === "right"}
@ -619,7 +619,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Текст-заполнитель — это текст, который" placeholder="Текст-заполнитель — это текст, который"
text={quiz.name} text={quiz.name}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.name = e.target.value; quiz.name = e.target.value;
})} })}
/> />
@ -636,7 +636,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Текст-заполнитель — это текст, который " placeholder="Текст-заполнитель — это текст, который "
text={quiz.config.startpage.description} text={quiz.config.startpage.description}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.description = e.target.value; quiz.config.startpage.description = e.target.value;
})} })}
/> />
@ -653,7 +653,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Начать" placeholder="Начать"
text={quiz.config.startpage.button} text={quiz.config.startpage.button}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.button = e.target.value; quiz.config.startpage.button = e.target.value;
})} })}
/> />
@ -670,14 +670,14 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="+7 900 000 00 00" placeholder="+7 900 000 00 00"
text={quiz.config.info.phonenumber} text={quiz.config.info.phonenumber}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.info.phonenumber = e.target.value; quiz.config.info.phonenumber = e.target.value;
})} })}
/> />
<CustomCheckbox <CustomCheckbox
label="Кликабельный" label="Кликабельный"
checked={quiz.config.info.clickable} checked={quiz.config.info.clickable}
handleChange={e => updateQuiz(quiz => { handleChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.info.clickable = e.target.checked; quiz.config.info.clickable = e.target.checked;
})} })}
/> />
@ -694,7 +694,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Текст-заполнитель — это текст, который " placeholder="Текст-заполнитель — это текст, который "
text={quiz.config.info.orgname} text={quiz.config.info.orgname}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.info.orgname = e.target.value; quiz.config.info.orgname = e.target.value;
})} })}
/> />
@ -711,7 +711,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Текст-заполнитель — это текст, который " placeholder="Текст-заполнитель — это текст, который "
text={quiz.config.info.site} text={quiz.config.info.site}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.info.site = e.target.value; quiz.config.info.site = e.target.value;
})} })}
/> />
@ -728,7 +728,7 @@ export default function StartPageSettings() {
<CustomTextField <CustomTextField
placeholder="Текст-заполнитель — это текст, который " placeholder="Текст-заполнитель — это текст, который "
text={quiz.config.info.law} text={quiz.config.info.law}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.info.law = e.target.value; quiz.config.info.law = e.target.value;
})} })}
/> />
@ -815,7 +815,7 @@ export default function StartPageSettings() {
borderRadius: 0, borderRadius: 0,
padding: 0, padding: 0,
}} }}
onChange={e => updateQuiz(quiz => { onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.noStartPage = e.target.checked; quiz.config.noStartPage = e.target.checked;
})} })}
checked={quiz.config.noStartPage} checked={quiz.config.noStartPage}
@ -832,14 +832,7 @@ export default function StartPageSettings() {
<Button <Button
variant="contained" variant="contained"
data-cy="setup-questions" data-cy="setup-questions"
onClick={() => { onClick={incrementCurrentStep}
updateQuiz(quiz => {
quiz.config.startpage.background.desktop = "https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg";
quiz.config.startpage.background.mobile = "https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png";
quiz.config.startpage.background.video = "https://youtu.be/dbaPkCiLPKQ";
});
incrementCurrentStep();
}}
> >
Настроить вопросы Настроить вопросы
</Button> </Button>

@ -7,6 +7,7 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useState } from "react"; import { useState } from "react";
@ -22,7 +23,7 @@ interface Props {
//Научи функцию принимать данные для валидации //Научи функцию принимать данные для валидации
export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => { export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { quiz, updateQuiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
if (!quiz) return null; // TODO throw and catch with error boundary if (!quiz) return null; // TODO throw and catch with error boundary
@ -31,7 +32,7 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
const file = imgInp.files?.[0]; const file = imgInp.files?.[0];
if (file) { if (file) {
if (file.size < 5242880) { if (file.size < 5242880) {
updateQuiz(quiz => { updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.desktop = URL.createObjectURL(file); quiz.config.startpage.background.desktop = URL.createObjectURL(file);
}); });
} else { } else {
@ -54,7 +55,7 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
if (file.size < 5242880) { if (file.size < 5242880) {
updateQuiz(quiz => { updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.desktop = URL.createObjectURL(file); quiz.config.startpage.background.desktop = URL.createObjectURL(file);
}); });
} else { } else {

@ -1,4 +1,5 @@
import { Box, Link, Typography, useTheme } from "@mui/material"; import { Box, Link, Typography, useTheme } from "@mui/material";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
@ -6,14 +7,14 @@ import { ChangeEvent, useState } from "react";
export default function Extra() { export default function Extra() {
const theme = useTheme(); const theme = useTheme();
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const { quiz, updateQuiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const expandedHC = (bool: boolean) => { const expandedHC = (bool: boolean) => {
setIsExpanded(bool); setIsExpanded(bool);
}; };
const mutationOrgMetaHC = (event: ChangeEvent<HTMLInputElement>) => { const mutationOrgMetaHC = (event: ChangeEvent<HTMLInputElement>) => {
updateQuiz(quiz => { updateQuiz(quiz?.id, quiz => {
quiz.config.meta = event.target.value; quiz.config.meta = event.target.value;
}); });
}; };

@ -7,7 +7,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
export default function StepOne() { export default function StepOne() {
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const config = quiz?.config; const config = quiz?.config;
if (!config) return null; // TODO throw and catch with error boundary if (!config) return null; // TODO throw and catch with error boundary

@ -16,7 +16,7 @@ import CardWithImage from "./CardWithImage";
export default function Steptwo() { export default function Steptwo() {
const theme = useTheme(); const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300)); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300));
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const config = quiz?.config; const config = quiz?.config;

@ -49,10 +49,10 @@ export const questionStore = create<QuestionStore>()(
isFirstPartialize = false; isFirstPartialize = false;
Object.keys(state.listQuestions).forEach((quizId) => { Object.keys(state.listQuestions).forEach((quizId) => {
[...state.listQuestions[quizId]].forEach(({ id, deleted }) => { [...state.listQuestions[quizId]].forEach(({ backendId: id, deleted }) => {
if (deleted) { if (deleted) {
const removedItemIndex = state.listQuestions[quizId].findIndex( const removedItemIndex = state.listQuestions[quizId].findIndex(
(item) => item.id === id (item) => item.backendId === id
); );
state.listQuestions[quizId].splice(removedItemIndex, 1); state.listQuestions[quizId].splice(removedItemIndex, 1);
@ -319,7 +319,7 @@ export const createQuestion = (
newData[quizId].splice( newData[quizId].splice(
placeIndex < 0 ? newData[quizId].length : placeIndex, placeIndex < 0 ? newData[quizId].length : placeIndex,
0, 0,
{ ...JSON.parse(JSON.stringify(defaultObject)), id } { ...JSON.parse(JSON.stringify(defaultObject)), backendId: id }
); );
questionStore.setState({ listQuestions: newData }); questionStore.setState({ listQuestions: newData });
@ -333,7 +333,7 @@ export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
const copiedQuiz = { ...listQuestions[quizId][copiedQuestionIndex] }; const copiedQuiz = { ...listQuestions[quizId][copiedQuestionIndex] };
listQuestions[quizId].splice(copiedQuestionIndex, 0, { listQuestions[quizId].splice(copiedQuestionIndex, 0, {
...copiedQuiz, ...copiedQuiz,
id: getRandom(), backendId: getRandom(),
}); });
questionStore.setState({ listQuestions }); questionStore.setState({ listQuestions });
@ -343,7 +343,7 @@ export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
export const removeQuestionForce = (quizId: number, removedId: number) => { export const removeQuestionForce = (quizId: number, removedId: number) => {
const questionListClone = { ...questionStore.getState()["listQuestions"] }; const questionListClone = { ...questionStore.getState()["listQuestions"] };
const removedItemIndex = questionListClone[quizId].findIndex( const removedItemIndex = questionListClone[quizId].findIndex(
({ id }) => id === removedId ({ backendId: id }) => id === removedId
); );
questionListClone[quizId].splice(removedItemIndex, 1); questionListClone[quizId].splice(removedItemIndex, 1);
questionStore.setState({ listQuestions: questionListClone }); questionStore.setState({ listQuestions: questionListClone });
@ -362,7 +362,7 @@ export const findQuestionById = (quizId: number) => {
questionStore questionStore
.getState() .getState()
["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => { ["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => {
if (quiz.id === quizId) { if (quiz.backendId === quizId) {
found = { quiz, index }; found = { quiz, index };
return true; return true;
} }

@ -1,14 +1,15 @@
import { questionApi } from "@api/question"; import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui"; import { devlog } from "@frontend/kitui";
import { EditQuestionResponse, questionToEditQuestionRequest } from "@model/question/edit"; import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared"; import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared";
import { produce } from "immer"; import { produce } from "immer";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { notReachable } from "../../utils/notReachable";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { QuestionsStore, useQuestionsStore } from "./store"; import { notReachable } from "../../utils/notReachable";
import { RequestQueue } from "../../utils/requestQueue"; import { RequestQueue } from "../../utils/requestQueue";
import { QuestionsStore, useQuestionsStore } from "./store";
import { nanoid } from "nanoid";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
@ -18,14 +19,6 @@ export const setQuestions = (questions: RawQuestion[] | null) => setProducedStat
questions, questions,
}); });
const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === question.id);
state.questions.splice(index, 1, question);
}, {
type: "setQuestion",
question,
});
const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => { const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
state.questions.push(question); state.questions.push(question);
}, { }, {
@ -33,28 +26,25 @@ const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
question, question,
}); });
const removeQuestion = (questionId: number) => setProducedState(state => { const removeQuestion = (questionId: string) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === questionId); 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", type: "removeQuestion",
questionId, questionId,
}); });
const setQuestionField = <T extends keyof AnyQuizQuestion>( const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => {
questionId: number,
field: T,
value: AnyQuizQuestion[T],
) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId); const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
question[field] = value; question.backendId = backendId;
}, { }, {
type: "setQuestionField", type: "setQuestionBackendId",
questionId, questionId: questionId,
field, backendId,
value,
}); });
export const reorderQuestions = ( export const reorderQuestions = (
@ -69,7 +59,7 @@ export const reorderQuestions = (
}); });
}; };
export const toggleExpandQuestion = (questionId: number) => setProducedState(state => { export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId); const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
@ -80,15 +70,8 @@ export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false); state.questions.forEach(question => question.expanded = false);
}); });
export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => { export const addQuestionVariant = (questionId: string) => {
const question = state.questions.find(q => q.id === questionId); updateQuestion(questionId, question => {
if (!question) return;
question.openedModalSettings = !question.openedModalSettings;
});
export const addQuestionVariant = (questionId: number) => {
updateQuestionWithFnOptimistic(questionId, question => {
switch (question.type) { switch (question.type) {
case "variant": case "variant":
case "emoji": case "emoji":
@ -111,8 +94,8 @@ export const addQuestionVariant = (questionId: number) => {
}); });
}; };
export const deleteQuestionVariant = (questionId: number, variantId: string) => { export const deleteQuestionVariant = (questionId: string, variantId: string) => {
updateQuestionWithFnOptimistic(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(variant => variant.id === variantId);
@ -123,12 +106,12 @@ export const deleteQuestionVariant = (questionId: number, variantId: string) =>
}; };
export const setQuestionVariantField = ( export const setQuestionVariantField = (
questionId: number, questionId: string,
variantId: string, variantId: string,
field: keyof QuestionVariant, field: keyof QuestionVariant,
value: QuestionVariant[keyof QuestionVariant], value: QuestionVariant[keyof QuestionVariant],
) => { ) => {
updateQuestionWithFnOptimistic(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(variant => variant.id === variantId);
@ -140,12 +123,12 @@ export const setQuestionVariantField = (
}; };
export const setQuestionImageVariantField = ( export const setQuestionImageVariantField = (
questionId: number, questionId: string,
variantId: string, variantId: string,
field: keyof ImageQuestionVariant, field: keyof ImageQuestionVariant,
value: ImageQuestionVariant[keyof ImageQuestionVariant], value: ImageQuestionVariant[keyof ImageQuestionVariant],
) => { ) => {
updateQuestionWithFnOptimistic(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(variant => variant.id === variantId);
@ -159,13 +142,13 @@ export const setQuestionImageVariantField = (
}; };
export const reorderQuestionVariants = ( export const reorderQuestionVariants = (
questionId: number, questionId: string,
sourceIndex: number, sourceIndex: number,
destinationIndex: number, destinationIndex: number,
) => { ) => {
if (sourceIndex === destinationIndex) return; if (sourceIndex === destinationIndex) return;
updateQuestionWithFnOptimistic(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);
@ -175,10 +158,10 @@ export const reorderQuestionVariants = (
}; };
export const setQuestionBackgroundImage = ( export const setQuestionBackgroundImage = (
questionId: number, questionId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(questionId, question => { updateQuestion(questionId, question => {
if (question.content.back === url) return; if (question.content.back === url) return;
if ( if (
@ -189,10 +172,10 @@ export const setQuestionBackgroundImage = (
}; };
export const setQuestionOriginalBackgroundImage = ( export const setQuestionOriginalBackgroundImage = (
questionId: number, questionId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(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);
@ -201,11 +184,11 @@ export const setQuestionOriginalBackgroundImage = (
}; };
export const setVariantImageUrl = ( export const setVariantImageUrl = (
questionId: number, questionId: string,
variantId: string, variantId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(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(variant => variant.id === variantId);
@ -219,11 +202,11 @@ export const setVariantImageUrl = (
}; };
export const setVariantOriginalImageUrl = ( export const setVariantOriginalImageUrl = (
questionId: number, questionId: string,
variantId: string, variantId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(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(
@ -239,10 +222,10 @@ export const setVariantOriginalImageUrl = (
}; };
export const setPageQuestionPicture = ( export const setPageQuestionPicture = (
questionId: number, questionId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(questionId, question => { updateQuestion(questionId, question => {
if (question.type !== "page") return; if (question.type !== "page") return;
if (question.content.picture === url) return; if (question.content.picture === url) return;
@ -255,10 +238,10 @@ export const setPageQuestionPicture = (
}; };
export const setPageQuestionOriginalPicture = ( export const setPageQuestionOriginalPicture = (
questionId: number, questionId: string,
url: string, url: string,
) => { ) => {
updateQuestionWithFnOptimistic(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;
@ -269,47 +252,52 @@ export const setPageQuestionOriginalPicture = (
}; };
export const setQuestionInnerName = ( export const setQuestionInnerName = (
questionId: number, questionId: string,
name: string, name: string,
) => { ) => {
updateQuestionWithFnOptimistic(questionId, question => { updateQuestion(questionId, question => {
question.content.innerName = name; question.content.innerName = name;
}); });
}; };
const REQUEST_DEBOUNCE = 1000; const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue<EditQuestionResponse>(); const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>; let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestionWithFnOptimistic = async ( export const updateQuestion = (
questionId: number, questionId: string,
updateFn: (question: AnyQuizQuestion) => void, updateFn: (question: AnyQuizQuestion) => void,
) => { ) => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); setProducedState(state => {
if (!question) return; const question = state.questions.find(q => q.id === questionId);
if (!question) return;
const updatedQuestion = produce(question, updateFn); updateFn(question);
setQuestion(updatedQuestion); }, {
type: "updateQuestion",
questionId,
updateFn: updateFn.toString(),
});
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(async () => { requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async (prevResponse) => { requestQueue.enqueue(async () => {
const questionId = prevResponse?.updated ?? updatedQuestion.id; const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
const response = await questionApi.edit(questionToEditQuestionRequest(updatedQuestion, questionId)); if (!question) return;
setQuestionField(questionId, "id", response.updated); const response = await questionApi.edit(questionToEditQuestionRequest(question));
return response; setQuestionBackendId(questionId, response.updated);
}).catch(error => { }).catch(error => {
if (isAxiosCanceledError(error)) return; if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, question, updatedQuestion }); devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос"); enqueueSnackbar("Не удалось сохранить вопрос");
}); });
}, REQUEST_DEBOUNCE); }, REQUEST_DEBOUNCE);
}; };
export const createQuestion = async (quizId: number, type: QuestionType = "variant") => { export const createQuestion = async (quizId: number, type: QuestionType = "variant") => requestQueue.enqueue(async () => {
try { try {
const question = await questionApi.create({ const question = await questionApi.create({
quiz_id: quizId, quiz_id: quizId,
@ -321,41 +309,45 @@ export const createQuestion = async (quizId: number, type: QuestionType = "varia
devlog("Error creating question", error); devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос"); enqueueSnackbar("Не удалось создать вопрос");
} }
}; });
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
export const deleteQuestion = async (questionId: number) => {
try { try {
await questionApi.delete(questionId); 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 () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
export const copyQuestion = async (questionId: number, quizId: number) => {
try { try {
const { updated: newQuestionId } = await questionApi.copy(questionId, quizId); const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId);
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = nanoid();
setProducedState(state => { setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
console.log(question);
const copiedQuestion = structuredClone(question);
copiedQuestion.id = newQuestionId;
state.questions.push(copiedQuestion); state.questions.push(copiedQuestion);
}, { }, {
type: "copyQuestion", type: "copyQuestion",
questionId, questionId: questionId,
quizId, 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,

@ -1,8 +1,8 @@
import { quizApi } from "@api/quiz"; import { quizApi } from "@api/quiz";
import { devlog, getMessageFromFetchError } from "@frontend/kitui"; import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { EditQuizResponse, quizToEditQuizRequest } from "@model/quiz/edit"; import { quizToEditQuizRequest } from "@model/quiz/edit";
import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz"; import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz";
import { QuizConfig, QuizSetupStep, maxQuizSetupSteps } from "@model/quizSettings"; import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings";
import { createQuestion } from "@root/questions/actions"; import { createQuestion } from "@root/questions/actions";
import { produce } from "immer"; import { produce } from "immer";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -21,156 +21,148 @@ export const setEditQuizId = (quizId: number | null) => setProducedState(state =
export const resetEditConfig = () => setProducedState(state => { export const resetEditConfig = () => setProducedState(state => {
state.editQuizId = null; state.editQuizId = null;
state.currentStep = 1; state.currentStep = 0;
}); });
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => { export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
state.quizById = {}; state.quizes = quizes?.map(rawQuizToQuiz) ?? [];
if (quizes === null) return;
quizes.forEach(quiz => state.quizById[quiz.id] = rawQuizToQuiz(quiz));
}, { }, {
type: "setQuizes", type: "setQuizes",
quizes, quizes,
}); });
export const setQuiz = (quiz: Quiz) => setProducedState(state => { const addQuiz = (quiz: Quiz) => setProducedState(state => {
state.quizById[quiz.id] = quiz; state.quizes.push(quiz);
}, { }, {
type: "setQuiz", type: "addQuiz",
quiz, quiz,
}); });
export const removeQuiz = (quizId: number) => setProducedState(state => { const removeQuiz = (quizId: string) => setProducedState(state => {
delete state.quizById[quizId]; const index = state.quizes.findIndex(q => q.id === quizId);
if (index === -1) return;
state.quizes.splice(index, 1);
}, { }, {
type: "removeQuiz", type: "removeQuiz",
quizId, quizId,
}); });
export const setQuizField = <T extends keyof Quiz>( const setQuizBackendId = (quizId: string, backendId: number) => setProducedState(state => {
quizId: number, const quiz = state.quizes.find(q => q.id === quizId);
field: T,
value: Quiz[T],
) => setProducedState(state => {
const quiz = state.quizById[quizId];
if (!quiz) return; if (!quiz) return;
const oldId = quiz.id; quiz.backendId = backendId;
quiz[field] = value;
if (field === "id") {
delete state.quizById[oldId];
state.quizById[value as number] = quiz;
}
}, { }, {
type: "setQuizField", type: "setQuizBackendId",
quizId, quizId,
field, backendId,
value,
}); });
export const incrementCurrentStep = () => setProducedState(state => { export const incrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.min( state.currentStep = Math.min(maxQuizSetupSteps - 1, state.currentStep + 1);
maxQuizSetupSteps, state.currentStep + 1
) as QuizSetupStep;
}, { }, {
type: "incrementCurrentStep", type: "incrementCurrentStep",
}); });
export const decrementCurrentStep = () => setProducedState(state => { export const decrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.max( state.currentStep = Math.max(0, state.currentStep - 1);
1, state.currentStep - 1
) as QuizSetupStep;
}, { }, {
type: "decrementCurrentStep", type: "decrementCurrentStep",
}); });
export const setCurrentStep = (step: number) => setProducedState(state => { export const setCurrentStep = (step: number) => setProducedState(state => {
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps, step)) as QuizSetupStep; state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step));
}); });
export const setQuizType = ( export const setQuizType = (
quizId: number, quizId: string,
quizType: QuizConfig["type"], quizType: QuizConfig["type"],
) => { ) => {
updateQuizWithFnOptimistic( updateQuiz(
quizId, quizId,
quiz => { quiz => {
quiz.config.type = quizType; quiz.config.type = quizType;
}, },
); );
incrementCurrentStep();
}; };
export const setQuizStartpageType = ( export const setQuizStartpageType = (
quizId: number | null, quizId: string,
startpageType: QuizConfig["startpageType"], startpageType: QuizConfig["startpageType"],
) => { ) => {
updateQuizWithFnOptimistic( updateQuiz(
quizId, quizId,
quiz => { quiz => {
quiz.config.startpageType = startpageType; quiz.config.startpageType = startpageType;
}, },
); );
incrementCurrentStep();
}; };
const REQUEST_DEBOUNCE = 1000; const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue<EditQuizResponse>(); const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>; let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuizWithFnOptimistic = async ( export const updateQuiz = async (
quizId: number | null, quizId: string | null | undefined,
updateFn: (quiz: Quiz) => void, updateFn: (quiz: Quiz) => void,
) => { ) => {
if (!quizId) return; if (!quizId) return;
const quiz = useQuizStore.getState().quizById[quizId]; setProducedState(state => {
if (!quiz) return; const quiz = state.quizes.find(q => q.id === quizId);
if (!quiz) return;
const updatedQuiz = produce(quiz, updateFn); updateFn(quiz);
setQuiz(updatedQuiz); }, {
type: "updateQuiz",
quizId,
updateFn: updateFn.toString(),
});
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(async () => { requestTimeoutId = setTimeout(async () => {
requestQueue.enqueue(async (prevResponse) => { requestQueue.enqueue(async () => {
const quizId = prevResponse?.updated ?? updatedQuiz.id; const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
const response = await quizApi.edit(quizToEditQuizRequest(updatedQuiz, quizId)); if (!quiz) return;
setQuizField(quizId, "id", response.updated); const response = await quizApi.edit(quizToEditQuizRequest(quiz));
setQuizBackendId(quizId, response.updated);
setEditQuizId(response.updated); setEditQuizId(response.updated);
return response;
}).catch(error => { }).catch(error => {
if (isAxiosCanceledError(error)) return; if (isAxiosCanceledError(error)) return;
devlog("Error editing quiz", { error, quiz, updatedQuiz }); devlog("Error editing quiz", error, quizId);
enqueueSnackbar("Не удалось сохранить настройки квиза"); enqueueSnackbar("Не удалось сохранить настройки квиза");
}); });
}, REQUEST_DEBOUNCE); }, REQUEST_DEBOUNCE);
}; };
export const createQuiz = async (navigate: NavigateFunction) => { export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enqueue(async () => {
try { try {
const quiz = await quizApi.create(); const rawQuiz = await quizApi.create();
const quiz = rawQuizToQuiz(rawQuiz);
setQuiz(rawQuizToQuiz(quiz)); addQuiz(quiz);
setEditQuizId(quiz.id); setEditQuizId(quiz.backendId);
navigate("/edit"); navigate("/edit");
await createQuestion(quiz.id); await createQuestion(rawQuiz.id);
} catch (error) { } catch (error) {
devlog("Error creating quiz", error); devlog("Error creating quiz", error);
const message = getMessageFromFetchError(error) ?? ""; const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось создать квиз. ${message}`); enqueueSnackbar(`Не удалось создать квиз. ${message}`);
} }
}; });
export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async () => {
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
if (!quiz) return;
export const deleteQuiz = async (quizId: number) => {
try { try {
await quizApi.delete(quizId); await quizApi.delete(quiz.backendId);
removeQuiz(quizId); removeQuiz(quizId);
} catch (error) { } catch (error) {
@ -179,7 +171,9 @@ export const deleteQuiz = async (quizId: number) => {
const message = getMessageFromFetchError(error) ?? ""; const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось удалить квиз. ${message}`); enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
} }
}; });
// TODO copy quiz
function setProducedState<A extends string | { type: unknown; }>( function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuizStore) => void, recipe: (state: QuizStore) => void,

@ -1,22 +1,11 @@
import { Quiz } from "@model/quiz/quiz";
import { useCallback } from "react";
import { updateQuizWithFnOptimistic } from "./actions";
import { useQuizStore } from "./store"; import { useQuizStore } from "./store";
export function useQuizArray(): Quiz[] {
const quizes = useQuizStore(state => state.quizById);
return Object.values(quizes).flatMap(quiz => quiz ? [quiz] : []);
}
export function useCurrentQuiz() { export function useCurrentQuiz() {
const quizId = useQuizStore(state => state.editQuizId); const quizId = useQuizStore(state => state.editQuizId);
const quiz = useQuizStore(state => state.quizById[quizId ?? -1]); const quizes = useQuizStore(state => state.quizes);
const updateQuiz = useCallback((updateFn: (quiz: Quiz) => void) => { const quiz = quizes.find(q => q.backendId === quizId);
updateQuizWithFnOptimistic(quizId, updateFn);
}, [quizId]);
return { quiz, updateQuiz }; return quiz;
} }

@ -1,19 +1,18 @@
import { Quiz } from "@model/quiz/quiz"; import { Quiz } from "@model/quiz/quiz";
import { QuizSetupStep } from "@model/quizSettings";
import { create } from "zustand"; import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
export type QuizStore = { export type QuizStore = {
quizById: Record<number, Quiz | undefined>; quizes: Quiz[];
editQuizId: number | null; editQuizId: number | null;
currentStep: QuizSetupStep; currentStep: number;
}; };
const initialState: QuizStore = { const initialState: QuizStore = {
quizById: {}, quizes: [],
editQuizId: null, editQuizId: null,
currentStep: 1, currentStep: 0,
}; };
export const useQuizStore = create<QuizStore>()( export const useQuizStore = create<QuizStore>()(
@ -31,6 +30,5 @@ export const useQuizStore = create<QuizStore>()(
editQuizId: state.editQuizId, editQuizId: state.editQuizId,
currentStep: state.currentStep, currentStep: state.currentStep,
}), }),
} })
)
); );

@ -1,14 +1,7 @@
import ChartPieIcon from "@icons/ChartPieIcon";
import CollapseMenuIcon from "@icons/CollapseMenuIcon"; import CollapseMenuIcon from "@icons/CollapseMenuIcon";
import ContactBookIcon from "@icons/ContactBookIcon";
import FlowArrowIcon from "@icons/FlowArrowIcon";
import GearIcon from "@icons/GearIcon"; import GearIcon from "@icons/GearIcon";
import LayoutIcon from "@icons/LayoutIcon";
import MegaphoneIcon from "@icons/MegaphoneIcon";
import PencilCircleIcon from "@icons/PencilCircleIcon"; import PencilCircleIcon from "@icons/PencilCircleIcon";
import PuzzlePieceIcon from "@icons/PuzzlePieceIcon"; import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
import QuestionIcon from "@icons/QuestionIcon";
import QuestionsMapIcon from "@icons/QuestionsMapIcon";
import TagIcon from "@icons/TagIcon"; import TagIcon from "@icons/TagIcon";
import { quizSetupSteps } from "@model/quizSettings"; import { quizSetupSteps } from "@model/quizSettings";
import { import {
@ -23,15 +16,6 @@ import { useQuizStore } from "@root/quizes/store";
import { useState } from "react"; import { useState } from "react";
import MenuItem from "./MenuItem"; import MenuItem from "./MenuItem";
const createQuizMenuItems = [
[LayoutIcon, "Стартовая страница"],
[QuestionIcon, "Вопросы"],
[ChartPieIcon, "Результаты"],
[QuestionsMapIcon, "Карта вопросов"],
[ContactBookIcon, "Форма контактов"],
[FlowArrowIcon, "Установка квиза"],
[MegaphoneIcon, "Запуск рекламы"],
] as const;
const quizSettingsMenuItems = [ const quizSettingsMenuItems = [
[TagIcon, "Дополнения"], [TagIcon, "Дополнения"],
@ -43,7 +27,6 @@ const quizSettingsMenuItems = [
export default function Sidebar() { export default function Sidebar() {
const theme = useTheme(); const theme = useTheme();
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false); const [isMenuCollapsed, setIsMenuCollapsed] = useState(false);
const [progress, setProgress] = useState<number>(1 / 7);
const currentStep = useQuizStore(state => state.currentStep); const currentStep = useQuizStore(state => state.currentStep);
const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev); const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev);
@ -100,19 +83,20 @@ export default function Sidebar() {
</IconButton> </IconButton>
</Box> </Box>
<List disablePadding> <List disablePadding>
{createQuizMenuItems.map((menuItem, index) => { {quizSetupSteps.map((menuItem, index) => {
const Icon = menuItem[0]; const Icon = menuItem.sidebarIcon;
return ( return (
<MenuItem <MenuItem
onClick={() => setCurrentStep(index + 1)} onClick={() => setCurrentStep(index)}
key={menuItem[1]} key={index}
text={menuItem[1]} text={menuItem.sidebarText}
isCollapsed={isMenuCollapsed} isCollapsed={isMenuCollapsed}
isActive={quizSetupSteps[currentStep].displayStep === index + 1} isActive={currentStep === index}
icon={ icon={
<Icon <Icon
color={ color={
quizSetupSteps[currentStep].displayStep === index + 1 currentStep === index
? theme.palette.brightPurple.main ? theme.palette.brightPurple.main
: isMenuCollapsed : isMenuCollapsed
? "white" ? "white"
@ -141,14 +125,15 @@ export default function Sidebar() {
Настройки квиза Настройки квиза
</Typography> </Typography>
)} )}
{/* <List disablePadding> // TODO <List disablePadding>
{quizSettingsMenuItems.map((menuItem, index) => { {quizSettingsMenuItems.map((menuItem, index) => {
const Icon = menuItem[0]; const Icon = menuItem[0];
const totalIndex = index + createQuizMenuItems.length; const totalIndex = index + quizSetupSteps.length;
const isActive = listQuizes[quizId].step === totalIndex + 1; const isActive = currentStep === totalIndex + 1;
return ( return (
<MenuItem <MenuItem
onClick={() => updateQuizesList(quizId, { step: totalIndex + 1 })} onClick={() => null}
key={menuItem[1]} key={menuItem[1]}
text={menuItem[1]} text={menuItem[1]}
isActive={isActive} isActive={isActive}
@ -169,7 +154,7 @@ export default function Sidebar() {
/> />
); );
})} })}
</List> */} </List>
</Box> </Box>
); );
} }

@ -11,7 +11,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
export default function QuizPreviewLayout() { export default function QuizPreviewLayout() {
const theme = useTheme(); const theme = useTheme();
const { quiz } = useCurrentQuiz(); const quiz = useCurrentQuiz();
const isTablet = useMediaQuery(theme.breakpoints.down(630)); const isTablet = useMediaQuery(theme.breakpoints.down(630));
if (!quiz) return null; if (!quiz) return null;

@ -1,9 +1,9 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import MobileStepper from "@mui/material/MobileStepper"; import MobileStepper from "@mui/material/MobileStepper";
import { QuizSetupStep, maxDisplayQuizSetupSteps, quizSetupSteps } from "@model/quizSettings"; import { maxQuizSetupSteps, quizSetupSteps } from "@model/quizSettings";
interface Props { interface Props {
activeStep: QuizSetupStep; activeStep: number;
} }
export default function ProgressMobileStepper({ export default function ProgressMobileStepper({
@ -28,9 +28,9 @@ export default function ProgressMobileStepper({
> >
<MobileStepper <MobileStepper
variant="progress" variant="progress"
steps={maxDisplayQuizSetupSteps} steps={maxQuizSetupSteps}
position="static" position="static"
activeStep={quizSetupSteps[activeStep].displayStep - 1} activeStep={activeStep}
sx={{ sx={{
width: "100%", width: "100%",
flexGrow: 1, flexGrow: 1,
@ -52,9 +52,9 @@ export default function ProgressMobileStepper({
sx={{ fontWeight: 400, fontSize: "12px", lineHeight: "14.22px" }} sx={{ fontWeight: 400, fontSize: "12px", lineHeight: "14.22px" }}
> >
{" "} {" "}
Шаг {quizSetupSteps[activeStep].displayStep} из {maxDisplayQuizSetupSteps} Шаг {activeStep + 1 } из {maxQuizSetupSteps}
</Typography> </Typography>
<Typography>{quizSetupSteps[activeStep].text}</Typography> <Typography>{quizSetupSteps[activeStep].stepperText}</Typography>
</Box> </Box>
</Box> </Box>
); );

@ -1,5 +1,4 @@
import { QuizSetupStep } from "@model/quizSettings"; import { QuizResultsType, QuizStartpageType, QuizType } from "@model/quizSettings";
import { notReachable } from "../utils/notReachable";
import ContactFormPage from "../pages/ContactFormPage/ContactFormPage"; import ContactFormPage from "../pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "../pages/InstallQuiz/InstallQuiz"; import InstallQuiz from "../pages/InstallQuiz/InstallQuiz";
import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage"; import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage";
@ -13,25 +12,33 @@ import Steptwo from "../pages/startPage/steptwo";
interface Props { interface Props {
activeStep: QuizSetupStep; activeStep: number;
quizType: string; quizType: QuizType;
quizStartPageType: QuizStartpageType;
quizResults: QuizResultsType;
} }
export default function SwitchStepPages({ export default function SwitchStepPages({
activeStep = 1, activeStep = 1,
quizType, quizType,
quizStartPageType,
quizResults,
}: Props) { }: Props) {
switch (activeStep) { switch (activeStep) {
case 1: return <StepOne />; case 0: {
case 2: return <Steptwo />; if (!quizType) return <StepOne />;
case 3: return <StartPageSettings />; if (!quizStartPageType) return <Steptwo />;
case 4: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />; return <StartPageSettings />;
case 5: return <Result />; }
case 6: return <Setting />; case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />;
case 7: return <QuestionsMap />; case 2: {
case 8: return <ContactFormPage />; if (!quizResults) return <Result />;
case 9: return <InstallQuiz />; return <Setting />;
case 10: return <>Реклама</>; }
default: return notReachable(activeStep); case 3: return <QuestionsMap />;
case 4: return <ContactFormPage />;
case 5: return <InstallQuiz />;
case 6: return <>Реклама</>;
default: throw new Error(`Invalid quiz setup step: ${activeStep}`);
} }
} }

@ -1,12 +1,12 @@
export class RequestQueue<T> { export class RequestQueue<T = unknown> {
private pendingPromise = false; private pendingPromise = false;
private items: Array<{ private items: Array<{
action: (prevPayload?: T | null) => Promise<T>; action: () => Promise<T>;
resolve: (value: T) => void; resolve: (value: T) => void;
reject: (reason?: any) => void; reject: (reason?: any) => void;
}> = []; }> = [];
enqueue(action: (prevPayload?: T | null) => Promise<T>) { enqueue(action: () => Promise<T>) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.items.length === 2) { if (this.items.length === 2) {
this.items[1] = { action, resolve, reject }; this.items[1] = { action, resolve, reject };
@ -17,17 +17,15 @@ export class RequestQueue<T> {
}); });
} }
async dequeue(prevPayload?: T | null) { async dequeue() {
if (this.pendingPromise) return; if (this.pendingPromise) return;
const item = this.items.shift(); const item = this.items.shift();
if (!item) return; if (!item) return;
let payload: T | null = null;
try { try {
this.pendingPromise = true; this.pendingPromise = true;
payload = await item.action(prevPayload); const payload = await item.action();
this.pendingPromise = false; this.pendingPromise = false;
item.resolve(payload); item.resolve(payload);
@ -35,7 +33,7 @@ export class RequestQueue<T> {
this.pendingPromise = false; this.pendingPromise = false;
item.reject(e); item.reject(e);
} finally { } finally {
this.dequeue(payload); this.dequeue();
} }
} }
} }