diff --git a/src/api/quiz.ts b/src/api/quiz.ts index befe9f3d..1921ca7a 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -71,7 +71,7 @@ function addQuizImages(quizId: number, image: Blob) { return makeRequest({ url: `${baseUrl}/quiz/putImages`, body: formData, - method: "POST", + method: "PUT", }); } @@ -100,7 +100,6 @@ const defaultCreateQuizBody: CreateQuizRequest = { "due_to": 0, "time_of_passing": 0, "pausable": false, - "question_cnt": 0, "super": false, "group_id": 0, }; diff --git a/src/constants/base.ts b/src/constants/base.ts index c1507ecc..0fa0b59c 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase } from "../model/questionTypes/shared"; -export const QUIZ_QUESTION_BASE: Omit = { +export const QUIZ_QUESTION_BASE: Omit = { quizId: 0, description: "", page: 0, diff --git a/src/constants/date.ts b/src/constants/date.ts index e6d2faa1..6455fac2 100644 --- a/src/constants/date.ts +++ b/src/constants/date.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionDate } from "../model/questionTypes/date"; -export const QUIZ_QUESTION_DATE: Omit = { +export const QUIZ_QUESTION_DATE: Omit = { ...QUIZ_QUESTION_BASE, type: "date", content: { diff --git a/src/constants/default.ts b/src/constants/default.ts index e33dc822..2fb3702d 100644 --- a/src/constants/default.ts +++ b/src/constants/default.ts @@ -13,7 +13,7 @@ import { QUIZ_QUESTION_VARIANT } from "./variant"; import { QUIZ_QUESTION_VARIMG } from "./varimg"; -export const defaultQuestionByType: Record> = { +export const defaultQuestionByType: Record> = { "date": QUIZ_QUESTION_DATE, "emoji": QUIZ_QUESTION_EMOJI, "file": QUIZ_QUESTION_FILE, diff --git a/src/constants/emoji.ts b/src/constants/emoji.ts index 1dcb25bc..7d7ce84f 100644 --- a/src/constants/emoji.ts +++ b/src/constants/emoji.ts @@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionEmoji } from "../model/questionTypes/emoji"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_EMOJI: Omit = { +export const QUIZ_QUESTION_EMOJI: Omit = { ...QUIZ_QUESTION_BASE, type: "emoji", content: { diff --git a/src/constants/file.ts b/src/constants/file.ts index a471b7bd..16118e6e 100644 --- a/src/constants/file.ts +++ b/src/constants/file.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionFile } from "../model/questionTypes/file"; -export const QUIZ_QUESTION_FILE: Omit = { +export const QUIZ_QUESTION_FILE: Omit = { ...QUIZ_QUESTION_BASE, type: "file", content: { diff --git a/src/constants/images.ts b/src/constants/images.ts index 7bb2bcc6..dd92dc9a 100644 --- a/src/constants/images.ts +++ b/src/constants/images.ts @@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionImages } from "../model/questionTypes/images"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_IMAGES: Omit = { +export const QUIZ_QUESTION_IMAGES: Omit = { ...QUIZ_QUESTION_BASE, type: "images", content: { diff --git a/src/constants/number.ts b/src/constants/number.ts index 466fdf30..96cbd5a3 100644 --- a/src/constants/number.ts +++ b/src/constants/number.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionNumber } from "../model/questionTypes/number"; -export const QUIZ_QUESTION_NUMBER: Omit = { +export const QUIZ_QUESTION_NUMBER: Omit = { ...QUIZ_QUESTION_BASE, type: "number", content: { diff --git a/src/constants/page.ts b/src/constants/page.ts index deee7a63..5fbb2965 100644 --- a/src/constants/page.ts +++ b/src/constants/page.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionPage } from "../model/questionTypes/page"; -export const QUIZ_QUESTION_PAGE: Omit = { +export const QUIZ_QUESTION_PAGE: Omit = { ...QUIZ_QUESTION_BASE, type: "page", content: { diff --git a/src/constants/rating.ts b/src/constants/rating.ts index e6c7bdc7..cef8bc24 100644 --- a/src/constants/rating.ts +++ b/src/constants/rating.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionRating } from "../model/questionTypes/rating"; -export const QUIZ_QUESTION_RATING: Omit = { +export const QUIZ_QUESTION_RATING: Omit = { ...QUIZ_QUESTION_BASE, type: "rating", content: { diff --git a/src/constants/select.ts b/src/constants/select.ts index a37f8c11..c3575fab 100644 --- a/src/constants/select.ts +++ b/src/constants/select.ts @@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionSelect } from "../model/questionTypes/select"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_SELECT: Omit = { +export const QUIZ_QUESTION_SELECT: Omit = { ...QUIZ_QUESTION_BASE, type: "select", content: { diff --git a/src/constants/text.ts b/src/constants/text.ts index 57622295..a64c4f9d 100644 --- a/src/constants/text.ts +++ b/src/constants/text.ts @@ -2,7 +2,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionText } from "../model/questionTypes/text"; -export const QUIZ_QUESTION_TEXT: Omit = { +export const QUIZ_QUESTION_TEXT: Omit = { ...QUIZ_QUESTION_BASE, type: "text", content: { diff --git a/src/constants/variant.ts b/src/constants/variant.ts index 02a825de..2fb787d1 100644 --- a/src/constants/variant.ts +++ b/src/constants/variant.ts @@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionVariant } from "../model/questionTypes/variant"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_VARIANT: Omit = { +export const QUIZ_QUESTION_VARIANT: Omit = { ...QUIZ_QUESTION_BASE, type: "variant", content: { diff --git a/src/constants/varimg.ts b/src/constants/varimg.ts index ef16eaef..ef971335 100644 --- a/src/constants/varimg.ts +++ b/src/constants/varimg.ts @@ -3,7 +3,7 @@ import { QUIZ_QUESTION_BASE } from "./base"; import type { QuizQuestionVarImg } from "../model/questionTypes/varimg"; import { nanoid } from "nanoid"; -export const QUIZ_QUESTION_VARIMG: Omit = { +export const QUIZ_QUESTION_VARIMG: Omit = { ...QUIZ_QUESTION_BASE, type: "varimg", content: { diff --git a/src/model/question/edit.ts b/src/model/question/edit.ts index 484ef709..66b95bf9 100644 --- a/src/model/question/edit.ts +++ b/src/model/question/edit.ts @@ -16,9 +16,9 @@ export interface EditQuestionResponse { updated: number; } -export function questionToEditQuestionRequest(question: AnyQuizQuestion, newId?: number): EditQuestionRequest { +export function questionToEditQuestionRequest(question: AnyQuizQuestion): EditQuestionRequest { return { - id: newId ?? question.id, + id: question.backendId, title: question.title, desc: question.description, type: question.type, diff --git a/src/model/question/question.ts b/src/model/question/question.ts index bcd013d7..f25d9cb8 100644 --- a/src/model/question/question.ts +++ b/src/model/question/question.ts @@ -1,5 +1,6 @@ import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { defaultQuestionByType } from "../../constants/default"; +import { nanoid } from "nanoid"; export type QuestionType = @@ -53,8 +54,8 @@ export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion } return { - id: rawQuestion.id, - fixedId: rawQuestion.id, + backendId: rawQuestion.id, + id: nanoid(), description: rawQuestion.description, page: rawQuestion.page, quizId: rawQuestion.quiz_id, diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index f4a30e34..d48e3f98 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -48,9 +48,9 @@ export interface ImageQuestionVariant extends QuestionVariant { } export interface QuizQuestionBase { - id: number; - /** fixed id for using it as a key prop */ - fixedId: number; + backendId: number; + /** Stable id, generated on client */ + id: string; quizId: number; title: string; description: string; diff --git a/src/model/quiz/create.ts b/src/model/quiz/create.ts index ebe6d2c5..c4271a79 100644 --- a/src/model/quiz/create.ts +++ b/src/model/quiz/create.ts @@ -26,7 +26,7 @@ export interface CreateQuizRequest { /** true if it is allowed for pause quiz */ pausable: boolean; /** count of questions */ - question_cnt: number; + question_cnt?: number; /** set true if squiz realize group functionality */ super: boolean; /** group of new quiz */ diff --git a/src/model/quiz/edit.ts b/src/model/quiz/edit.ts index 539decfb..a548a38c 100644 --- a/src/model/quiz/edit.ts +++ b/src/model/quiz/edit.ts @@ -43,9 +43,9 @@ export interface EditQuizResponse { updated: number; } -export function quizToEditQuizRequest(quiz: Quiz, newId?: number): EditQuizRequest { +export function quizToEditQuizRequest(quiz: Quiz): EditQuizRequest { return { - id: newId ?? quiz.id, + id: quiz.backendId, fp: quiz.fingerprinting, rep: quiz.repeatable, note_prevented: quiz.note_prevented, diff --git a/src/model/quiz/quiz.ts b/src/model/quiz/quiz.ts index 35aeb3e9..0de02cdc 100644 --- a/src/model/quiz/quiz.ts +++ b/src/model/quiz/quiz.ts @@ -1,9 +1,12 @@ import { QuizConfig, defaultQuizConfig } from "@model/quizSettings"; +import { nanoid } from "nanoid"; export interface Quiz { + /** Stable id, generated on client */ + id: string; /** Id of created quiz */ - id: number; + backendId: number; /** string id for customers */ qid: string; /** true if quiz deleted */ @@ -112,13 +115,6 @@ export interface RawQuiz { group_id: number; } -export function quizToRawQuiz(quiz: Quiz): RawQuiz { - return { - ...quiz, - config: JSON.stringify(quiz.config), - }; -} - export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz { let config = defaultQuizConfig; @@ -131,5 +127,7 @@ export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz { return { ...rawQuiz, config, + backendId: rawQuiz.id, + id: nanoid(), }; } diff --git a/src/model/quizSettings.ts b/src/model/quizSettings.ts index dc44ec9d..33e3533a 100644 --- a/src/model/quizSettings.ts +++ b/src/model/quizSettings.ts @@ -1,31 +1,38 @@ -export const quizSetupSteps = { - 1: { displayStep: 1, text: "Настройка стартовой страницы" }, - 2: { displayStep: 1, text: "Настройка стартовой страницы" }, - 3: { displayStep: 1, text: "Настройка стартовой страницы" }, - 4: { displayStep: 2, text: "Задайте вопросы" }, - 5: { displayStep: 3, text: "Настройте авторезультаты" }, - 6: { displayStep: 3, text: "Настройте авторезультаты" }, - 7: { displayStep: 4, text: "Оценка графа карты вопросов" }, - 8: { displayStep: 5, text: "Настройте форму контактов" }, - 9: { displayStep: 6, text: "Установите квиз" }, - 10: { displayStep: 7, text: "Запустите рекламу" }, -} as const; +import ChartPieIcon from "@icons/ChartPieIcon"; +import ContactBookIcon from "@icons/ContactBookIcon"; +import FlowArrowIcon from "@icons/FlowArrowIcon"; +import LayoutIcon from "@icons/LayoutIcon"; +import MegaphoneIcon from "@icons/MegaphoneIcon"; +import QuestionIcon from "@icons/QuestionIcon"; +import QuestionsMapIcon from "@icons/QuestionsMapIcon"; -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 QuizType = "quiz" | "form" | null; + +export type QuizResultsType = true | null; + export interface QuizConfig { - type: "quiz" | "form"; + type: QuizType; logo: string; noStartPage: boolean; startpageType: QuizStartpageType; + results: QuizResultsType; startpage: { description: string; button: string; @@ -49,19 +56,20 @@ export interface QuizConfig { } export const defaultQuizConfig: QuizConfig = { - type: "quiz", + type: null, logo: "", noStartPage: false, - startpageType: "standard", + startpageType: null, + results: null, startpage: { description: "", button: "", position: "left", background: { type: null, - desktop: "", - mobile: "", - video: "", + desktop: "https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg", + mobile: "https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png", + video: "https://youtu.be/dbaPkCiLPKQ", cycle: false, }, }, diff --git a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx index 466a2fb7..686c2057 100644 --- a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx +++ b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx @@ -22,7 +22,7 @@ import type { ImageQuestionVariant, QuestionVariant } from "../../../model/quest type AnswerItemProps = { index: number; - questionId: number; + questionId: string; variant: QuestionVariant | ImageQuestionVariant; largeCheck: boolean; additionalContent?: ReactNode; @@ -40,11 +40,7 @@ export const AnswerItem = ({ const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(790)); const [isOpen, setIsOpen] = useState(false); - const [anchorEl, setAnchorEl] = useState(null); - - const setQuestionVariantAnswer = useDebouncedCallback((value) => { - setQuestionVariantField(questionId, variant.id, "answer", value); - }, 1000); + const [anchorEl, setAnchorEl] = useState(null); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -76,7 +72,9 @@ export const AnswerItem = ({ focused={false} placeholder={"Добавьте ответ"} multiline={largeCheck} - onChange={({ target }) => setQuestionVariantAnswer(target.value)} + onChange={({ target }) => { + setQuestionVariantField(questionId, variant.id, "answer", target.value) + }} onKeyDown={(event: KeyboardEvent) => { if (event.code === "Enter" && !largeCheck) { addQuestionVariant(questionId); diff --git a/src/pages/Questions/ButtonsOptions.tsx b/src/pages/Questions/ButtonsOptions.tsx index 75294822..011aa4ce 100644 --- a/src/pages/Questions/ButtonsOptions.tsx +++ b/src/pages/Questions/ButtonsOptions.tsx @@ -11,7 +11,7 @@ import { useMediaQuery, useTheme, } 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 { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon"; import Branching from "../../assets/icons/questionsPage/branching"; @@ -38,7 +38,7 @@ export default function ButtonsOptions({ const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); const openedModal = () => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.openedModalSettings = true; }); }; @@ -279,6 +279,7 @@ export default function ButtonsOptions({ deleteQuestion(question.id); }} + data-cy="delete-question" > diff --git a/src/pages/Questions/ButtonsOptionsAndPict.tsx b/src/pages/Questions/ButtonsOptionsAndPict.tsx index 1101399b..b9063bd8 100644 --- a/src/pages/Questions/ButtonsOptionsAndPict.tsx +++ b/src/pages/Questions/ButtonsOptionsAndPict.tsx @@ -10,7 +10,7 @@ import { useMediaQuery, useTheme, } 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 { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal"; import { useEffect, useState } from "react"; @@ -184,7 +184,7 @@ export default function ButtonsOptionsAndPict({ onMouseLeave={() => setButtonHover("")} onClick={() => { SSHC("branching"); - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.openedModalSettings = true; }); }} @@ -320,6 +320,7 @@ export default function ButtonsOptionsAndPict({ deleteQuestion(question.id); }} + data-cy="delete-question" > diff --git a/src/pages/Questions/DataOptions/settingData.tsx b/src/pages/Questions/DataOptions/settingData.tsx index f62d9369..ebd53df8 100644 --- a/src/pages/Questions/DataOptions/settingData.tsx +++ b/src/pages/Questions/DataOptions/settingData.tsx @@ -1,5 +1,5 @@ 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -19,7 +19,7 @@ export default function SettingsData({ question }: SettingsDataProps) { const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "date") return; question.content.dateRange = target.checked; @@ -60,7 +60,7 @@ export default function SettingsData({ question }: SettingsDataProps) { label={"Выбор времени"} checked={question.content.time} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "date") return; question.content.time = target.checked; @@ -88,7 +88,7 @@ export default function SettingsData({ question }: SettingsDataProps) { label={"Необязательный вопрос"} checked={!question.required} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.required = !target.checked; }); }} @@ -109,7 +109,7 @@ export default function SettingsData({ question }: SettingsDataProps) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName : ""; }); diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index fcea54b2..a639bd31 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -29,7 +29,7 @@ import { useMediaQuery, useTheme, } 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 type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { useDebouncedCallback } from "use-debounce"; @@ -54,7 +54,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging const anchorRef = useRef(null); const setTitle = useDebouncedCallback((title) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.title = title; }); }, 200); @@ -250,6 +250,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging deleteQuestion(question.id); }} + data-cy="delete-question" > { - const { quiz } = useCurrentQuiz(); - useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), { + const quiz = useCurrentQuiz(); + const { isLoading } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), { onSuccess: setQuestions, onError: error => { const message = isAxiosError(error) ? (error.response?.data ?? "") : ""; @@ -29,6 +29,8 @@ export const DraggableList = () => { if (destination) reorderQuestions(source.index, destination.index); }; + if (isLoading && !questions) return Загрузка вопросов...; + return ( @@ -36,7 +38,7 @@ export const DraggableList = () => { {questions.map((question, index) => ( { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); const debounceAnswer = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "select") return; question.content.default = value; }); - }, 1000); + }, 200); return ( <> @@ -73,7 +73,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) { checked={question.content.multi} dataCy="multiple-answers-checkbox" handleChange={({ target }) => - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "select") return; question.content.multi = target.checked; @@ -130,7 +130,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) { label={"Необязательный вопрос"} checked={!question.required} handleChange={(e) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.required = !e.target.checked; }); }} @@ -141,7 +141,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName : ""; }); diff --git a/src/pages/Questions/Emoji/Emoji.tsx b/src/pages/Questions/Emoji/Emoji.tsx index d6a87012..2fb3ea30 100644 --- a/src/pages/Questions/Emoji/Emoji.tsx +++ b/src/pages/Questions/Emoji/Emoji.tsx @@ -9,7 +9,7 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { addQuestionVariant, updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { addQuestionVariant, updateQuestion } from "@root/questions/actions"; import { EmojiPicker } from "@ui_kit/EmojiPicker"; import { useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; @@ -181,7 +181,7 @@ export default function Emoji({ question }: Props) { { setOpen(false); - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "emoji") return; const variant = question.content.variants.find(v => v.id === selectedVariant); diff --git a/src/pages/Questions/Emoji/settingEmoji.tsx b/src/pages/Questions/Emoji/settingEmoji.tsx index ab68e239..ec2753e9 100644 --- a/src/pages/Questions/Emoji/settingEmoji.tsx +++ b/src/pages/Questions/Emoji/settingEmoji.tsx @@ -1,5 +1,5 @@ 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -20,7 +20,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) { const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "emoji") return; question.content.multi = target.checked; @@ -60,7 +60,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) { sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} label={'Вариант "свой ответ"'} checked={question.content.own} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "emoji") return; question.content.own = target.checked; @@ -86,7 +86,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) { sx={{ mr: isMobile ? "0px" : "16px" }} label={"Необязательный вопрос"} checked={!question.required} - handleChange={(e) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={(e) => updateQuestion(question.id, question => { if (question.type !== "emoji") return; question.content.required = !e.target.checked; @@ -107,7 +107,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) { }} label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName : ""; })} diff --git a/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx b/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx index 2fb6383b..04a7743a 100644 --- a/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx +++ b/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx @@ -14,7 +14,7 @@ import { import { useState } from "react"; import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions"; 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 { AnyQuizQuestion } from "../../../../model/questionTypes/shared"; @@ -118,7 +118,7 @@ export const ChooseAnswerModal = ({ onClick={() => { setOpenModal(false); - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.type = selectedValue; }); }} diff --git a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx index 3ba6aed0..51dcb772 100644 --- a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx +++ b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx @@ -1,5 +1,5 @@ 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 { Draggable } from "react-beautiful-dnd"; import { AnyQuizQuestion, QuizQuestionBase } from "../../../../model/questionTypes/shared"; @@ -46,7 +46,7 @@ export default memo( { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.deleted = false; }); }} diff --git a/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx b/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx index e0881171..e54ece63 100644 --- a/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx @@ -12,7 +12,7 @@ import Page from "@icons/questionsPage/page"; import RatingIcon from "@icons/questionsPage/rating"; import Slider from "@icons/questionsPage/slider"; 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 { useRef, useState } from "react"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; @@ -37,10 +37,10 @@ export default function QuestionsPageCard({ const anchorRef = useRef(null); const setTitle = useDebouncedCallback((title) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.title = title; }); - }, 1000); + }, 200); return ( <> diff --git a/src/pages/Questions/Form/FormDraggableList/index.tsx b/src/pages/Questions/Form/FormDraggableList/index.tsx index 52cd5139..cf3673a7 100644 --- a/src/pages/Questions/Form/FormDraggableList/index.tsx +++ b/src/pages/Questions/Form/FormDraggableList/index.tsx @@ -13,8 +13,8 @@ import { enqueueSnackbar } from "notistack"; export const FormDraggableList = () => { - const { quiz } = useCurrentQuiz(); - useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), { + const quiz = useCurrentQuiz(); + useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), { onSuccess: setQuestions, onError: error => { const message = isAxiosError(error) ? (error.response?.data ?? "") : ""; @@ -36,7 +36,7 @@ export const FormDraggableList = () => { {questions.map((question, index) => ( { - createQuestion(quiz.id); + createQuestion(quiz.backendId); }} + data-cy="create-question" > diff --git a/src/pages/Questions/Form/FormTypeQuestions.tsx b/src/pages/Questions/Form/FormTypeQuestions.tsx index 9e2ab871..6c5c7ff4 100644 --- a/src/pages/Questions/Form/FormTypeQuestions.tsx +++ b/src/pages/Questions/Form/FormTypeQuestions.tsx @@ -17,7 +17,7 @@ import type { AnyQuizQuestion, } from "../../../model/questionTypes/shared"; import { QuestionType } from "@model/question/question"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; type ButtonTypeQuestion = { @@ -83,7 +83,7 @@ export default function FormTypeQuestions({ question }: Props) { { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.type = questionType; }) }} diff --git a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx index f5f61d1f..4f1bbe2b 100644 --- a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx +++ b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx @@ -1,5 +1,5 @@ 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -18,16 +18,16 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic const isMobile = useMediaQuery(theme.breakpoints.down(680)); const setReplText = useDebouncedCallback((replText) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "varimg") return; question.content.replText = replText; }); - }, 1000); + }, 200); const setDescription = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( <> @@ -59,7 +59,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic sx={{ mr: isMobile ? "0px" : "16px" }} label={'Вариант "свой ответ"'} checked={question.content.own} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "varimg") return; question.content.own = target.checked; @@ -112,7 +112,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic sx={{ mr: isMobile ? "0px" : "16px" }} label={"Необязательный вопрос"} checked={question.content.required} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "varimg") return; question.content.required = target.checked; @@ -126,7 +126,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic }} label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = ""; })} diff --git a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx index 305a6c2d..f47b281b 100644 --- a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx +++ b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx @@ -6,7 +6,7 @@ import { 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -44,10 +44,10 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro const debounced = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); const updateProportions = (proportions: Proportion) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.xy = proportions; @@ -115,7 +115,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro label={"Можно несколько"} checked={question.content.multi} dataCy="multiple-answers-checkbox" - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.multi = target.checked; @@ -129,7 +129,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro label={"Большие картинки"} checked={question.content.largeCheck} handleChange={({ target }) => - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.largeCheck = target.checked; @@ -141,7 +141,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} label={'Вариант "свой ответ"'} checked={question.content.own} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.own = target.checked; @@ -183,7 +183,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro Формат updateQuestionWithFnOptimistic(question.id, question => { + onClick={() => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.format = "carousel"; @@ -193,7 +193,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro Icon={FormatIcon2} /> updateQuestionWithFnOptimistic(question.id, question => { + onClick={() => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.format = "masonry"; @@ -212,7 +212,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro sx={{ alignItems: isMobile ? "flex-start" : "" }} label={"Необязательный вопрос"} checked={question.content.required} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.required = target.checked; @@ -233,7 +233,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro }} label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} - handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => { + handleChange={({ target }) => updateQuestion(question.id, question => { if (question.type !== "images") return; question.content.innerNameCheck = target.checked; diff --git a/src/pages/Questions/OwnTextField/OwnTextField.tsx b/src/pages/Questions/OwnTextField/OwnTextField.tsx index 94a8b734..9a3e5a3c 100644 --- a/src/pages/Questions/OwnTextField/OwnTextField.tsx +++ b/src/pages/Questions/OwnTextField/OwnTextField.tsx @@ -1,5 +1,5 @@ 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 { useState } from "react"; import { useDebouncedCallback } from "use-debounce"; @@ -20,12 +20,12 @@ export default function OwnTextField({ question }: Props) { const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const setPlaceholder = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "text") return; question.content.placeholder = value; }); - }, 1000); + }, 200); const SSHC = (data: string) => { setSwitchState(data); diff --git a/src/pages/Questions/OwnTextField/settingTextField.tsx b/src/pages/Questions/OwnTextField/settingTextField.tsx index 964f7ca9..ad74ac37 100644 --- a/src/pages/Questions/OwnTextField/settingTextField.tsx +++ b/src/pages/Questions/OwnTextField/settingTextField.tsx @@ -9,7 +9,7 @@ import { 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 CustomTextField from "@ui_kit/CustomTextField"; import CheckedIcon from "@ui_kit/RadioCheck"; @@ -43,7 +43,7 @@ export default function SettingTextField({ const debounced = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( question.content.answerType === value )} onChange={({ target }: React.ChangeEvent) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "text") return; question.content.answerType = ANSWER_TYPES[Number(target.value)].value; @@ -119,7 +119,7 @@ export default function SettingTextField({ label={"Только числа"} checked={question.content.onlyNumbers} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "text") return; question.content.onlyNumbers = target.checked; @@ -157,7 +157,7 @@ export default function SettingTextField({ label={"Автозаполнение адреса"} checked={question.content.autofill} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.autofill = target.checked; }); }} @@ -171,7 +171,7 @@ export default function SettingTextField({ label={"Необязательный вопрос"} checked={!question.required} handleChange={(e) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.required = !e.target.checked; }); }} @@ -193,7 +193,7 @@ export default function SettingTextField({ label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName diff --git a/src/pages/Questions/PageOptions/PageOptions.tsx b/src/pages/Questions/PageOptions/PageOptions.tsx index 087e77fa..dbde17a5 100644 --- a/src/pages/Questions/PageOptions/PageOptions.tsx +++ b/src/pages/Questions/PageOptions/PageOptions.tsx @@ -2,7 +2,7 @@ import { VideofileIcon } from "@icons/questionsPage/VideofileIcon"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { openCropModal } from "@root/cropModal"; 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 CustomTextField from "@ui_kit/CustomTextField"; 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 setText = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "page") return; question.content.text = value; }); - }, 1000); + }, 200); const SSHC = (data: string) => { setSwitchState(data); @@ -225,7 +225,7 @@ export default function PageOptions({ disableInput, question }: Props) { onClose={() => setOpenVideoModal(false)} video={question.content.video} onUpload={(url) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "page") return; question.content.video = url; diff --git a/src/pages/Questions/PageOptions/SettingPageOptions.tsx b/src/pages/Questions/PageOptions/SettingPageOptions.tsx index 4994a878..13fb7774 100644 --- a/src/pages/Questions/PageOptions/SettingPageOptions.tsx +++ b/src/pages/Questions/PageOptions/SettingPageOptions.tsx @@ -10,7 +10,7 @@ import CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; import type { QuizQuestionPage } from "../../../model/questionTypes/page"; -import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { setQuestionInnerName, updateQuestion } from "@root/questions/actions"; type SettingPageOptionsProps = { @@ -25,7 +25,7 @@ export default function SettingPageOptions({ const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = ""; }) diff --git a/src/pages/Questions/QuestionsPage.tsx b/src/pages/Questions/QuestionsPage.tsx index 59120fac..c731c4c3 100755 --- a/src/pages/Questions/QuestionsPage.tsx +++ b/src/pages/Questions/QuestionsPage.tsx @@ -7,7 +7,7 @@ import { useTheme, } from "@mui/material"; 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 QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import { createPortal } from "react-dom"; @@ -19,7 +19,7 @@ import { DraggableList } from "./DraggableList"; export default function QuestionsPage() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(660)); - const { quiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); if (!quiz) return null; @@ -59,13 +59,14 @@ export default function QuestionsPage() { > { - createQuestion(quiz.id); + createQuestion(quiz.backendId); }} sx={{ position: "fixed", left: isMobile ? "20px" : "250px", bottom: "20px", }} + data-cy="create-question" > @@ -73,6 +74,8 @@ export default function QuestionsPage() { diff --git a/src/pages/Questions/RatingOptions/RatingOptions.tsx b/src/pages/Questions/RatingOptions/RatingOptions.tsx index baae6bd4..9fd1dd82 100644 --- a/src/pages/Questions/RatingOptions/RatingOptions.tsx +++ b/src/pages/Questions/RatingOptions/RatingOptions.tsx @@ -18,7 +18,7 @@ import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon"; import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon"; import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; interface Props { @@ -43,19 +43,19 @@ export default function RatingOptions({ question }: Props) { const positiveRef = useRef(null); const debounceNegativeDescription = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.ratingNegativeDescription = value.substring(0, 15); }); - }, 500); + }, 200); const debouncePositiveDescription = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.ratingPositiveDescription = value.substring(0, 15); }); - }, 500); + }, 200); useEffect(() => { setNegativeText(question.content.ratingNegativeDescription); @@ -120,7 +120,7 @@ export default function RatingOptions({ question }: Props) { {...(itemNumber === 0 || itemNumber === question.content.steps - 1 ? { onClick: () => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.ratingExpanded = true; diff --git a/src/pages/Questions/RatingOptions/settingRating.tsx b/src/pages/Questions/RatingOptions/settingRating.tsx index 20884f28..e42b0294 100644 --- a/src/pages/Questions/RatingOptions/settingRating.tsx +++ b/src/pages/Questions/RatingOptions/settingRating.tsx @@ -1,6 +1,6 @@ import { QuizQuestionRating } from "@model/questionTypes/rating"; 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -27,7 +27,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); const buttonRatingForm: ButtonRatingFrom[] = [ { name: "star", icon: }, @@ -79,7 +79,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.form = name; @@ -121,7 +121,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { valueLabelDisplay="auto" sx={{ color: theme.palette.brightPurple.main, padding: "0" }} onChange={(_, value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.steps = Number(value) || 1; @@ -150,7 +150,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { label={"Необязательный вопрос"} checked={!question.required} handleChange={(e) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.required = !e.target.checked; @@ -169,7 +169,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "rating") return; question.content.innerNameCheck = target.checked; diff --git a/src/pages/Questions/SliderOptions/SliderOptions.tsx b/src/pages/Questions/SliderOptions/SliderOptions.tsx index 0048e70a..d44203a9 100644 --- a/src/pages/Questions/SliderOptions/SliderOptions.tsx +++ b/src/pages/Questions/SliderOptions/SliderOptions.tsx @@ -4,7 +4,7 @@ import ButtonsOptions from "../ButtonsOptions"; import CustomNumberField from "@ui_kit/CustomNumberField"; import SwitchSlider from "./switchSlider"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; interface Props { @@ -56,7 +56,7 @@ export default function SliderOptions({ question }: Props) { max={99} value={question.content.range.split("—")[0]} onChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; 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]); if (min >= max) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; 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) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.start = min; @@ -92,7 +92,7 @@ export default function SliderOptions({ question }: Props) { max={100} value={question.content.range.split("—")[1]} onChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.range = `${question.content.range.split("—")[0]}—${target.value}`; @@ -106,7 +106,7 @@ export default function SliderOptions({ question }: Props) { const range = max - min; if (max <= min) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.range = `${min}—${min + 1 >= 100 ? 100 : min + 1}`; @@ -114,7 +114,7 @@ export default function SliderOptions({ question }: Props) { } if (start > max) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.start = max; @@ -122,7 +122,7 @@ export default function SliderOptions({ question }: Props) { } if (step > max) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.step = min; @@ -158,7 +158,7 @@ export default function SliderOptions({ question }: Props) { max={Number(question.content.range.split("—")[1])} value={String(question.content.start)} onChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.start = Number(target.value); @@ -185,7 +185,7 @@ export default function SliderOptions({ question }: Props) { error={stepError} value={String(question.content.step)} onChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.step = Number(target.value); @@ -198,7 +198,7 @@ export default function SliderOptions({ question }: Props) { const step = Number(target.value); if (step > max) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.step = max; diff --git a/src/pages/Questions/SliderOptions/settingSlider.tsx b/src/pages/Questions/SliderOptions/settingSlider.tsx index 1c8d32f6..d39434c1 100644 --- a/src/pages/Questions/SliderOptions/settingSlider.tsx +++ b/src/pages/Questions/SliderOptions/settingSlider.tsx @@ -1,5 +1,5 @@ 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -19,7 +19,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.chooseRange = target.checked; @@ -80,7 +80,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { label={"Необязательный вопрос"} checked={!question.required} handleChange={(e) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.required = !e.target.checked; @@ -103,7 +103,7 @@ export default function SettingSlider({ question }: SettingSliderProps) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "number") return; question.content.innerNameCheck = target.checked; diff --git a/src/pages/Questions/TypeQuestions.tsx b/src/pages/Questions/TypeQuestions.tsx index c31c1905..7cab9887 100755 --- a/src/pages/Questions/TypeQuestions.tsx +++ b/src/pages/Questions/TypeQuestions.tsx @@ -11,7 +11,7 @@ import OptionsPict from "../../assets/icons/questionsPage/options_pict"; import Page from "../../assets/icons/questionsPage/page"; import RatingIcon from "../../assets/icons/questionsPage/rating"; import Slider from "../../assets/icons/questionsPage/slider"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; import type { AnyQuizQuestion, } from "../../model/questionTypes/shared"; @@ -42,7 +42,7 @@ export default function TypeQuestions({ question }: Props) { updateQuestionWithFnOptimistic(question.id, question => { + onClick={() => updateQuestion(question.id, question => { question.type = value; })} icon={icon} diff --git a/src/pages/Questions/UploadFile/UploadFile.tsx b/src/pages/Questions/UploadFile/UploadFile.tsx index a0342b7b..091b76f0 100644 --- a/src/pages/Questions/UploadFile/UploadFile.tsx +++ b/src/pages/Questions/UploadFile/UploadFile.tsx @@ -9,7 +9,7 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; import { useEffect, useState } from "react"; import ArrowDown from "../../../assets/icons/ArrowDownIcon"; import InfoIcon from "../../../assets/icons/InfoIcon"; @@ -50,7 +50,7 @@ export default function UploadFile({ question }: Props) { }; const handleChange = ({ target }: SelectChangeEvent) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "file") return; question.content.type = target.value as UploadFileType; @@ -63,7 +63,7 @@ export default function UploadFile({ question }: Props) { ); if (!isTypeSetted) { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (question.type !== "file") return; question.content.type = DESIGN_TYPES[0].value; diff --git a/src/pages/Questions/UploadFile/settingUpload.tsx b/src/pages/Questions/UploadFile/settingUpload.tsx index 579ff12b..a7e4cdc2 100644 --- a/src/pages/Questions/UploadFile/settingUpload.tsx +++ b/src/pages/Questions/UploadFile/settingUpload.tsx @@ -5,7 +5,7 @@ import { 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -23,7 +23,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) { const setInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.autofill = target.checked; }); }} @@ -61,7 +61,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) { label={"Необязательный вопрос"} checked={!question.required} handleChange={(e) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.required = !e.target.checked; }); }} @@ -82,7 +82,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName : ""; }); diff --git a/src/pages/Questions/answerOptions/responseSettings.tsx b/src/pages/Questions/answerOptions/responseSettings.tsx index 046401f0..8ed5dd2e 100644 --- a/src/pages/Questions/answerOptions/responseSettings.tsx +++ b/src/pages/Questions/answerOptions/responseSettings.tsx @@ -5,7 +5,7 @@ import { 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 CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; @@ -26,7 +26,7 @@ export default function ResponseSettings({ question }: Props) { const updateQuestionInnerName = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value); - }, 1000); + }, 200); return ( { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (!("largeCheck" in question.content)) return; question.content.largeCheck = target.checked; @@ -76,7 +76,7 @@ export default function ResponseSettings({ question }: Props) { checked={question.content.multi} dataCy="multiple-answers-checkbox" handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (!("multi" in question.content)) return; question.content.multi = target.checked; @@ -88,7 +88,7 @@ export default function ResponseSettings({ question }: Props) { label={'Вариант "свой ответ"'} checked={question.content.own} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { if (!("own" in question.content)) return; question.content.own = target.checked; @@ -124,7 +124,7 @@ export default function ResponseSettings({ question }: Props) { label={"Необязательный вопрос"} checked={!question.required} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.required = !target.checked; }); }} @@ -145,7 +145,7 @@ export default function ResponseSettings({ question }: Props) { label={"Внутреннее название вопроса"} checked={question.content.innerNameCheck} handleChange={({ target }) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.innerNameCheck = target.checked; question.content.innerName = target.checked ? question.content.innerName : ""; }); diff --git a/src/pages/Questions/branchingQuestions.tsx b/src/pages/Questions/branchingQuestions.tsx index 7905f44b..83cefc74 100644 --- a/src/pages/Questions/branchingQuestions.tsx +++ b/src/pages/Questions/branchingQuestions.tsx @@ -16,7 +16,7 @@ import { Typography, useTheme, } from "@mui/material"; -import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { updateQuestion } from "@root/questions/actions"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; import { useEffect, useRef, useState } from "react"; @@ -48,7 +48,7 @@ export default function BranchingQuestions({ }, [title]); const handleClose = () => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.openedModalSettings = false; }); }; @@ -144,7 +144,7 @@ export default function BranchingQuestions({ activeItemIndex={question.content.rule.show ? 0 : 1} sx={{ maxWidth: "140px" }} onChange={(action) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.rule.show = action === ACTIONS[0]; }); }} @@ -177,7 +177,7 @@ export default function BranchingQuestions({ { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.rule.reqs.splice(index, 1); }); }} @@ -190,7 +190,7 @@ export default function BranchingQuestions({ activeItemIndex={request.id ? Number(request.id) : -1} items={STIPULATIONS} onChange={(stipulation) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.rule.reqs[index].id = String( STIPULATIONS.findIndex((item) => item.includes(stipulation)) ); @@ -222,7 +222,7 @@ export default function BranchingQuestions({ const answerItemIndex = ANSWERS.findIndex( (answerItem) => answerItem === answer ); - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { const vars = question.content.rule.reqs[index].vars; if (vars.includes(answerItemIndex)) { vars.push(answerItemIndex); @@ -249,7 +249,7 @@ export default function BranchingQuestions({ label={ANSWERS[item]} variant="outlined" onDelete={() => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { const vars = question.content.rule.reqs[index].vars; const removedItemIndex = vars.findIndex((varItem) => varItem === item); @@ -280,7 +280,7 @@ export default function BranchingQuestions({ marginBottom: "10px", }} onClick={() => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.rule.reqs.push({ id: "", vars: [] }); }); }} @@ -292,7 +292,7 @@ export default function BranchingQuestions({ aria-labelledby="demo-controlled-radio-buttons-group" value={question.content.rule.or ? 1 : 0} onChange={(_, value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.rule.or = Boolean(Number(value)); }); }} diff --git a/src/pages/Questions/helpQuestions.tsx b/src/pages/Questions/helpQuestions.tsx index c70c5608..a997db2d 100644 --- a/src/pages/Questions/helpQuestions.tsx +++ b/src/pages/Questions/helpQuestions.tsx @@ -1,6 +1,6 @@ import { AnyQuizQuestion } from "@model/questionTypes/shared"; 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 SelectableButton from "@ui_kit/SelectableButton"; import UploadBox from "@ui_kit/UploadBox"; @@ -21,10 +21,10 @@ export default function HelpQuestions({ question }: HelpQuestionsProps) { const [backgroundType, setBackgroundType] = useState("text"); const updateQuestionHint = useDebouncedCallback((value) => { - updateQuestionWithFnOptimistic(question.id, question => { + updateQuestion(question.id, question => { question.content.hint.text = value; }); - }, 1000); + }, 200); return ( setOpen(false)} video={question.content.hint.video} - onUpload={url => updateQuestionWithFnOptimistic(question.id, question => { + onUpload={url => updateQuestion(question.id, question => { question.content.hint.video = url; })} /> diff --git a/src/pages/Result/Result.tsx b/src/pages/Result/Result.tsx index 2d9c20f4..5fc0b076 100644 --- a/src/pages/Result/Result.tsx +++ b/src/pages/Result/Result.tsx @@ -1,34 +1,39 @@ 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 Info from "../../assets/icons/Info"; - -import image from "../../assets/Rectangle 110.png"; -import { incrementCurrentStep } from "@root/quizes/actions"; export const Result = () => { + const quiz = useCurrentQuiz(); - return ( - - - - - - - - - - - - ); + if (!quiz) return null; + + return ( + + + + + + + + + + + + ); }; diff --git a/src/pages/auth/Signin.tsx b/src/pages/auth/Signin.tsx index 1c6d8646..6d2d9f44 100644 --- a/src/pages/auth/Signin.tsx +++ b/src/pages/auth/Signin.tsx @@ -146,13 +146,14 @@ export default function SigninDialog() { onBlur: formik.handleBlur, error: formik.touched.email && Boolean(formik.errors.email), helperText: formik.touched.email && formik.errors.email, + "data-cy": "username", }} onChange={formik.handleChange} color="#F2F3F7" id="email" label="Email" gap={upMd ? "10px" : "10px"} - /> + /> Войти diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index d5d42698..79952d4f 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -155,6 +155,7 @@ export default function SignupDialog() { onBlur: formik.handleBlur, error: formik.touched.email && Boolean(formik.errors.email), helperText: formik.touched.email && formik.errors.email, + "data-cy": "username", }} onChange={formik.handleChange} color="#F2F3F7" @@ -170,6 +171,7 @@ export default function SignupDialog() { error: formik.touched.password && Boolean(formik.errors.password), helperText: formik.touched.password && formik.errors.password, autoComplete: "new-password", + "data-cy": "password", }} onChange={formik.handleChange} color="#F2F3F7" @@ -188,6 +190,7 @@ export default function SignupDialog() { helperText: formik.touched.repeatPassword && formik.errors.repeatPassword, autoComplete: "new-password", + "data-cy": "repeat-password", }} onChange={formik.handleChange} color="#F2F3F7" @@ -210,6 +213,7 @@ export default function SignupDialog() { backgroundColor: "black", }, }} + data-cy="signup" > Зарегистрироваться diff --git a/src/pages/createQuize/MyQuizzesFull.tsx b/src/pages/createQuize/MyQuizzesFull.tsx index 466a728f..555737d8 100644 --- a/src/pages/createQuize/MyQuizzesFull.tsx +++ b/src/pages/createQuize/MyQuizzesFull.tsx @@ -19,7 +19,7 @@ import ComplexNavText from "./ComplexNavText"; import FirstQuiz from "./FirstQuiz"; import QuizCard from "./QuizCard"; import { setQuizes, createQuiz } from "@root/quizes/actions"; -import { useQuizArray } from "@root/quizes/hooks"; +import { useQuizStore } from "@root/quizes/store"; interface Props { @@ -40,7 +40,7 @@ export default function MyQuizzesFull({ enqueueSnackbar(`Не удалось получить квизы. ${message}`); }, }); - const quizArray = useQuizArray(); + const quizArray = useQuizStore(state => state.quizes); const navigate = useNavigate(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(500)); @@ -69,6 +69,7 @@ export default function MyQuizzesFull({ minWidth: "44px", }} onClick={() => createQuiz(navigate)} + data-cy="create-quiz" > {isMobile ? "+" : "Создать +"} diff --git a/src/pages/createQuize/QuizCard.tsx b/src/pages/createQuize/QuizCard.tsx index 9abaf348..86844727 100755 --- a/src/pages/createQuize/QuizCard.tsx +++ b/src/pages/createQuize/QuizCard.tsx @@ -33,7 +33,7 @@ export default function QuizCard({ const navigate = useNavigate(); function handleEditClick() { - setEditQuizId(quiz.id); + setEditQuizId(quiz.backendId); navigate("/edit"); } @@ -139,6 +139,7 @@ export default function QuizCard({ ml: "auto", }} onClick={() => deleteQuiz(quiz.id)} + data-cy="delete-quiz" > diff --git a/src/pages/startPage/StartPage.tsx b/src/pages/startPage/StartPage.tsx index 080c36e8..c11ffed1 100755 --- a/src/pages/startPage/StartPage.tsx +++ b/src/pages/startPage/StartPage.tsx @@ -43,8 +43,8 @@ export default function StartPage() { }); const theme = useTheme(); const navigate = useNavigate(); - const quizId = useQuizStore(state => state.editQuizId); - const { quiz } = useCurrentQuiz(); + const editQuizId = useQuizStore(state => state.editQuizId); + const quiz = useCurrentQuiz(); const currentStep = useQuizStore(state => state.currentStep); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(660)); @@ -53,8 +53,8 @@ export default function StartPage() { const quizConfig = quiz?.config; useEffect(() => { - if (quizId === null) navigate("/list"); - }, [navigate, quizId]); + if (editQuizId === null) navigate("/list"); + }, [navigate, editQuizId]); useEffect(() => () => resetEditConfig(), []); @@ -227,6 +227,8 @@ export default function StartPage() { } diff --git a/src/pages/startPage/StartPageSettings.tsx b/src/pages/startPage/StartPageSettings.tsx index a0182a5e..634a3305 100755 --- a/src/pages/startPage/StartPageSettings.tsx +++ b/src/pages/startPage/StartPageSettings.tsx @@ -22,7 +22,7 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { incrementCurrentStep } from "@root/quizes/actions"; +import { incrementCurrentStep, updateQuiz } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; @@ -63,7 +63,7 @@ export default function StartPageSettings() { const theme = useTheme(); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1500)); const isTablet = useMediaQuery(theme.breakpoints.down(950)); - const { quiz, updateQuiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const [formState, setFormState] = useState<"design" | "content">("design"); const designType = quiz?.config?.startpageType; @@ -179,7 +179,7 @@ export default function StartPageSettings() { variant="outlined" value={designType} displayEmpty - onChange={e => updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.startpageType = e.target.value as QuizStartpageType; })} sx={{ @@ -264,7 +264,7 @@ export default function StartPageSettings() { > updateQuiz(quiz => { + onClick={() => updateQuiz(quiz.id, quiz => { quiz.config.startpage.background.type = "image"; })} > @@ -272,7 +272,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onClick={() => updateQuiz(quiz.id, quiz => { quiz.config.startpage.background.type = "video"; })} > @@ -418,7 +418,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + handleChange={e => updateQuiz(quiz.id, quiz => { quiz.config.startpage.background.cycle = e.target.checked; })} /> @@ -452,14 +452,14 @@ export default function StartPageSettings() { }} > updateQuiz(quiz => { + onClick={() => updateQuiz(quiz.id, quiz => { quiz.config.startpage.position = "left"; })} isActive={quiz.config.startpage.position === "left"} Icon={AlignLeftIcon} /> updateQuiz(quiz => { + onClick={() => updateQuiz(quiz.id, quiz => { 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" }} /> updateQuiz(quiz => { + onClick={() => updateQuiz(quiz.id, quiz => { quiz.config.startpage.position = "right"; })} isActive={quiz.config.startpage.position === "right"} @@ -619,7 +619,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.name = e.target.value; })} /> @@ -636,7 +636,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.startpage.description = e.target.value; })} /> @@ -653,7 +653,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.startpage.button = e.target.value; })} /> @@ -670,14 +670,14 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.info.phonenumber = e.target.value; })} /> updateQuiz(quiz => { + handleChange={e => updateQuiz(quiz.id, quiz => { quiz.config.info.clickable = e.target.checked; })} /> @@ -694,7 +694,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.info.orgname = e.target.value; })} /> @@ -711,7 +711,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.info.site = e.target.value; })} /> @@ -728,7 +728,7 @@ export default function StartPageSettings() { updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.info.law = e.target.value; })} /> @@ -815,7 +815,7 @@ export default function StartPageSettings() { borderRadius: 0, padding: 0, }} - onChange={e => updateQuiz(quiz => { + onChange={e => updateQuiz(quiz.id, quiz => { quiz.config.noStartPage = e.target.checked; })} checked={quiz.config.noStartPage} @@ -832,14 +832,7 @@ export default function StartPageSettings() { diff --git a/src/pages/startPage/dropZone.tsx b/src/pages/startPage/dropZone.tsx index 67aa5c38..99db3ea0 100644 --- a/src/pages/startPage/dropZone.tsx +++ b/src/pages/startPage/dropZone.tsx @@ -7,6 +7,7 @@ import { Typography, useTheme, } from "@mui/material"; +import { updateQuiz } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { enqueueSnackbar } from "notistack"; import { useState } from "react"; @@ -22,7 +23,7 @@ interface Props { //Научи функцию принимать данные для валидации export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => { const theme = useTheme(); - const { quiz, updateQuiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const [ready, setReady] = useState(false); 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]; if (file) { if (file.size < 5242880) { - updateQuiz(quiz => { + updateQuiz(quiz.id, quiz => { quiz.config.startpage.background.desktop = URL.createObjectURL(file); }); } else { @@ -54,7 +55,7 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => { const file = event.dataTransfer.files[0]; if (file.size < 5242880) { - updateQuiz(quiz => { + updateQuiz(quiz.id, quiz => { quiz.config.startpage.background.desktop = URL.createObjectURL(file); }); } else { diff --git a/src/pages/startPage/extra.tsx b/src/pages/startPage/extra.tsx index 0ff6fe7c..c49dca17 100644 --- a/src/pages/startPage/extra.tsx +++ b/src/pages/startPage/extra.tsx @@ -1,4 +1,5 @@ import { Box, Link, Typography, useTheme } from "@mui/material"; +import { updateQuiz } from "@root/quizes/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; import CustomTextField from "@ui_kit/CustomTextField"; import { ChangeEvent, useState } from "react"; @@ -6,14 +7,14 @@ import { ChangeEvent, useState } from "react"; export default function Extra() { const theme = useTheme(); const [isExpanded, setIsExpanded] = useState(false); - const { quiz, updateQuiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const expandedHC = (bool: boolean) => { setIsExpanded(bool); }; const mutationOrgMetaHC = (event: ChangeEvent) => { - updateQuiz(quiz => { + updateQuiz(quiz?.id, quiz => { quiz.config.meta = event.target.value; }); }; diff --git a/src/pages/startPage/stepOne.tsx b/src/pages/startPage/stepOne.tsx index 918a3961..51e31614 100755 --- a/src/pages/startPage/stepOne.tsx +++ b/src/pages/startPage/stepOne.tsx @@ -7,7 +7,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks"; export default function StepOne() { - const { quiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const config = quiz?.config; if (!config) return null; // TODO throw and catch with error boundary diff --git a/src/pages/startPage/steptwo.tsx b/src/pages/startPage/steptwo.tsx index 0e155a12..86ff93a4 100755 --- a/src/pages/startPage/steptwo.tsx +++ b/src/pages/startPage/steptwo.tsx @@ -16,7 +16,7 @@ import CardWithImage from "./CardWithImage"; export default function Steptwo() { const theme = useTheme(); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300)); - const { quiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const config = quiz?.config; diff --git a/src/stores/questions.ts b/src/stores/questions.ts index 4b9520e8..0dfdc280 100644 --- a/src/stores/questions.ts +++ b/src/stores/questions.ts @@ -49,10 +49,10 @@ export const questionStore = create()( isFirstPartialize = false; Object.keys(state.listQuestions).forEach((quizId) => { - [...state.listQuestions[quizId]].forEach(({ id, deleted }) => { + [...state.listQuestions[quizId]].forEach(({ backendId: id, deleted }) => { if (deleted) { const removedItemIndex = state.listQuestions[quizId].findIndex( - (item) => item.id === id + (item) => item.backendId === id ); state.listQuestions[quizId].splice(removedItemIndex, 1); @@ -319,7 +319,7 @@ export const createQuestion = ( newData[quizId].splice( placeIndex < 0 ? newData[quizId].length : placeIndex, 0, - { ...JSON.parse(JSON.stringify(defaultObject)), id } + { ...JSON.parse(JSON.stringify(defaultObject)), backendId: id } ); questionStore.setState({ listQuestions: newData }); @@ -333,7 +333,7 @@ export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => { const copiedQuiz = { ...listQuestions[quizId][copiedQuestionIndex] }; listQuestions[quizId].splice(copiedQuestionIndex, 0, { ...copiedQuiz, - id: getRandom(), + backendId: getRandom(), }); questionStore.setState({ listQuestions }); @@ -343,7 +343,7 @@ export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => { export const removeQuestionForce = (quizId: number, removedId: number) => { const questionListClone = { ...questionStore.getState()["listQuestions"] }; const removedItemIndex = questionListClone[quizId].findIndex( - ({ id }) => id === removedId + ({ backendId: id }) => id === removedId ); questionListClone[quizId].splice(removedItemIndex, 1); questionStore.setState({ listQuestions: questionListClone }); @@ -362,7 +362,7 @@ export const findQuestionById = (quizId: number) => { questionStore .getState() ["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => { - if (quiz.id === quizId) { + if (quiz.backendId === quizId) { found = { quiz, index }; return true; } diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 98bebd65..da912edf 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -1,14 +1,15 @@ import { questionApi } from "@api/question"; 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 { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; -import { notReachable } from "../../utils/notReachable"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; -import { QuestionsStore, useQuestionsStore } from "./store"; +import { notReachable } from "../../utils/notReachable"; import { RequestQueue } from "../../utils/requestQueue"; +import { QuestionsStore, useQuestionsStore } from "./store"; +import { nanoid } from "nanoid"; export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { @@ -18,14 +19,6 @@ export const setQuestions = (questions: RawQuestion[] | null) => setProducedStat 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 => { state.questions.push(question); }, { @@ -33,28 +26,25 @@ const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => { question, }); -const removeQuestion = (questionId: number) => setProducedState(state => { +const removeQuestion = (questionId: string) => setProducedState(state => { const index = state.questions.findIndex(q => q.id === questionId); + if (index === -1) return; + state.questions.splice(index, 1); }, { type: "removeQuestion", questionId, }); -const setQuestionField = ( - questionId: number, - field: T, - value: AnyQuizQuestion[T], -) => setProducedState(state => { +const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => { const question = state.questions.find(q => q.id === questionId); if (!question) return; - question[field] = value; + question.backendId = backendId; }, { - type: "setQuestionField", - questionId, - field, - value, + type: "setQuestionBackendId", + questionId: questionId, + backendId, }); 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); if (!question) return; @@ -80,15 +70,8 @@ export const collapseAllQuestions = () => setProducedState(state => { state.questions.forEach(question => question.expanded = false); }); -export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => { - const question = state.questions.find(q => q.id === questionId); - if (!question) return; - - question.openedModalSettings = !question.openedModalSettings; -}); - -export const addQuestionVariant = (questionId: number) => { - updateQuestionWithFnOptimistic(questionId, question => { +export const addQuestionVariant = (questionId: string) => { + updateQuestion(questionId, question => { switch (question.type) { case "variant": case "emoji": @@ -111,8 +94,8 @@ export const addQuestionVariant = (questionId: number) => { }); }; -export const deleteQuestionVariant = (questionId: number, variantId: string) => { - updateQuestionWithFnOptimistic(questionId, question => { +export const deleteQuestionVariant = (questionId: string, variantId: string) => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId); @@ -123,12 +106,12 @@ export const deleteQuestionVariant = (questionId: number, variantId: string) => }; export const setQuestionVariantField = ( - questionId: number, + questionId: string, variantId: string, field: keyof QuestionVariant, value: QuestionVariant[keyof QuestionVariant], ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId); @@ -140,12 +123,12 @@ export const setQuestionVariantField = ( }; export const setQuestionImageVariantField = ( - questionId: number, + questionId: string, variantId: string, field: keyof ImageQuestionVariant, value: ImageQuestionVariant[keyof ImageQuestionVariant], ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId); @@ -159,13 +142,13 @@ export const setQuestionImageVariantField = ( }; export const reorderQuestionVariants = ( - questionId: number, + questionId: string, sourceIndex: number, destinationIndex: number, ) => { if (sourceIndex === destinationIndex) return; - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const [removed] = question.content.variants.splice(sourceIndex, 1); @@ -175,10 +158,10 @@ export const reorderQuestionVariants = ( }; export const setQuestionBackgroundImage = ( - questionId: number, + questionId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (question.content.back === url) return; if ( @@ -189,10 +172,10 @@ export const setQuestionBackgroundImage = ( }; export const setQuestionOriginalBackgroundImage = ( - questionId: number, + questionId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (question.content.originalBack === url) return; URL.revokeObjectURL(question.content.originalBack); @@ -201,11 +184,11 @@ export const setQuestionOriginalBackgroundImage = ( }; export const setVariantImageUrl = ( - questionId: number, + questionId: string, variantId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const variant = question.content.variants.find(variant => variant.id === variantId); @@ -219,11 +202,11 @@ export const setVariantImageUrl = ( }; export const setVariantOriginalImageUrl = ( - questionId: number, + questionId: string, variantId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (!("variants" in question.content)) return; const variant = question.content.variants.find( @@ -239,10 +222,10 @@ export const setVariantOriginalImageUrl = ( }; export const setPageQuestionPicture = ( - questionId: number, + questionId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (question.type !== "page") return; if (question.content.picture === url) return; @@ -255,10 +238,10 @@ export const setPageQuestionPicture = ( }; export const setPageQuestionOriginalPicture = ( - questionId: number, + questionId: string, url: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { if (question.type !== "page") return; if (question.content.originalPicture === url) return; @@ -269,47 +252,52 @@ export const setPageQuestionOriginalPicture = ( }; export const setQuestionInnerName = ( - questionId: number, + questionId: string, name: string, ) => { - updateQuestionWithFnOptimistic(questionId, question => { + updateQuestion(questionId, question => { question.content.innerName = name; }); }; -const REQUEST_DEBOUNCE = 1000; -const requestQueue = new RequestQueue(); +const REQUEST_DEBOUNCE = 200; +const requestQueue = new RequestQueue(); let requestTimeoutId: ReturnType; -export const updateQuestionWithFnOptimistic = async ( - questionId: number, +export const updateQuestion = ( + questionId: string, updateFn: (question: AnyQuizQuestion) => void, ) => { - const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); - if (!question) return; + setProducedState(state => { + const question = state.questions.find(q => q.id === questionId); + if (!question) return; - const updatedQuestion = produce(question, updateFn); - setQuestion(updatedQuestion); + updateFn(question); + }, { + type: "updateQuestion", + questionId, + updateFn: updateFn.toString(), + }); clearTimeout(requestTimeoutId); - requestTimeoutId = setTimeout(async () => { - requestQueue.enqueue(async (prevResponse) => { - const questionId = prevResponse?.updated ?? updatedQuestion.id; - const response = await questionApi.edit(questionToEditQuestionRequest(updatedQuestion, questionId)); + requestTimeoutId = setTimeout(() => { + requestQueue.enqueue(async () => { + const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); + if (!question) return; - setQuestionField(questionId, "id", response.updated); + const response = await questionApi.edit(questionToEditQuestionRequest(question)); - return response; + setQuestionBackendId(questionId, response.updated); }).catch(error => { if (isAxiosCanceledError(error)) return; - devlog("Error editing question", { error, question, updatedQuestion }); + devlog("Error editing question", { error, questionId }); enqueueSnackbar("Не удалось сохранить вопрос"); }); }, REQUEST_DEBOUNCE); }; -export const createQuestion = async (quizId: number, type: QuestionType = "variant") => { +export const createQuestion = async (quizId: number, type: QuestionType = "variant") => requestQueue.enqueue(async () => { try { const question = await questionApi.create({ quiz_id: quizId, @@ -321,41 +309,45 @@ export const createQuestion = async (quizId: number, type: QuestionType = "varia devlog("Error creating question", error); 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 { - await questionApi.delete(questionId); + await questionApi.delete(question.backendId); removeQuestion(questionId); } catch (error) { devlog("Error deleting question", error); 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 { - 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 => { - 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); }, { type: "copyQuestion", - questionId, + questionId: questionId, quizId, }); } catch (error) { devlog("Error copying question", error); enqueueSnackbar("Не удалось скопировать вопрос"); } -}; +}); function setProducedState( recipe: (state: QuestionsStore) => void, diff --git a/src/stores/quizes/actions.ts b/src/stores/quizes/actions.ts index 7eac11fa..c8dee4cc 100644 --- a/src/stores/quizes/actions.ts +++ b/src/stores/quizes/actions.ts @@ -1,8 +1,8 @@ import { quizApi } from "@api/quiz"; 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 { QuizConfig, QuizSetupStep, maxQuizSetupSteps } from "@model/quizSettings"; +import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings"; import { createQuestion } from "@root/questions/actions"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; @@ -21,156 +21,148 @@ export const setEditQuizId = (quizId: number | null) => setProducedState(state = export const resetEditConfig = () => setProducedState(state => { state.editQuizId = null; - state.currentStep = 1; + state.currentStep = 0; }); export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => { - state.quizById = {}; - if (quizes === null) return; - - quizes.forEach(quiz => state.quizById[quiz.id] = rawQuizToQuiz(quiz)); + state.quizes = quizes?.map(rawQuizToQuiz) ?? []; }, { type: "setQuizes", quizes, }); -export const setQuiz = (quiz: Quiz) => setProducedState(state => { - state.quizById[quiz.id] = quiz; +const addQuiz = (quiz: Quiz) => setProducedState(state => { + state.quizes.push(quiz); }, { - type: "setQuiz", + type: "addQuiz", quiz, }); -export const removeQuiz = (quizId: number) => setProducedState(state => { - delete state.quizById[quizId]; +const removeQuiz = (quizId: string) => setProducedState(state => { + const index = state.quizes.findIndex(q => q.id === quizId); + if (index === -1) return; + + state.quizes.splice(index, 1); }, { type: "removeQuiz", quizId, }); -export const setQuizField = ( - quizId: number, - field: T, - value: Quiz[T], -) => setProducedState(state => { - const quiz = state.quizById[quizId]; +const setQuizBackendId = (quizId: string, backendId: number) => setProducedState(state => { + const quiz = state.quizes.find(q => q.id === quizId); if (!quiz) return; - const oldId = quiz.id; - quiz[field] = value; - - if (field === "id") { - delete state.quizById[oldId]; - state.quizById[value as number] = quiz; - } + quiz.backendId = backendId; }, { - type: "setQuizField", + type: "setQuizBackendId", quizId, - field, - value, + backendId, }); export const incrementCurrentStep = () => setProducedState(state => { - state.currentStep = Math.min( - maxQuizSetupSteps, state.currentStep + 1 - ) as QuizSetupStep; + state.currentStep = Math.min(maxQuizSetupSteps - 1, state.currentStep + 1); }, { type: "incrementCurrentStep", }); export const decrementCurrentStep = () => setProducedState(state => { - state.currentStep = Math.max( - 1, state.currentStep - 1 - ) as QuizSetupStep; + state.currentStep = Math.max(0, state.currentStep - 1); }, { type: "decrementCurrentStep", }); export const setCurrentStep = (step: number) => setProducedState(state => { - state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps, step)) as QuizSetupStep; + state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step)); }); export const setQuizType = ( - quizId: number, + quizId: string, quizType: QuizConfig["type"], ) => { - updateQuizWithFnOptimistic( + updateQuiz( quizId, quiz => { quiz.config.type = quizType; }, ); - incrementCurrentStep(); }; export const setQuizStartpageType = ( - quizId: number | null, + quizId: string, startpageType: QuizConfig["startpageType"], ) => { - updateQuizWithFnOptimistic( + updateQuiz( quizId, quiz => { quiz.config.startpageType = startpageType; }, ); - incrementCurrentStep(); }; -const REQUEST_DEBOUNCE = 1000; -const requestQueue = new RequestQueue(); +const REQUEST_DEBOUNCE = 200; +const requestQueue = new RequestQueue(); let requestTimeoutId: ReturnType; -export const updateQuizWithFnOptimistic = async ( - quizId: number | null, +export const updateQuiz = async ( + quizId: string | null | undefined, updateFn: (quiz: Quiz) => void, ) => { if (!quizId) return; - const quiz = useQuizStore.getState().quizById[quizId]; - if (!quiz) return; + setProducedState(state => { + const quiz = state.quizes.find(q => q.id === quizId); + if (!quiz) return; - const updatedQuiz = produce(quiz, updateFn); - setQuiz(updatedQuiz); + updateFn(quiz); + }, { + type: "updateQuiz", + quizId, + updateFn: updateFn.toString(), + }); clearTimeout(requestTimeoutId); requestTimeoutId = setTimeout(async () => { - requestQueue.enqueue(async (prevResponse) => { - const quizId = prevResponse?.updated ?? updatedQuiz.id; - const response = await quizApi.edit(quizToEditQuizRequest(updatedQuiz, quizId)); + requestQueue.enqueue(async () => { + const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId); + if (!quiz) return; - setQuizField(quizId, "id", response.updated); + const response = await quizApi.edit(quizToEditQuizRequest(quiz)); + + setQuizBackendId(quizId, response.updated); setEditQuizId(response.updated); - - return response; }).catch(error => { if (isAxiosCanceledError(error)) return; - devlog("Error editing quiz", { error, quiz, updatedQuiz }); + devlog("Error editing quiz", error, quizId); enqueueSnackbar("Не удалось сохранить настройки квиза"); }); }, REQUEST_DEBOUNCE); }; -export const createQuiz = async (navigate: NavigateFunction) => { +export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enqueue(async () => { try { - const quiz = await quizApi.create(); + const rawQuiz = await quizApi.create(); + const quiz = rawQuizToQuiz(rawQuiz); - setQuiz(rawQuizToQuiz(quiz)); - setEditQuizId(quiz.id); + addQuiz(quiz); + setEditQuizId(quiz.backendId); navigate("/edit"); - await createQuestion(quiz.id); + await createQuestion(rawQuiz.id); } catch (error) { devlog("Error creating quiz", error); const message = getMessageFromFetchError(error) ?? ""; enqueueSnackbar(`Не удалось создать квиз. ${message}`); } -}; +}); + +export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async () => { + const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId); + if (!quiz) return; -export const deleteQuiz = async (quizId: number) => { try { - await quizApi.delete(quizId); + await quizApi.delete(quiz.backendId); removeQuiz(quizId); } catch (error) { @@ -179,7 +171,9 @@ export const deleteQuiz = async (quizId: number) => { const message = getMessageFromFetchError(error) ?? ""; enqueueSnackbar(`Не удалось удалить квиз. ${message}`); } -}; +}); + +// TODO copy quiz function setProducedState( recipe: (state: QuizStore) => void, diff --git a/src/stores/quizes/hooks.ts b/src/stores/quizes/hooks.ts index 4a782de6..e357e20f 100644 --- a/src/stores/quizes/hooks.ts +++ b/src/stores/quizes/hooks.ts @@ -1,22 +1,11 @@ -import { Quiz } from "@model/quiz/quiz"; -import { useCallback } from "react"; -import { updateQuizWithFnOptimistic } from "./actions"; 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() { 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) => { - updateQuizWithFnOptimistic(quizId, updateFn); - }, [quizId]); + const quiz = quizes.find(q => q.backendId === quizId); - return { quiz, updateQuiz }; + return quiz; } diff --git a/src/stores/quizes/store.ts b/src/stores/quizes/store.ts index 6b71d873..1656acd9 100644 --- a/src/stores/quizes/store.ts +++ b/src/stores/quizes/store.ts @@ -1,19 +1,18 @@ import { Quiz } from "@model/quiz/quiz"; -import { QuizSetupStep } from "@model/quizSettings"; import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; export type QuizStore = { - quizById: Record; + quizes: Quiz[]; editQuizId: number | null; - currentStep: QuizSetupStep; + currentStep: number; }; const initialState: QuizStore = { - quizById: {}, + quizes: [], editQuizId: null, - currentStep: 1, + currentStep: 0, }; export const useQuizStore = create()( @@ -31,6 +30,5 @@ export const useQuizStore = create()( editQuizId: state.editQuizId, currentStep: state.currentStep, }), - } - ) + }) ); diff --git a/src/ui_kit/Sidebar.tsx b/src/ui_kit/Sidebar.tsx index bc16ea7b..b5111c5c 100755 --- a/src/ui_kit/Sidebar.tsx +++ b/src/ui_kit/Sidebar.tsx @@ -1,14 +1,7 @@ -import ChartPieIcon from "@icons/ChartPieIcon"; import CollapseMenuIcon from "@icons/CollapseMenuIcon"; -import ContactBookIcon from "@icons/ContactBookIcon"; -import FlowArrowIcon from "@icons/FlowArrowIcon"; import GearIcon from "@icons/GearIcon"; -import LayoutIcon from "@icons/LayoutIcon"; -import MegaphoneIcon from "@icons/MegaphoneIcon"; import PencilCircleIcon from "@icons/PencilCircleIcon"; import PuzzlePieceIcon from "@icons/PuzzlePieceIcon"; -import QuestionIcon from "@icons/QuestionIcon"; -import QuestionsMapIcon from "@icons/QuestionsMapIcon"; import TagIcon from "@icons/TagIcon"; import { quizSetupSteps } from "@model/quizSettings"; import { @@ -23,15 +16,6 @@ import { useQuizStore } from "@root/quizes/store"; import { useState } from "react"; import MenuItem from "./MenuItem"; -const createQuizMenuItems = [ - [LayoutIcon, "Стартовая страница"], - [QuestionIcon, "Вопросы"], - [ChartPieIcon, "Результаты"], - [QuestionsMapIcon, "Карта вопросов"], - [ContactBookIcon, "Форма контактов"], - [FlowArrowIcon, "Установка квиза"], - [MegaphoneIcon, "Запуск рекламы"], -] as const; const quizSettingsMenuItems = [ [TagIcon, "Дополнения"], @@ -43,7 +27,6 @@ const quizSettingsMenuItems = [ export default function Sidebar() { const theme = useTheme(); const [isMenuCollapsed, setIsMenuCollapsed] = useState(false); - const [progress, setProgress] = useState(1 / 7); const currentStep = useQuizStore(state => state.currentStep); const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev); @@ -100,19 +83,20 @@ export default function Sidebar() { - {createQuizMenuItems.map((menuItem, index) => { - const Icon = menuItem[0]; + {quizSetupSteps.map((menuItem, index) => { + const Icon = menuItem.sidebarIcon; + return ( setCurrentStep(index + 1)} - key={menuItem[1]} - text={menuItem[1]} + onClick={() => setCurrentStep(index)} + key={index} + text={menuItem.sidebarText} isCollapsed={isMenuCollapsed} - isActive={quizSetupSteps[currentStep].displayStep === index + 1} + isActive={currentStep === index} icon={ )} - {/* // TODO + {quizSettingsMenuItems.map((menuItem, index) => { const Icon = menuItem[0]; - const totalIndex = index + createQuizMenuItems.length; - const isActive = listQuizes[quizId].step === totalIndex + 1; + const totalIndex = index + quizSetupSteps.length; + const isActive = currentStep === totalIndex + 1; + return ( updateQuizesList(quizId, { step: totalIndex + 1 })} + onClick={() => null} key={menuItem[1]} text={menuItem[1]} isActive={isActive} @@ -169,7 +154,7 @@ export default function Sidebar() { /> ); })} - */} + ); } diff --git a/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx b/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx index b11ee04d..7b7ddb20 100644 --- a/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx +++ b/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx @@ -11,7 +11,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks"; export default function QuizPreviewLayout() { const theme = useTheme(); - const { quiz } = useCurrentQuiz(); + const quiz = useCurrentQuiz(); const isTablet = useMediaQuery(theme.breakpoints.down(630)); if (!quiz) return null; diff --git a/src/ui_kit/Stepper.tsx b/src/ui_kit/Stepper.tsx index 00b3e4d2..152d741b 100755 --- a/src/ui_kit/Stepper.tsx +++ b/src/ui_kit/Stepper.tsx @@ -1,9 +1,9 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import MobileStepper from "@mui/material/MobileStepper"; -import { QuizSetupStep, maxDisplayQuizSetupSteps, quizSetupSteps } from "@model/quizSettings"; +import { maxQuizSetupSteps, quizSetupSteps } from "@model/quizSettings"; interface Props { - activeStep: QuizSetupStep; + activeStep: number; } export default function ProgressMobileStepper({ @@ -28,9 +28,9 @@ export default function ProgressMobileStepper({ > {" "} - Шаг {quizSetupSteps[activeStep].displayStep} из {maxDisplayQuizSetupSteps} + Шаг {activeStep + 1 } из {maxQuizSetupSteps} - {quizSetupSteps[activeStep].text} + {quizSetupSteps[activeStep].stepperText} ); diff --git a/src/ui_kit/switchStepPages.tsx b/src/ui_kit/switchStepPages.tsx index f8415a24..87662b25 100755 --- a/src/ui_kit/switchStepPages.tsx +++ b/src/ui_kit/switchStepPages.tsx @@ -1,5 +1,4 @@ -import { QuizSetupStep } from "@model/quizSettings"; -import { notReachable } from "../utils/notReachable"; +import { QuizResultsType, QuizStartpageType, QuizType } from "@model/quizSettings"; import ContactFormPage from "../pages/ContactFormPage/ContactFormPage"; import InstallQuiz from "../pages/InstallQuiz/InstallQuiz"; import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage"; @@ -13,25 +12,33 @@ import Steptwo from "../pages/startPage/steptwo"; interface Props { - activeStep: QuizSetupStep; - quizType: string; + activeStep: number; + quizType: QuizType; + quizStartPageType: QuizStartpageType; + quizResults: QuizResultsType; } export default function SwitchStepPages({ activeStep = 1, quizType, + quizStartPageType, + quizResults, }: Props) { switch (activeStep) { - case 1: return ; - case 2: return ; - case 3: return ; - case 4: return quizType === "form" ? : ; - case 5: return ; - case 6: return ; - case 7: return ; - case 8: return ; - case 9: return ; - case 10: return <>Реклама; - default: return notReachable(activeStep); + case 0: { + if (!quizType) return ; + if (!quizStartPageType) return ; + return ; + } + case 1: return quizType === "form" ? : ; + case 2: { + if (!quizResults) return ; + return ; + } + case 3: return ; + case 4: return ; + case 5: return ; + case 6: return <>Реклама; + default: throw new Error(`Invalid quiz setup step: ${activeStep}`); } -} +} diff --git a/src/utils/requestQueue.ts b/src/utils/requestQueue.ts index 9eb9a251..2c9fd143 100644 --- a/src/utils/requestQueue.ts +++ b/src/utils/requestQueue.ts @@ -1,12 +1,12 @@ -export class RequestQueue { +export class RequestQueue { private pendingPromise = false; private items: Array<{ - action: (prevPayload?: T | null) => Promise; + action: () => Promise; resolve: (value: T) => void; reject: (reason?: any) => void; }> = []; - enqueue(action: (prevPayload?: T | null) => Promise) { + enqueue(action: () => Promise) { return new Promise((resolve, reject) => { if (this.items.length === 2) { this.items[1] = { action, resolve, reject }; @@ -17,17 +17,15 @@ export class RequestQueue { }); } - async dequeue(prevPayload?: T | null) { + async dequeue() { if (this.pendingPromise) return; const item = this.items.shift(); if (!item) return; - let payload: T | null = null; - try { this.pendingPromise = true; - payload = await item.action(prevPayload); + const payload = await item.action(); this.pendingPromise = false; item.resolve(payload); @@ -35,7 +33,7 @@ export class RequestQueue { this.pendingPromise = false; item.reject(e); } finally { - this.dequeue(payload); + this.dequeue(); } } }