diff --git a/src/model/question/create.ts b/src/model/question/create.ts index d7beef2a..ad8e44b3 100644 --- a/src/model/question/create.ts +++ b/src/model/question/create.ts @@ -1,4 +1,4 @@ -import { DefiniteQuestionType } from "@model/questionTypes/shared"; +import { QuestionType } from "./question"; export interface CreateQuestionRequest { @@ -9,7 +9,7 @@ export interface CreateQuestionRequest { /** description of question. html/text */ description?: string; /** type of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */ - type?: DefiniteQuestionType; + type?: QuestionType; /** set true if user MUST answer this question */ required?: boolean; /** page of question */ diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index d0dacc6b..1830a40d 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -86,6 +86,13 @@ export type AnyQuizQuestion = | QuizQuestionRating; // | QuizQuestionInitial; +type FilterQuestionsWithVariants = T extends { + content: { variants: QuestionVariant[] | ImageQuestionVariant[]; }; +} ? T : never; + +export type QuizQuestionsWithVariants = FilterQuestionsWithVariants; + + export const createQuestionVariant: () => QuestionVariant = () => ({ id: nanoid(), answer: "", diff --git a/src/pages/Questions/AnswerDraggableList/index.tsx b/src/pages/Questions/AnswerDraggableList/index.tsx index 943a1f4a..a59c84af 100644 --- a/src/pages/Questions/AnswerDraggableList/index.tsx +++ b/src/pages/Questions/AnswerDraggableList/index.tsx @@ -1,21 +1,19 @@ import { Box } from "@mui/material"; -import { DragDropContext, Droppable } from "react-beautiful-dnd"; -import { AnswerItem } from "./AnswerItem"; +import { reorderQuestionVariants } from "@root/questions/actions"; import { type ReactNode } from "react"; import type { DropResult } from "react-beautiful-dnd"; -import type { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared"; -import { reorderQuestionVariants } from "@root/questions/actions"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; +import type { ImageQuestionVariant, QuestionVariant, QuizQuestionsWithVariants } from "../../../model/questionTypes/shared"; +import { AnswerItem } from "./AnswerItem"; type AnswerDraggableListProps = { - variants: QuestionVariant[]; - question: AnyQuizQuestion; + question: QuizQuestionsWithVariants; additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; }; export const AnswerDraggableList = ({ - variants, question, additionalContent, additionalMobile, @@ -31,7 +29,7 @@ export const AnswerDraggableList = ({ {(provided) => ( - {variants.map((variant, index) => ( + {question.content.variants.map((variant, index) => ( void; - totalIndex: number; - sx?: SxProps; + switchState: string; + SSHC: (data: string) => void; + question: AnyQuizQuestion; + sx?: SxProps; } export default function ButtonsOptions({ - SSHC, - switchState, - totalIndex, + SSHC, + switchState, + question, }: Props) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const { listQuizes } = quizStore(); - const [openedReallyChangingModal, setOpenedReallyChangingModal] = - useState(false); - const quize = listQuizes[quizId]; - const question = listQuestions[quizId][totalIndex] as QuizQuestionBase; + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); - useEffect(() => { - if (question.deleteTimeoutId) { - clearTimeout(question.deleteTimeoutId); - } - }, [listQuestions]); + const openedModal = () => { + updateQuestionWithFnOptimistic(question.id, question => { + question.openedModalSettings = true; + }); + }; - const openedModal = () => { - updateQuestionsList(quizId, totalIndex, { - openedModalSettings: true, - }); - }; + const buttonSetting: { + icon: JSX.Element; + title: string; + value: string; + myFunc?: any; + }[] = [ + { + icon: ( + + ), + title: "Настройки", + value: "setting", + }, + { + icon: ( + + ), + title: "Подсказка", + value: "help", + }, + { + icon: ( + + ), + title: "Ветвление", + value: "branching", + myFunc: openedModal, + }, + ]; - const theme = useTheme(); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); - - const buttonSetting: { - icon: JSX.Element; - title: string; - value: string; - myFunc?: any; - }[] = [ - { - icon: ( - - ), - title: "Настройки", - value: "setting", - }, - { - icon: ( - - ), - title: "Подсказка", - value: "help", - }, - { - icon: ( - - ), - title: "Ветвление", - value: "branching", - myFunc: openedModal, - }, - ]; - - return ( - - - {buttonSetting.map(({ icon, title, value, myFunc }) => ( - - {value === "branching" ? ( - + - + {buttonSetting.map(({ icon, title, value, myFunc }) => ( + + {value === "branching" ? ( + + + Будет показан при условии + + + Название + + + Условие 1, Условие 2 + + + Все условия обязательны + + + } + > + { + SSHC(value); + myFunc(); + }} + sx={{ + backgroundColor: + switchState === value + ? theme.palette.brightPurple.main + : "transparent", + color: + switchState === value + ? "#ffffff" + : theme.palette.grey3.main, + minWidth: isWrappMiniButtonSetting ? "30px" : "64px", + height: "30px", + "&:hover": { + color: theme.palette.grey3.main, + "& path": { stroke: theme.palette.grey3.main }, + }, + }} + > + {icon} + {isWrappMiniButtonSetting ? null : title} + + + ) : ( + <> + { + SSHC(value); + myFunc(); + }} + sx={{ + backgroundColor: + switchState === value + ? theme.palette.brightPurple.main + : "transparent", + color: + switchState === value + ? "#ffffff" + : theme.palette.grey3.main, + minWidth: isWrappMiniButtonSetting ? "30px" : "64px", + height: "30px", + "&:hover": { + color: theme.palette.grey3.main, + "& path": { stroke: theme.palette.grey3.main }, + }, + }} + > + {icon} + {isWrappMiniButtonSetting ? null : title} + + + )} + + ))} + <> + - Будет показан при условии - - - Название - - + + - Условие 1, Условие 2 - - - Все условия обязательны - - - } - > - { - SSHC(value); - myFunc(); - }} - sx={{ - backgroundColor: - switchState === value - ? theme.palette.brightPurple.main - : "transparent", - color: - switchState === value - ? "#ffffff" - : theme.palette.grey3.main, - minWidth: isWrappMiniButtonSetting ? "30px" : "64px", - height: "30px", - "&:hover": { - color: theme.palette.grey3.main, - "& path": { stroke: theme.palette.grey3.main }, - }, - }} + + + + + + + + + + + + copyQuestion(question.id, question.quizId)} > - {icon} - {isWrappMiniButtonSetting ? null : title} - - - ) : ( - <> - { - SSHC(value); - myFunc(); - }} - sx={{ - backgroundColor: - switchState === value - ? theme.palette.brightPurple.main - : "transparent", - color: - switchState === value - ? "#ffffff" - : theme.palette.grey3.main, - minWidth: isWrappMiniButtonSetting ? "30px" : "64px", - height: "30px", - "&:hover": { - color: theme.palette.grey3.main, - "& path": { stroke: theme.palette.grey3.main }, - }, - }} + + + { // TODO + // const removedId = question.id; + // if (question.deleteTimeoutId) { + // clearTimeout(question.deleteTimeoutId); + // } + + // removeQuestion(quizId, totalIndex); + + // const newTimeoutId = window.setTimeout(() => { + // removeQuestionForce(quizId, removedId); + // }, 5000); + + // updateQuestionsList(quizId, totalIndex, { + // ...question, + // deleteTimeoutId: newTimeoutId, + // }); + + deleteQuestion(question.id); + }} > - {icon} - {isWrappMiniButtonSetting ? null : title} - - - )} - - ))} - <> - setOpenedReallyChangingModal(true)} - sx={{ - minWidth: "30px", - height: "30px", - backgroundColor: "#FEDFD0", - }} - > - - - setOpenedReallyChangingModal(true)} - sx={{ - minWidth: "30px", - height: "30px", - backgroundColor: "#FEDFD0", - }} - > - - - setOpenedReallyChangingModal(true)} - sx={{ - minWidth: "30px", - height: "30px", - backgroundColor: "#FEDFD0", - }} - > - - - - - - - - - copyQuestion(quizId, totalIndex)} - > - - - { - const removedId = question.id; - if (question.deleteTimeoutId) { - clearTimeout(question.deleteTimeoutId); - } - - removeQuestion(quizId, totalIndex); - - const newTimeoutId = window.setTimeout(() => { - removeQuestionForce(quizId, removedId); - }, 5000); - - updateQuestionsList(quizId, totalIndex, { - ...question, - deleteTimeoutId: newTimeoutId, - }); - }} - > - - - - - ); + + + + + ); } diff --git a/src/pages/Questions/ButtonsOptionsAndPict.tsx b/src/pages/Questions/ButtonsOptionsAndPict.tsx index 41a5344a..1101399b 100644 --- a/src/pages/Questions/ButtonsOptionsAndPict.tsx +++ b/src/pages/Questions/ButtonsOptionsAndPict.tsx @@ -1,6 +1,7 @@ import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight"; import { DoubleTick } from "@icons/questionsPage/DoubleTick"; import { VectorQuestions } from "@icons/questionsPage/VectorQuestions"; +import { QuizQuestionVarImg } from "@model/questionTypes/varimg"; import { Box, IconButton, @@ -20,13 +21,13 @@ import { DeleteIcon } from "../../assets/icons/questionsPage/deleteIcon"; import { HideIcon } from "../../assets/icons/questionsPage/hideIcon"; import ImgIcon from "../../assets/icons/questionsPage/imgIcon"; import SettingIcon from "../../assets/icons/questionsPage/settingIcon"; -import type { AnyQuizQuestion } from "../../model/questionTypes/shared"; +import { QuizQuestionVariant } from "@model/questionTypes/variant"; interface Props { switchState: string; SSHC: (data: string) => void; - question: AnyQuizQuestion; + question: QuizQuestionVariant | QuizQuestionVarImg; } export default function ButtonsOptionsAndPict({ diff --git a/src/pages/Questions/DataOptions/DataOptions.tsx b/src/pages/Questions/DataOptions/DataOptions.tsx index 2830d34d..bc42a75c 100644 --- a/src/pages/Questions/DataOptions/DataOptions.tsx +++ b/src/pages/Questions/DataOptions/DataOptions.tsx @@ -3,13 +3,14 @@ import { useState } from "react"; import InfoIcon from "../../../assets/icons/InfoIcon"; import ButtonsOptions from "../ButtonsOptions"; import SwitchData from "./switchData"; +import { QuizQuestionDate } from "@model/questionTypes/date"; interface Props { - totalIndex: number; + question: QuizQuestionDate; } -export default function DataOptions({ totalIndex }: Props) { +export default function DataOptions({ question }: Props) { const [switchState, setSwitchState] = useState("setting"); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(790)); @@ -49,8 +50,8 @@ export default function DataOptions({ totalIndex }: Props) { - - + + ); } diff --git a/src/pages/Questions/DataOptions/settingData.tsx b/src/pages/Questions/DataOptions/settingData.tsx index a6fab243..f62d9369 100644 --- a/src/pages/Questions/DataOptions/settingData.tsx +++ b/src/pages/Questions/DataOptions/settingData.tsx @@ -1,139 +1,134 @@ -import { useParams } from "react-router-dom"; -import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; +import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; +import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; -import { questionStore, updateQuestionsList } from "@root/questions"; - import type { QuizQuestionDate } from "../../../model/questionTypes/date"; + type SettingsDataProps = { - totalIndex: number; + question: QuizQuestionDate; }; -export default function SettingsData({ totalIndex }: SettingsDataProps) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionDate; - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); +export default function SettingsData({ question }: SettingsDataProps) { + const theme = useTheme(); + const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); + const setInnerName = useDebouncedCallback((value) => { + setQuestionInnerName(question.id, value); + }, 1000); - return ( - - - - Настройки календаря - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, dateRange: target.checked }, - }); - }} - /> - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, time: target.checked }, - }); - }} - /> - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - required: !target.checked, - }); - }} - /> + return ( - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); - }} - /> - - - + > + + + Настройки календаря + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "date") return; + + question.content.dateRange = target.checked; + }); + }} + /> + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "date") return; + + question.content.time = target.checked; + }); + }} + /> + + + + Настройки вопросов + + { + updateQuestionWithFnOptimistic(question.id, question => { + question.required = !target.checked; + }); + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + }); + }} + /> + + + + + + + {question.content.innerNameCheck && ( + setInnerName(target.value)} + /> + )} - - {question.content.innerNameCheck && ( - debounced(target.value)} - /> - )} - - - ); + ); } diff --git a/src/pages/Questions/DataOptions/switchData.tsx b/src/pages/Questions/DataOptions/switchData.tsx index 507a92b2..abbbb833 100644 --- a/src/pages/Questions/DataOptions/switchData.tsx +++ b/src/pages/Questions/DataOptions/switchData.tsx @@ -1,27 +1,25 @@ -import * as React from "react"; +import { QuizQuestionDate } from "@model/questionTypes/date"; +import BranchingQuestions from "../branchingQuestions"; import HelpQuestions from "../helpQuestions"; import SettingData from "./settingData"; -import BranchingQuestions from "../branchingQuestions"; + interface Props { switchState: string; - totalIndex: number; + question: QuizQuestionDate; } export default function SwitchData({ switchState = "setting", - totalIndex, + question, }: Props) { switch (switchState) { case "setting": - return ; - break; + return ; case "help": - return ; - break; + return ; case "branching": - return ; - break; + return ; default: return <>; } diff --git a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx index 478e72da..d0115f72 100644 --- a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx +++ b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx @@ -1,31 +1,22 @@ -import { useState } from "react"; import { Box, - Typography, - Popper, - Grow, - Paper, - MenuList, - MenuItem, - ClickAwayListener, - Modal, Button, + ClickAwayListener, + Grow, + MenuItem, + MenuList, + Modal, + Paper, + Popper, + Typography, useTheme, } from "@mui/material"; - -import { - updateQuestionsList, - removeQuestionForce, - createQuestion, -} from "@root/questions"; +import { useState } from "react"; import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; - import type { RefObject } from "react"; -import type { - QuizQuestionType, - QuizQuestionBase, - AnyQuizQuestion, -} from "../../../model/questionTypes/shared"; +import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; +import { QuestionType } from "@model/question/question"; + type ChooseAnswerModalProps = { open: boolean; @@ -43,7 +34,7 @@ export const ChooseAnswerModal = ({ switchState, }: ChooseAnswerModalProps) => { const [openModal, setOpenModal] = useState(false); - const [selectedValue, setSelectedValue] = useState("text"); + const [selectedValue, setSelectedValue] = useState("text"); const theme = useTheme(); return ( diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index a44d56e6..43abf605 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -31,7 +31,7 @@ import Page from "@icons/questionsPage/page"; import RatingIcon from "@icons/questionsPage/rating"; import Slider from "@icons/questionsPage/slider"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import { copyQuestion, deleteQuestion, toggleExpandQuestion } from "@root/questions/actions"; +import { copyQuestion, createQuestion, deleteQuestion, toggleExpandQuestion } from "@root/questions/actions"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; @@ -307,7 +307,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging }} > createQuestion(quizId, "nonselected", totalIndex + 1)} + onClick={() => createQuestion(question.quizId)} sx={{ display: plusVisible && !isDragging ? "flex" : "none", width: "100%", diff --git a/src/pages/Questions/DraggableList/index.tsx b/src/pages/Questions/DraggableList/index.tsx index abafd4bc..6d82ce76 100644 --- a/src/pages/Questions/DraggableList/index.tsx +++ b/src/pages/Questions/DraggableList/index.tsx @@ -1,15 +1,15 @@ -import { Box } from "@mui/material"; -import { DragDropContext, Droppable } from "react-beautiful-dnd"; -import DraggableListItem from "./DraggableListItem"; -import type { DropResult } from "react-beautiful-dnd"; -import { useCurrentQuiz } from "@root/quizes/hooks"; -import { useQuestionArray } from "@root/questions/hooks"; -import useSWR from "swr"; import { questionApi } from "@api/question"; -import { setQuestions } from "@root/questions/actions"; -import { isAxiosError } from "axios"; import { devlog } from "@frontend/kitui"; +import { Box } from "@mui/material"; +import { reorderQuestions, setQuestions } from "@root/questions/actions"; +import { useQuestionsStore } from "@root/questions/store"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { isAxiosError } from "axios"; import { enqueueSnackbar } from "notistack"; +import type { DropResult } from "react-beautiful-dnd"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; +import useSWR from "swr"; +import DraggableListItem from "./DraggableListItem"; export const DraggableList = () => { @@ -23,18 +23,10 @@ export const DraggableList = () => { enqueueSnackbar(`Не удалось получить вопросы. ${message}`); } }); - const questions = useQuestionArray(); + const questions = useQuestionsStore(state => state.questions); - const onDragEnd = ({ destination, source }: DropResult) => { // TODO - // if (destination) { - // const newItems = reorder( - // listQuestions[quizId], - // source.index, - // destination.index - // ); - - // updateQuestionsListDragAndDrop(quizId, newItems); - // } + const onDragEnd = ({ destination, source }: DropResult) => { + if (destination) reorderQuestions(source.index, destination.index); }; return ( diff --git a/src/pages/Questions/DropDown/DropDown.tsx b/src/pages/Questions/DropDown/DropDown.tsx index 630fc76c..f5e035be 100644 --- a/src/pages/Questions/DropDown/DropDown.tsx +++ b/src/pages/Questions/DropDown/DropDown.tsx @@ -1,40 +1,25 @@ import { useState } from "react"; -import { useParams } from "react-router-dom"; import { Box, Typography, Link, useTheme, useMediaQuery } from "@mui/material"; import { AnswerDraggableList } from "../AnswerDraggableList"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import SwitchDropDown from "./switchDropDown"; import ButtonsOptions from "../ButtonsOptions"; - import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; +import { addQuestionVariant } from "@root/questions/actions"; + interface Props { - totalIndex: number; + question: QuizQuestionSelect; } -export default function DropDown({ totalIndex }: Props) { +export default function DropDown({ question }: Props) { const [switchState, setSwitchState] = useState("setting"); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionSelect; const SSHC = (data: string) => { setSwitchState(data); - }; - - const addNewAnswer = () => { - const answerNew = question.content.variants.slice(); - answerNew.push({ answer: "", extendedText: "", hints: "" }); - - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, variants: answerNew }, - }); - }; + }; return ( <> @@ -56,10 +41,7 @@ export default function DropDown({ totalIndex }: Props) { Добавьте ответ ) : ( - + )} addQuestionVariant(question.id)} > Добавьте ответ @@ -108,9 +90,9 @@ export default function DropDown({ totalIndex }: Props) { - + ); } diff --git a/src/pages/Questions/DropDown/settingDropDown.tsx b/src/pages/Questions/DropDown/settingDropDown.tsx index a1d30251..c50a0066 100644 --- a/src/pages/Questions/DropDown/settingDropDown.tsx +++ b/src/pages/Questions/DropDown/settingDropDown.tsx @@ -1,202 +1,194 @@ -import { useParams } from "react-router-dom"; import { - Box, - Typography, - Tooltip, - useMediaQuery, - useTheme, + Box, + Tooltip, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import InfoIcon from "../../../assets/icons/InfoIcon"; - import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; + type SettingDropDownProps = { - totalIndex: number; + question: QuizQuestionSelect; }; -export default function SettingDropDown({ totalIndex }: SettingDropDownProps) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); +export default function SettingDropDown({ question }: SettingDropDownProps) { + const theme = useTheme(); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const theme = useTheme(); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const debounced = useDebouncedCallback((value) => { + setQuestionInnerName(question.id, value); + }, 1000); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionSelect; - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); - const debounceAnswer = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, default: value }, - }); - }, 1000); + const debounceAnswer = useDebouncedCallback((value) => { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "select") return; - return ( - <> - - - - Настройки ответов - - - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, multi: target.checked }, - }) - } - /> - - + - Текст в выпадающем списке - - debounceAnswer(target.value)} - /> - - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - required: !e.target.checked, - }); - }} - /> - - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); - }} - /> - - - - - - - - - Текст в выпадающем списке - - debounceAnswer(target.value)} - /> - - {question.content.innerNameCheck && ( - debounced(target.value)} - /> - )} - - - - ); + + + Настройки ответов + + + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "select") return; + + question.content.multi = target.checked; + }) + } + /> + + + Текст в выпадающем списке + + debounceAnswer(target.value)} + /> + + + + + Настройки вопросов + + { + updateQuestionWithFnOptimistic(question.id, question => { + question.required = !e.target.checked; + }); + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + }); + }} + /> + + + + + + + + + Текст в выпадающем списке + + debounceAnswer(target.value)} + /> + + {question.content.innerNameCheck && ( + debounced(target.value)} + /> + )} + + + + ); } diff --git a/src/pages/Questions/DropDown/switchDropDown.tsx b/src/pages/Questions/DropDown/switchDropDown.tsx index ac7d3b12..654c77e5 100644 --- a/src/pages/Questions/DropDown/switchDropDown.tsx +++ b/src/pages/Questions/DropDown/switchDropDown.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; +import { QuizQuestionSelect } from "@model/questionTypes/select"; +import BranchingQuestions from "../branchingQuestions"; import HelpQuestions from "../helpQuestions"; import SettingDropDown from "./settingDropDown"; -import BranchingQuestions from "../branchingQuestions"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionSelect; } export default function SwitchDropDown({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/Emoji/Emoji.tsx b/src/pages/Questions/Emoji/Emoji.tsx index 55a7cc37..d6a87012 100644 --- a/src/pages/Questions/Emoji/Emoji.tsx +++ b/src/pages/Questions/Emoji/Emoji.tsx @@ -1,257 +1,242 @@ -import { useState } from "react"; -import { useParams } from "react-router-dom"; -import { - Box, - Link, - Typography, - useMediaQuery, - useTheme, - Popover, -} from "@mui/material"; -import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; -import ButtonsOptions from "../ButtonsOptions"; -import SwitchEmoji from "./switchEmoji"; -import { AnswerDraggableList } from "../AnswerDraggableList"; -import { EmojiPicker } from "@ui_kit/EmojiPicker"; import { EmojiIcons } from "@icons/EmojiIocns"; import AddEmoji from "@icons/questionsPage/addEmoji"; import PlusImage from "@icons/questionsPage/plus"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - +import { + Box, + Link, + Popover, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { addQuestionVariant, updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { EmojiPicker } from "@ui_kit/EmojiPicker"; +import { useState } from "react"; +import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; +import { AnswerDraggableList } from "../AnswerDraggableList"; +import ButtonsOptions from "../ButtonsOptions"; +import SwitchEmoji from "./switchEmoji"; + interface Props { - totalIndex: number; + question: QuizQuestionEmoji; } -export default function Emoji({ totalIndex }: Props) { - const [switchState, setSwitchState] = useState("setting"); - const [open, setOpen] = useState(false); - const [anchorElement, setAnchorElement] = useState( - null - ); - const [currentIndex, setCurrentIndex] = useState(0); - const { listQuestions } = questionStore(); - const quizId = Number(useParams().quizId); - const theme = useTheme(); - const question = listQuestions[quizId][totalIndex] as QuizQuestionEmoji; - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); +export default function Emoji({ question }: Props) { + const [switchState, setSwitchState] = useState("setting"); + const [open, setOpen] = useState(false); + const [anchorElement, setAnchorElement] = useState( + null + ); + const [selectedVariant, setSelectedVariant] = useState(null); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const SSHC = (data: string) => { - setSwitchState(data); - }; + const SSHC = (data: string) => { + setSwitchState(data); + }; - return ( - <> - - ( - <> - {!isTablet && ( - - { - setAnchorElement(currentTarget); - setCurrentIndex(index); - setOpen(true); + return ( + <> + + ( + <> + {!isTablet && ( + + { + setAnchorElement(currentTarget); + setSelectedVariant(variant.id); + setOpen(true); + }} + > + + {variant.extendedText ? ( + + + {variant.extendedText} + + + + + + ) : ( + + )} + + + + )} + + )} + additionalMobile={(variant) => ( + <> + {isTablet && ( + { + setAnchorElement(currentTarget); + setSelectedVariant(variant.id); + setOpen(true); + }} + sx={{ + display: "flex", + alignItems: "center", + m: "8px", + position: "relative", + }} + > + + {variant.extendedText ? ( + + {variant.extendedText} + + ) : ( + + )} + + + + + + )} + + )} + /> + event.stopPropagation()} + onClose={() => setOpen(false)} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + sx={{ + ".MuiPaper-root.MuiPaper-rounded": { + borderRadius: "10px", + }, }} - > - - {variant.extendedText ? ( - - - {variant.extendedText} - - - - - - ) : ( - - )} - - - - )} - - )} - additionalMobile={(variant, index) => ( - <> - {isTablet && ( - { - setAnchorElement(currentTarget); - setCurrentIndex(index); - setOpen(true); - }} - sx={{ - display: "flex", - alignItems: "center", - m: "8px", - position: "relative", - }} > - - {variant.extendedText ? ( - - {variant.extendedText} - - ) : ( - { + setOpen(false); + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "emoji") return; + + const variant = question.content.variants.find(v => v.id === selectedVariant); + if (!variant) return; + + variant.extendedText = native; + }); + }} /> - )} - + - + - + > + addQuestionVariant(question.id)} + > + Добавьте ответ + + {!isTablet && ( + <> + + или нажмите Enter + + + + )} - )} - - )} - /> - event.stopPropagation()} - onClose={() => setOpen(false)} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - sx={{ - ".MuiPaper-root.MuiPaper-rounded": { - borderRadius: "10px", - }, - }} - > - { - setOpen(false); - const cloneVariants = [...question.content.variants]; - - cloneVariants[currentIndex] = { - ...cloneVariants[currentIndex], - extendedText: native, - }; - - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, variants: cloneVariants }, - }); - }} - /> - - - { - const answerNew = question.content.variants.slice(); - answerNew.push({ answer: "", extendedText: "", hints: "" }); - - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, variants: answerNew }, - }); - }} - > - Добавьте ответ - - {!isTablet && ( - <> - - или нажмите Enter - - - - )} - - - - - - ); + + + + + ); } diff --git a/src/pages/Questions/Emoji/settingEmoji.tsx b/src/pages/Questions/Emoji/settingEmoji.tsx index db92a202..ab68e239 100644 --- a/src/pages/Questions/Emoji/settingEmoji.tsx +++ b/src/pages/Questions/Emoji/settingEmoji.tsx @@ -1,142 +1,131 @@ -import { useParams } from "react-router-dom"; -import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; +import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; +import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; -import { questionStore, updateQuestionsList } from "@root/questions"; - import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; + type SettingEmojiProps = { - totalIndex: number; + question: QuizQuestionEmoji; }; -export default function SettingEmoji({ totalIndex }: SettingEmojiProps) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const isTablet = useMediaQuery(theme.breakpoints.down(985)); - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); +export default function SettingEmoji({ question }: SettingEmojiProps) { + const theme = useTheme(); + const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); + const isTablet = useMediaQuery(theme.breakpoints.down(985)); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionEmoji; - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); + const setInnerName = useDebouncedCallback((value) => { + setQuestionInnerName(question.id, value); + }, 1000); - return ( - - - - Настройки ответов - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, multi: target.checked }, - }); - }} - /> - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, own: target.checked }, - }); - }} - /> - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - required: !e.target.checked, - }); - }} - /> + return ( - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); - }} - /> - - - + > + + + Настройки ответов + + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "emoji") return; + + question.content.multi = target.checked; + })} + /> + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "emoji") return; + + question.content.own = target.checked; + })} + /> + + + + Настройки вопросов + + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "emoji") return; + + question.content.required = !e.target.checked; + })} + /> + + updateQuestionWithFnOptimistic(question.id, question => { + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + })} + /> + + + + + + + {question.content.innerNameCheck && ( + setInnerName(target.value)} + /> + )} - - {question.content.innerNameCheck && ( - debounced(target.value)} - /> - )} - - - ); + ); } diff --git a/src/pages/Questions/Emoji/switchEmoji.tsx b/src/pages/Questions/Emoji/switchEmoji.tsx index 7fda054b..5c49bcaa 100644 --- a/src/pages/Questions/Emoji/switchEmoji.tsx +++ b/src/pages/Questions/Emoji/switchEmoji.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; +import { QuizQuestionEmoji } from "@model/questionTypes/emoji"; import BranchingQuestions from "../branchingQuestions"; import HelpQuestions from "../helpQuestions"; import SettingEmoji from "./settingEmoji"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionEmoji; } export default function SwitchEmoji({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx index da5a51bd..348cce0c 100644 --- a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx +++ b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx @@ -76,9 +76,8 @@ export default memo( > )} diff --git a/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx b/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx index 9fbed6ff..789306d2 100644 --- a/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/Form/FormDraggableList/QuestionPageCard.tsx @@ -1,170 +1,157 @@ -import { useState, useRef, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { Box, InputAdornment, Paper } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; - -import CustomTextField from "@ui_kit/CustomTextField"; -import { ChooseAnswerModal } from "./ChooseAnswerModal"; -import FormTypeQuestions from "../FormTypeQuestions"; -import SwitchQuestionsPage from "../../SwitchQuestionsPage"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import { PointsIcon } from "@icons/questionsPage/PointsIcon"; import Answer from "@icons/questionsPage/answer"; -import OptionsPict from "@icons/questionsPage/options_pict"; -import OptionsAndPict from "@icons/questionsPage/options_and_pict"; +import AnswerGroup from "@icons/questionsPage/answerGroup"; +import Date from "@icons/questionsPage/date"; +import Download from "@icons/questionsPage/download"; +import DropDown from "@icons/questionsPage/drop_down"; import Emoji from "@icons/questionsPage/emoji"; import Input from "@icons/questionsPage/input"; -import DropDown from "@icons/questionsPage/drop_down"; -import Date from "@icons/questionsPage/date"; -import Slider from "@icons/questionsPage/slider"; -import Download from "@icons/questionsPage/download"; +import OptionsAndPict from "@icons/questionsPage/options_and_pict"; +import OptionsPict from "@icons/questionsPage/options_pict"; import Page from "@icons/questionsPage/page"; import RatingIcon from "@icons/questionsPage/rating"; -import AnswerGroup from "@icons/questionsPage/answerGroup"; - +import Slider from "@icons/questionsPage/slider"; +import { Box, InputAdornment, Paper } from "@mui/material"; +import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import CustomTextField from "@ui_kit/CustomTextField"; +import { useRef, useState } from "react"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; -import type { QuizQuestionBase } from "../../../../model/questionTypes/shared"; +import { useDebouncedCallback } from "use-debounce"; +import type { AnyQuizQuestion } from "../../../../model/questionTypes/shared"; +import SwitchQuestionsPage from "../../SwitchQuestionsPage"; +import { ChooseAnswerModal } from "./ChooseAnswerModal"; + interface Props { - totalIndex: number; - draggableProps: DraggableProvidedDragHandleProps | null | undefined; - isDragging: boolean; + question: AnyQuizQuestion; + draggableProps: DraggableProvidedDragHandleProps | null | undefined; +} + +export default function QuestionsPageCard({ + question, + draggableProps, +}: Props) { + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + + const setTitle = useDebouncedCallback((title) => { + updateQuestionWithFnOptimistic(question.id, question => { + question.title = title; + }); + }, 1000); + + return ( + <> + + + setTitle(target.value)} + sx={{ margin: "20px", width: "auto" }} + InputProps={{ + startAdornment: ( + + setOpen((isOpened) => !isOpened)} + > + {IconAndrom(question.type)} + + setOpen(false)} + anchorRef={anchorRef} + question={question} + switchState={question.type} + /> + + ), + endAdornment: ( + + {totalIndex !== 0 && ( + + + + )} + + ), + }} + /> + {/* {question.type === "" ? ( + + ) : ( */} + + {/* )} */} + + + + ); } const IconAndrom = (switchState: string) => { - switch (switchState) { - case "variant": - return ; - case "images": - return ( - - ); - case "varimg": - return ( - - ); - case "emoji": - return ; - case "text": - return ; - case "select": - return ( - - ); - case "date": - return ; - case "number": - return ; - case "file": - return ( - - ); - case "page": - return ; - case "rating": - return ( - - ); - default: - return ( - - ); - } -}; -export default function QuestionsPageCard({ - totalIndex, - draggableProps, - isDragging, -}: Props) { - const [open, setOpen] = useState(false); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex]; - const anchorRef = useRef(null); - const debounced = useDebouncedCallback((title) => { - updateQuestionsList(quizId, totalIndex, { title }); - }, 1000); - - useEffect(() => { - if (question.deleteTimeoutId) { - clearTimeout(question.deleteTimeoutId); + switch (switchState) { + case "variant": + return ; + case "images": + return ( + + ); + case "varimg": + return ( + + ); + case "emoji": + return ; + case "text": + return ; + case "select": + return ( + + ); + case "date": + return ; + case "number": + return ; + case "file": + return ( + + ); + case "page": + return ; + case "rating": + return ( + + ); + default: + return ( + + ); } - }, [question]); - - return ( - <> - - - debounced(target.value)} - sx={{ margin: "20px", width: "auto" }} - InputProps={{ - startAdornment: ( - - setOpen((isOpened) => !isOpened)} - > - {IconAndrom(question.type)} - - setOpen(false)} - anchorRef={anchorRef} - totalIndex={totalIndex} - switchState={question.type} - /> - - ), - endAdornment: ( - - {totalIndex !== 0 && ( - - - - )} - - ), - }} - /> - {/* {question.type === "" ? ( - - ) : ( */} - - {/* )} */} - - - - ); -} +}; diff --git a/src/pages/Questions/Form/FormDraggableList/helper.ts b/src/pages/Questions/Form/FormDraggableList/helper.ts deleted file mode 100644 index 203ee13b..00000000 --- a/src/pages/Questions/Form/FormDraggableList/helper.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const reorder = ( - list: T[], - startIndex: number, - endIndex: number -): T[] => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; -}; diff --git a/src/pages/Questions/Form/FormDraggableList/index.tsx b/src/pages/Questions/Form/FormDraggableList/index.tsx index 959869b3..eefaba53 100644 --- a/src/pages/Questions/Form/FormDraggableList/index.tsx +++ b/src/pages/Questions/Form/FormDraggableList/index.tsx @@ -1,52 +1,35 @@ -import { useParams } from "react-router-dom"; import { Box } from "@mui/material"; -import { DragDropContext, Droppable } from "react-beautiful-dnd"; - -import FormDraggableListItem from "./FormDraggableListItem"; - -import { questionStore, updateQuestionsListDragAndDrop } from "@root/questions"; - -import { reorder } from "./helper"; - import type { DropResult } from "react-beautiful-dnd"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; +import FormDraggableListItem from "./FormDraggableListItem"; +import { useQuestionsStore } from "@root/questions/store"; +import { reorderQuestions } from "@root/questions/actions"; + export const FormDraggableList = () => { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); + const questions = useQuestionsStore(state => state.questions); - const onDragEnd = ({ destination, source }: DropResult) => { - if (destination?.index === 0) { - return; - } + const onDragEnd = ({ destination, source }: DropResult) => { + if (destination) reorderQuestions(source.index, destination.index); + }; - if (destination) { - const newItems = reorder( - listQuestions[quizId], - source.index, - destination.index - ); - - updateQuestionsListDragAndDrop(quizId, newItems); - } - }; - - return ( - - - {(provided, snapshot) => ( - - {listQuestions[quizId]?.map((question, index) => ( - - ))} - {provided.placeholder} - - )} - - - ); + return ( + + + {(provided, snapshot) => ( + + {questions.map((question, index) => ( + + ))} + {provided.placeholder} + + )} + + + ); }; diff --git a/src/pages/Questions/Form/FormQuestionsPage.tsx b/src/pages/Questions/Form/FormQuestionsPage.tsx index 98973346..0581409d 100644 --- a/src/pages/Questions/Form/FormQuestionsPage.tsx +++ b/src/pages/Questions/Form/FormQuestionsPage.tsx @@ -1,135 +1,111 @@ import { Box, Button, Typography, useTheme } from "@mui/material"; -import { useParams } from "react-router-dom"; - -import { FormDraggableList } from "./FormDraggableList"; - -import { - questionStore, - createQuestion, - updateQuestionsList, -} from "@root/questions"; -import { quizStore } from "@root/quizes"; - -import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft"; -import AddAnswer from "../../../assets/icons/questionsPage/addAnswer"; - -import type { - AnyQuizQuestion, - QuizQuestionBase, -} from "../../../model/questionTypes/shared"; +import { incrementCurrentStep } from "@root/quizes/actions"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import { createPortal } from "react-dom"; +import AddAnswer from "../../../assets/icons/questionsPage/addAnswer"; +import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft"; +import { FormDraggableList } from "./FormDraggableList"; +import { collapseAllQuestions, createQuestion } from "@root/questions/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; + export default function FormQuestionsPage() { - const { listQuizes, updateQuizesList } = quizStore(); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const handleNext = () => { - updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 }); - }; + const theme = useTheme(); + const { quiz } = useCurrentQuiz(); - const collapseEverything = () => { - listQuestions[quizId].forEach((item, index) => { - updateQuestionsList(quizId, index, { - ...item, - expanded: false, - }); - }); - }; + if (!quiz) return null; - const theme = useTheme(); - - return ( - <> - - Заголовок анкеты - - - - - { - createQuestion(quizId); - }} - > - - - Добавить еще один вопрос - - - - - - - - {createPortal(, document.body)} - - ); + return ( + <> + + Заголовок анкеты + + + + + { + createQuestion(quiz.id); + }} + > + + + Добавить еще один вопрос + + + + + + + + {createPortal(, document.body)} + + ); } diff --git a/src/pages/Questions/Form/FormTypeQuestions.tsx b/src/pages/Questions/Form/FormTypeQuestions.tsx index 949f45d2..9e2ab871 100644 --- a/src/pages/Questions/Form/FormTypeQuestions.tsx +++ b/src/pages/Questions/Form/FormTypeQuestions.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { useParams } from "react-router-dom"; import { Box } from "@mui/material"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; @@ -12,23 +11,14 @@ import Input from "../../../assets/icons/questionsPage/input"; import DropDown from "../../../assets/icons/questionsPage/drop_down"; import Date from "../../../assets/icons/questionsPage/date"; import Slider from "../../../assets/icons/questionsPage/slider"; -import Download from "../../../assets/icons/questionsPage/download"; - -import { - questionStore, - updateQuestionsList, - createQuestion, - removeQuestionForce, -} from "@root/questions"; +import Download from "../../../assets/icons/questionsPage/download"; import type { - QuizQuestionBase, + AnyQuizQuestion, } from "../../../model/questionTypes/shared"; import { QuestionType } from "@model/question/question"; +import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; -interface Props { - totalIndex: number; -} type ButtonTypeQuestion = { icon: JSX.Element; @@ -69,11 +59,12 @@ const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [ }, ]; -export default function FormTypeQuestions({ totalIndex }: Props) { +interface Props { + question: AnyQuizQuestion; +} + +export default function FormTypeQuestions({ question }: Props) { const [switchState, setSwitchState] = useState(""); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex] as QuizQuestionBase; return ( @@ -85,21 +76,16 @@ export default function FormTypeQuestions({ totalIndex }: Props) { margin: "20px", }} > - {(totalIndex === 0 + {(true /* TODO какое-то непонятное условие */ ? BUTTON_TYPE_QUESTIONS : BUTTON_TYPE_SHORT_QUESTIONS - ).map(({ icon, title, value }) => ( + ).map(({ icon, title, value: questionType }) => ( { - const clonedQuestion = { ...question }; - - removeQuestionForce(quizId, clonedQuestion.id); - createQuestion(quizId, value, totalIndex); - updateQuestionsList(quizId, totalIndex, { - expanded: clonedQuestion.expanded, - type: value, - }); + updateQuestionWithFnOptimistic(question.id, question => { + question.type = questionType; + }) }} icon={icon} text={title} @@ -109,9 +95,10 @@ export default function FormTypeQuestions({ totalIndex }: Props) { - + {/* TODO конфликт типов */} + {/* */} ); } diff --git a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx index eec66c9d..06c94707 100644 --- a/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx +++ b/src/pages/Questions/OptionsAndPicture/OptionsAndPicture.tsx @@ -1,371 +1,352 @@ -import { - Box, - Link, - Typography, - useTheme, - useMediaQuery, - InputAdornment, - IconButton, - Button, - Popover, - TextareaAutosize, - TextField, -} from "@mui/material"; -import { useState } from "react"; -import { useParams } from "react-router-dom"; - -import { CropModal } from "@ui_kit/Modal/CropModal"; -import { AnswerDraggableList } from "../AnswerDraggableList"; -import { UploadImageModal } from "../UploadImage/UploadImageModal"; - import { ImageAddIcons } from "@icons/ImageAddIcons"; import { MessageIcon } from "@icons/messagIcon"; import { PointsIcon } from "@icons/questionsPage/PointsIcon"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; -import { questionStore, setVariantImageUrl, setVariantOriginalImageUrl, updateQuestionsList } from "@root/questions"; +import { + Box, + IconButton, + InputAdornment, + Link, + Popover, + TextField, + TextareaAutosize, + Typography, + useMediaQuery, + useTheme +} from "@mui/material"; +import { openCropModal } from "@root/cropModal"; +import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; +import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions"; +import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; +import { CropModal } from "@ui_kit/Modal/CropModal"; +import { useState } from "react"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; +import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; +import { AnswerDraggableList } from "../AnswerDraggableList"; import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict"; +import { UploadImageModal } from "../UploadImage/UploadImageModal"; import SwitchOptionsAndPict from "./switchOptionsAndPict"; -import { openCropModal } from "@root/cropModal"; -import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; -import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; interface Props { - totalIndex: number; + question: QuizQuestionVarImg; } -export default function OptionsAndPicture({ totalIndex }: Props) { - const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false); +export default function OptionsAndPicture({ question }: Props) { const [switchState, setSwitchState] = useState("setting"); - const [currentIndex, setCurrentIndex] = useState(0); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); + const [selectedVariantId, setSelectedVariantId] = useState(null); const theme = useTheme(); const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg; - const SSHC = (data: string) => { - setSwitchState(data); - }; + const SSHC = (data: string) => { + setSwitchState(data); + }; const handleImageUpload = (files: FileList | null) => { - if (!files?.length) return; + if (!files?.length || !selectedVariantId) return; const [file] = Array.from(files); const url = URL.createObjectURL(file); - setVariantImageUrl(quizId, totalIndex, currentIndex, url); - setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url); - setIsUploadImageModalOpen(false); + setVariantImageUrl(question.id, selectedVariantId, url); + setVariantOriginalImageUrl(question.id, selectedVariantId, url); + closeImageUploadModal(); openCropModal(url, url); }; function handleCropModalSaveClick(url: string) { - setVariantImageUrl(quizId, totalIndex, currentIndex, url); + if (!selectedVariantId) return; + + setVariantImageUrl(question.id, selectedVariantId, url); } return ( - <> - - ( - <> - {!isMobile && ( - { - if (!("originalImageUrl" in variant)) return; + <> + + ( + <> + {!isMobile && ( + { + if (!("originalImageUrl" in variant)) return; - setCurrentIndex(index); - if (variant.extendedText) return openCropModal( - variant.extendedText, - variant.originalImageUrl - ); + setSelectedVariantId(variant.id); + if (variant.extendedText) return openCropModal( + variant.extendedText, + variant.originalImageUrl + ); - setIsUploadImageModalOpen(true); - }} - onPlusClick={() => { - setCurrentIndex(index); - setIsUploadImageModalOpen(true); - }} - sx={{ mx: "10px" }} - /> - )} - - )} - additionalMobile={(variant, index) => ( - <> - {isMobile && ( - { - if (!("originalImageUrl" in variant)) return; + openImageUploadModal(); + }} + onPlusClick={() => { + setSelectedVariantId(variant.id); + openImageUploadModal(); + }} + sx={{ mx: "10px" }} + /> + )} + + )} + additionalMobile={(variant) => ( + <> + {isMobile && ( + { + if (!("originalImageUrl" in variant)) return; - setCurrentIndex(index); - if (variant.extendedText) return openCropModal( - variant.extendedText, - variant.originalImageUrl - ); + setSelectedVariantId(variant.id); + if (variant.extendedText) return openCropModal( + variant.extendedText, + variant.originalImageUrl + ); - setIsUploadImageModalOpen(true); - }} - onPlusClick={() => { - setCurrentIndex(index); - setIsUploadImageModalOpen(true); - }} - sx={{ m: "8px", width: "auto" }} - /> - )} - - )} - /> - setIsUploadImageModalOpen(false)} - imgHC={handleImageUpload} - /> - - - - - - - {!isMobile && ( - - - - - - + - - - )} - - ), - endAdornment: ( - - - - - - - - - - - - ), - }} - sx={{ - "& .MuiInputBase-root": { - padding: "13.5px", - borderRadius: "10px", - background: "#ffffff", - height: "48px", - }, - "& .MuiOutlinedInput-notchedOutline": { - border: "none", - }, - }} - inputProps={{ - sx: { fontSize: "18px", lineHeight: "21px", py: 0 }, - }} - /> + openImageUploadModal(); + }} + onPlusClick={() => { + setSelectedVariantId(variant.id); + openImageUploadModal(); + }} + sx={{ m: "8px", width: "auto" }} + /> + )} + + )} + /> + + + + + + + + {!isMobile && ( + + + + + + + + + + )} + + ), + endAdornment: ( + + + + + + + + + + + + ), + }} + sx={{ + "& .MuiInputBase-root": { + padding: "13.5px", + borderRadius: "10px", + background: "#ffffff", + height: "48px", + }, + "& .MuiOutlinedInput-notchedOutline": { + border: "none", + }, + }} + inputProps={{ + sx: { fontSize: "18px", lineHeight: "21px", py: 0 }, + }} + /> - {isMobile && ( - - - - - - - - + - - - - )} - - - { - const clonedContent = { ...question.content }; - clonedContent.variants.push({ - answer: "", - hints: "", - extendedText: "", - originalImageUrl: "", - }); - updateQuestionsList(quizId, totalIndex, { - content: clonedContent, - }); - }} - > - Добавьте ответ - - {isMobile ? null : ( - <> - - или нажмите Enter - - - - )} - - - - - + {isMobile && ( + + + + + + + + + + + + + )} + + + { + addQuestionVariant(question.id) + }} + > + Добавьте ответ + + {isMobile ? null : ( + <> + + или нажмите Enter + + + + )} + + + + + - ); + ); } diff --git a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx index b6b12cfd..f5f61d1f 100644 --- a/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx +++ b/src/pages/Questions/OptionsAndPicture/SettingOptionsAndPict.tsx @@ -1,175 +1,166 @@ -import { useParams } from "react-router-dom"; -import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; +import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; +import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; + type SettingOptionsAndPictProps = { - totalIndex: number; + question: QuizQuestionVarImg; }; -export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndPictProps) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); +export default function SettingOptionsAndPict({ question }: SettingOptionsAndPictProps) { + const theme = useTheme(); + const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(680)); - const isMobile = useMediaQuery(theme.breakpoints.down(680)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg; - const debounced = useDebouncedCallback((replText) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, replText }, - }); - }, 1000); - const debounceDescription = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); + const setReplText = useDebouncedCallback((replText) => { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "varimg") return; - return ( - <> - - - - Настройки ответов - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, own: target.checked }, - }); - }} - /> - {!isWrappColumn && ( - - { + setQuestionInnerName(question.id, value); + }, 1000); + + return ( + <> + - Текст-заглушка на картинке - - debounced(target.value)} - /> + > + + + Настройки ответов + + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "varimg") return; + + question.content.own = target.checked; + })} + /> + {!isWrappColumn && ( + + + Текст-заглушка на картинке + + setReplText(target.value)} + /> + + )} + + + + Настройки вопросов + + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "varimg") return; + + question.content.required = target.checked; + })} + /> + + updateQuestionWithFnOptimistic(question.id, question => { + question.content.innerNameCheck = target.checked; + question.content.innerName = ""; + })} + /> + + + + + + + {question.content.innerNameCheck && ( + setDescription(target.value)} + /> + )} + {isWrappColumn && ( + <> + + Текст-заглушка на картинке + + setReplText(target.value)} + /> + + )} + - )} - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, required: target.checked }, - }); - }} - /> - - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: "", - }, - }); - }} - /> - - - - - - - {question.content.innerNameCheck && ( - debounceDescription(target.value)} - /> - )} - {isWrappColumn && ( - <> - - Текст-заглушка на картинке - - debounced(target.value)} - /> - - )} - - - - ); + + ); } diff --git a/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx b/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx index 8235fda2..9dd2bdcc 100644 --- a/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx +++ b/src/pages/Questions/OptionsAndPicture/switchOptionsAndPict.tsx @@ -1,32 +1,29 @@ -import * as React from "react"; -import BranchingQuestions from "../branchingQuestions"; -import SettingOptionsAndPict from "./SettingOptionsAndPict"; -import HelpQuestions from "../helpQuestions"; +import { QuizQuestionVarImg } from "@model/questionTypes/varimg"; import UploadImage from "../UploadImage"; +import BranchingQuestions from "../branchingQuestions"; +import HelpQuestions from "../helpQuestions"; +import SettingOptionsAndPict from "./SettingOptionsAndPict"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionVarImg; } export default function SwitchOptionsAndPict({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - case "image": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + case "image": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/OptionsPicture/OptionsPicture.tsx b/src/pages/Questions/OptionsPicture/OptionsPicture.tsx index b0fe7d33..591c5a94 100644 --- a/src/pages/Questions/OptionsPicture/OptionsPicture.tsx +++ b/src/pages/Questions/OptionsPicture/OptionsPicture.tsx @@ -6,11 +6,11 @@ import { useTheme } from "@mui/material"; import { openCropModal } from "@root/cropModal"; -import { setVariantImageUrl, setVariantOriginalImageUrl, updateQuestionsList } from "@root/questions"; +import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; +import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; import { CropModal } from "@ui_kit/Modal/CropModal"; import { useState } from "react"; -import { useParams } from "react-router-dom"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import { AnswerDraggableList } from "../AnswerDraggableList"; @@ -24,49 +24,39 @@ interface Props { } export default function OptionsPicture({ question }: Props) { - const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false); - const [currentIndex, setCurrentIndex] = useState(0); const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const quizId = Number(useParams().quizId); + const [selectedVariantId, setSelectedVariantId] = useState(null); const [switchState, setSwitchState] = useState("setting"); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); const SSHC = (data: string) => { setSwitchState(data); }; const handleImageUpload = (files: FileList | null) => { - if (!files?.length) return; + if (!files?.length || !selectedVariantId) return; const [file] = Array.from(files); const url = URL.createObjectURL(file); - setVariantImageUrl(quizId, totalIndex, currentIndex, url); - setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url); - setIsUploadImageModalOpen(false); + setVariantImageUrl(question.id, selectedVariantId, url); + setVariantOriginalImageUrl(question.id, selectedVariantId, url); + closeImageUploadModal(); openCropModal(url, url); }; - const addNewAnswer = () => { - const answerNew = question.content.variants.slice(); - answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" }); - - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, variants: answerNew }, - }); - }; - function handleCropModalSaveClick(url: string) { - setVariantImageUrl(quizId, totalIndex, currentIndex, url); + if (!selectedVariantId) return; + + setVariantImageUrl(question.id, selectedVariantId, url); } return ( <> ( + question={question} + additionalContent={(variant) => ( <> {!isMobile && ( { if (!("originalImageUrl" in variant)) return; - setCurrentIndex(index); + setSelectedVariantId(variant.id); if (variant.extendedText) { return openCropModal( variant.extendedText, @@ -82,18 +72,18 @@ export default function OptionsPicture({ question }: Props) { ); } - setIsUploadImageModalOpen(true); + openImageUploadModal(); }} onPlusClick={() => { - setCurrentIndex(index); - setIsUploadImageModalOpen(true); + setSelectedVariantId(variant.id); + openImageUploadModal(); }} sx={{ mx: "10px" }} /> )} )} - additionalMobile={(variant, index) => ( + additionalMobile={(variant) => ( <> {isMobile && ( { if (!("originalImageUrl" in variant)) return; - setCurrentIndex(index); + setSelectedVariantId(variant.id); if (variant.extendedText) { return openCropModal( variant.extendedText, @@ -109,11 +99,11 @@ export default function OptionsPicture({ question }: Props) { ); } - setIsUploadImageModalOpen(true); + openImageUploadModal(); }} onPlusClick={() => { - setCurrentIndex(index); - setIsUploadImageModalOpen(true); + setSelectedVariantId(variant.id); + openImageUploadModal(); }} sx={{ m: "8px", width: "auto" }} /> @@ -121,18 +111,14 @@ export default function OptionsPicture({ question }: Props) { )} /> - setIsUploadImageModalOpen(false)} - imgHC={handleImageUpload} - /> + addQuestionVariant(question.id)} > Добавьте ответ @@ -159,8 +145,8 @@ export default function OptionsPicture({ question }: Props) { )} - - + + ); diff --git a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx index a7ce9727..305a6c2d 100644 --- a/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx +++ b/src/pages/Questions/OptionsPicture/settingOpytionsPict.tsx @@ -1,327 +1,314 @@ -import { useEffect } from "react"; -import { useParams } from "react-router-dom"; import { - Box, - Button, - Typography, - Tooltip, - useMediaQuery, - useTheme, + Box, + Button, + Tooltip, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import InfoIcon from "../../../assets/icons/InfoIcon"; -import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2"; import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1"; +import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2"; import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIcon11"; -import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21"; import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12"; - +import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21"; import type { QuizQuestionImages } from "../../../model/questionTypes/images"; -interface Props { - Icon: (props: { color: string }) => JSX.Element; - // Icon: React.ElementType; - isActive?: boolean; - onClick: () => void; -} - -type SettingOpytionsPictProps = { - totalIndex: number; -}; type Proportion = "1:1" | "2:1" | "1:2"; type ProportionItem = { - value: Proportion; - icon: (props: { color: string }) => JSX.Element; + value: Proportion; + icon: (props: { color: string; }) => JSX.Element; }; const PROPORTIONS: ProportionItem[] = [ - { value: "1:1", icon: ProportionsIcon11 }, - { value: "2:1", icon: ProportionsIcon21 }, - { value: "1:2", icon: ProportionsIcon12 }, + { value: "1:1", icon: ProportionsIcon11 }, + { value: "2:1", icon: ProportionsIcon21 }, + { value: "1:2", icon: ProportionsIcon12 }, ]; -export function SelectIconButton({ Icon, isActive = false, onClick }: Props) { - const theme = useTheme(); +type SettingOpytionsPictProps = { + question: QuizQuestionImages; +}; - return ( - diff --git a/src/pages/Questions/RatingOptions/RatingOptions.tsx b/src/pages/Questions/RatingOptions/RatingOptions.tsx index a447d84c..baae6bd4 100644 --- a/src/pages/Questions/RatingOptions/RatingOptions.tsx +++ b/src/pages/Questions/RatingOptions/RatingOptions.tsx @@ -1,17 +1,15 @@ import { useState, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { - Box, - Typography, - TextField, - useMediaQuery, - useTheme, + Box, + Typography, + TextField, + useMediaQuery, + useTheme, } from "@mui/material"; import { useDebouncedCallback } from "use-debounce"; -import { questionStore, updateQuestionsList } from "@root/questions"; import ButtonsOptions from "../ButtonsOptions"; import SwitchRating from "./switchRating"; - import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon"; import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon"; import HeartIcon from "../../../assets/icons/questionsPage/heartIcon"; @@ -19,260 +17,253 @@ import LikeIcon from "../../../assets/icons/questionsPage/likeIcon"; 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"; + interface Props { - totalIndex: number; + question: QuizQuestionRating; } export type ButtonRatingFrom = { - name: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag"; - icon: JSX.Element; + name: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag"; + icon: JSX.Element; }; -export default function RatingOptions({ totalIndex }: Props) { - const [switchState, setSwitchState] = useState("setting"); - const [negativeText, setNegativeText] = useState(""); - const [positiveText, setPositiveText] = useState(""); - const [negativeTextWidth, setNegativeTextWidth] = useState(0); - const [positiveTextWidth, setPositiveTextWidth] = useState(0); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionRating; - const negativeRef = useRef(null); - const positiveRef = useRef(null); - const debounceNegativeDescription = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - ratingNegativeDescription: value.substring(0, 15), - }, - }); - }, 500); - const debouncePositiveDescription = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - ratingPositiveDescription: value.substring(0, 15), - }, - }); - }, 500); +export default function RatingOptions({ question }: Props) { + const [switchState, setSwitchState] = useState("setting"); + const [negativeText, setNegativeText] = useState(""); + const [positiveText, setPositiveText] = useState(""); + const [negativeTextWidth, setNegativeTextWidth] = useState(0); + const [positiveTextWidth, setPositiveTextWidth] = useState(0); + const quizId = Number(useParams().quizId); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const negativeRef = useRef(null); + const positiveRef = useRef(null); - useEffect(() => { - setNegativeText(question.content.ratingNegativeDescription); - setPositiveText(question.content.ratingPositiveDescription); - }, []); + const debounceNegativeDescription = useDebouncedCallback((value) => { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; - useEffect(() => { - setNegativeTextWidth(negativeRef.current?.offsetWidth || 0); - }, [negativeText]); + question.content.ratingNegativeDescription = value.substring(0, 15); + }); + }, 500); + const debouncePositiveDescription = useDebouncedCallback((value) => { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; - useEffect(() => { - setPositiveTextWidth(positiveRef.current?.offsetWidth || 0); - }, [positiveText]); + question.content.ratingPositiveDescription = value.substring(0, 15); + }); + }, 500); - const buttonRatingForm: ButtonRatingFrom[] = [ - { - name: "star", - icon: , - }, - { name: "trophie", icon: }, - { name: "flag", icon: }, - { name: "heart", icon: }, - { name: "like", icon: }, - { - name: "bubble", - icon: , - }, - { name: "hashtag", icon: }, - ]; + useEffect(() => { + setNegativeText(question.content.ratingNegativeDescription); + setPositiveText(question.content.ratingPositiveDescription); + }, []); - const SSHC = (data: string) => { - setSwitchState(data); - }; + useEffect(() => { + setNegativeTextWidth(negativeRef.current?.offsetWidth || 0); + }, [negativeText]); - return ( - <> - - - {Array.from( - { length: question.content.steps }, - (_, index) => index - ).map((itemNumber) => ( + useEffect(() => { + setPositiveTextWidth(positiveRef.current?.offsetWidth || 0); + }, [positiveText]); + + const buttonRatingForm: ButtonRatingFrom[] = [ + { + name: "star", + icon: , + }, + { name: "trophie", icon: }, + { name: "flag", icon: }, + { name: "heart", icon: }, + { name: "like", icon: }, + { + name: "bubble", + icon: , + }, + { name: "hashtag", icon: }, + ]; + + const SSHC = (data: string) => { + setSwitchState(data); + }; + + return ( + <> { - updateQuestionsList( - quizId, - totalIndex, - { - content: { - ...question.content, - ratingExpanded: true, - }, - } - ); - }, - sx: { - cursor: "pointer", - transform: "scale(1.5)", - ":hover": { - transform: "scale(1.7)", - transition: "0.2s", - }, - }, - } - : { sx: { transform: "scale(1.5)" } })} + sx={{ + display: "flex", + px: "20px", + flexDirection: "column", + gap: "20px", + marginTop: isMobile ? "20px" : 0, + }} > - { - buttonRatingForm.find( - ({ name }) => question.content.form === name - )?.icon - } + + {Array.from( + { length: question.content.steps }, + (_, index) => index + ).map((itemNumber) => ( + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; + + question.content.ratingExpanded = true; + }); + }, + sx: { + cursor: "pointer", + transform: "scale(1.5)", + ":hover": { + transform: "scale(1.7)", + transition: "0.2s", + }, + }, + } + : { sx: { transform: "scale(1.5)" } })} + > + { + buttonRatingForm.find( + ({ name }) => question.content.form === name + )?.icon + } + + ))} + + + + + {negativeText} + + { + if (target.value.length <= 15) { + setNegativeText(target.value); + debounceNegativeDescription(target.value); + } + }} + onBlur={({ target }) => debounceNegativeDescription(target.value)} + sx={{ + width: negativeTextWidth + 10 + "px", + maxWidth: isMobile ? "140px" : "230px", + background: "transparent", + fontSize: "18px", + minWidth: "95px", + transition: "0.2s", + "& .MuiInputBase-root": { + "& .MuiInputBase-input": { + color: theme.palette.grey2.main, + fontSize: "16px", + padding: "0 3px", + borderRadius: "3px", + border: "1px solid", + borderColor: "transparent", + "&:hover, &:focus": { + borderColor: theme.palette.grey2.main, + }, + }, + "& .MuiOutlinedInput-notchedOutline": { + outline: "none", + border: "none", + }, + }, + }} + /> + + + + {positiveText} + + { + if (target.value.length <= 15) { + setPositiveText(target.value); + debouncePositiveDescription(target.value); + } + }} + onBlur={({ target }) => debouncePositiveDescription(target.value)} + sx={{ + width: positiveTextWidth + 10 + "px", + maxWidth: isMobile ? "140px" : "230px", + background: "transparent", + fontSize: "18px", + minWidth: "95px", + transition: "0.2s", + "& .MuiInputBase-root": { + "& .MuiInputBase-input": { + color: theme.palette.grey2.main, + fontSize: "16px", + padding: "0 3px", + borderRadius: "3px", + border: "1px solid", + borderColor: "transparent", + "&:hover, &:focus": { + borderColor: theme.palette.grey2.main, + }, + }, + "& .MuiOutlinedInput-notchedOutline": { + outline: "none", + border: "none", + }, + }, + }} + /> + + - ))} - - - - - {negativeText} - - { - if (target.value.length <= 15) { - setNegativeText(target.value); - debounceNegativeDescription(target.value); - } - }} - onBlur={({ target }) => debounceNegativeDescription(target.value)} - sx={{ - width: negativeTextWidth + 10 + "px", - maxWidth: isMobile ? "140px" : "230px", - background: "transparent", - fontSize: "18px", - minWidth: "95px", - transition: "0.2s", - "& .MuiInputBase-root": { - "& .MuiInputBase-input": { - color: theme.palette.grey2.main, - fontSize: "16px", - padding: "0 3px", - borderRadius: "3px", - border: "1px solid", - borderColor: "transparent", - "&:hover, &:focus": { - borderColor: theme.palette.grey2.main, - }, - }, - "& .MuiOutlinedInput-notchedOutline": { - outline: "none", - border: "none", - }, - }, - }} + - - - - {positiveText} - - { - if (target.value.length <= 15) { - setPositiveText(target.value); - debouncePositiveDescription(target.value); - } - }} - onBlur={({ target }) => debouncePositiveDescription(target.value)} - sx={{ - width: positiveTextWidth + 10 + "px", - maxWidth: isMobile ? "140px" : "230px", - background: "transparent", - fontSize: "18px", - minWidth: "95px", - transition: "0.2s", - "& .MuiInputBase-root": { - "& .MuiInputBase-input": { - color: theme.palette.grey2.main, - fontSize: "16px", - padding: "0 3px", - borderRadius: "3px", - border: "1px solid", - borderColor: "transparent", - "&:hover, &:focus": { - borderColor: theme.palette.grey2.main, - }, - }, - "& .MuiOutlinedInput-notchedOutline": { - outline: "none", - border: "none", - }, - }, - }} - /> - - - - - - - ); + + + ); } diff --git a/src/pages/Questions/RatingOptions/settingRating.tsx b/src/pages/Questions/RatingOptions/settingRating.tsx index 272b5fc0..20884f28 100644 --- a/src/pages/Questions/RatingOptions/settingRating.tsx +++ b/src/pages/Questions/RatingOptions/settingRating.tsx @@ -1,198 +1,196 @@ -import { useParams } from "react-router-dom"; -import { Box, ButtonBase, Slider, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; - +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 CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; -import { questionStore, updateQuestionsList } from "@root/questions"; - +import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; -import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon"; import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon"; -import HeartIcon from "../../../assets/icons/questionsPage/heartIcon"; -import LikeIcon from "../../../assets/icons/questionsPage/likeIcon"; -import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon"; -import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon"; import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini"; - +import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon"; +import HeartIcon from "../../../assets/icons/questionsPage/heartIcon"; +import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon"; +import LikeIcon from "../../../assets/icons/questionsPage/likeIcon"; +import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon"; import type { ButtonRatingFrom } from "./RatingOptions"; -import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; + type SettingSliderProps = { - totalIndex: number; + question: QuizQuestionRating; }; -export default function SettingSlider({ totalIndex }: SettingSliderProps) { - const quizId = Number(useParams().quizId); - const theme = useTheme(); - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber; - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); +export default function SettingSlider({ question }: SettingSliderProps) { + const theme = useTheme(); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const buttonRatingForm: ButtonRatingFrom[] = [ - { name: "star", icon: }, - { name: "trophie", icon: }, - { name: "flag", icon: }, - { name: "heart", icon: }, - { name: "like", icon: }, - { - name: "bubble", - icon: , - }, - { name: "hashtag", icon: }, - ]; + const setInnerName = useDebouncedCallback((value) => { + setQuestionInnerName(question.id, value); + }, 1000); - return ( - - - - Настройки рейтинга - - - Форма - - - {buttonRatingForm.map(({ name, icon }, index) => ( - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, form: name }, - }); - }} - sx={{ - backgroundColor: - question.content.form === name - ? theme.palette.brightPurple.main - : "transparent", - color: - question.content.form === name - ? "#ffffff" - : theme.palette.grey3.main, - width: "40px", - height: "40px", - borderRadius: "4px", - }} - > - {icon} - - ))} - + const buttonRatingForm: ButtonRatingFrom[] = [ + { name: "star", icon: }, + { name: "trophie", icon: }, + { name: "flag", icon: }, + { name: "heart", icon: }, + { name: "like", icon: }, + { + name: "bubble", + icon: , + }, + { name: "hashtag", icon: }, + ]; - - Количество - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, steps: Number(value) || 1 }, - }); - }} - /> - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - required: !e.target.checked, - }); - }} - /> + return ( - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); + sx={{ + display: "flex", + justifyContent: "space-between", + flexDirection: isWrappColumn ? "column" : null, }} - /> - - - + > + + + Настройки рейтинга + + + Форма + + + {buttonRatingForm.map(({ name, icon }, index) => ( + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; + + question.content.form = name; + }); + }} + sx={{ + backgroundColor: + question.content.form === name + ? theme.palette.brightPurple.main + : "transparent", + color: + question.content.form === name + ? "#ffffff" + : theme.palette.grey3.main, + width: "40px", + height: "40px", + borderRadius: "4px", + }} + > + {icon} + + ))} + + + + Количество + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; + + question.content.steps = Number(value) || 1; + }); + }} + /> + + + + Настройки вопросов + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; + + question.required = !e.target.checked; + }); + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "rating") return; + + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + }); + }} + /> + + + + + + + {question.content.innerNameCheck && ( + setInnerName(target.value)} + /> + )} - - {question.content.innerNameCheck && ( - debounced(target.value)} - /> - )} - - - ); + ); } diff --git a/src/pages/Questions/RatingOptions/switchRating.tsx b/src/pages/Questions/RatingOptions/switchRating.tsx index ed904590..568525b8 100644 --- a/src/pages/Questions/RatingOptions/switchRating.tsx +++ b/src/pages/Questions/RatingOptions/switchRating.tsx @@ -1,27 +1,26 @@ +import { QuizQuestionRating } from "@model/questionTypes/rating"; import BranchingQuestions from "../branchingQuestions"; import HelpQuestions from "../helpQuestions"; import SettingRating from "./settingRating"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionRating; } export default function SwitchRating({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/SliderOptions/SliderOptions.tsx b/src/pages/Questions/SliderOptions/SliderOptions.tsx index 89ec3600..0048e70a 100644 --- a/src/pages/Questions/SliderOptions/SliderOptions.tsx +++ b/src/pages/Questions/SliderOptions/SliderOptions.tsx @@ -1,226 +1,222 @@ import { useState } from "react"; -import { useParams } from "react-router-dom"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import ButtonsOptions from "../ButtonsOptions"; import CustomNumberField from "@ui_kit/CustomNumberField"; import SwitchSlider from "./switchSlider"; -import { questionStore, updateQuestionsList } from "@root/questions"; - import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; +import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; + interface Props { - totalIndex: number; + question: QuizQuestionNumber; } -export default function SliderOptions({ totalIndex }: Props) { - const theme = useTheme(); - const isTablet = useMediaQuery(theme.breakpoints.down(980)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const [switchState, setSwitchState] = useState("setting"); - const [stepError, setStepError] = useState(""); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber; +export default function SliderOptions({ question }: Props) { + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down(980)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const [switchState, setSwitchState] = useState("setting"); + const [stepError, setStepError] = useState(""); - const SSHC = (data: string) => { - setSwitchState(data); - }; + const SSHC = (data: string) => { + setSwitchState(data); + }; - return ( - <> - - - - Выбор значения из диапазона - - - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - range: `${target.value}—${ - question.content.range.split("—")[1] - }`, - }, - }); - }} - onBlur={({ target }) => { - const start = question.content.start; - const min = Number(target.value); - const max = Number(question.content.range.split("—")[1]); - - if (min >= max) { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - range: `${max - 1 >= 0 ? max - 1 : 0}—${ - question.content.range.split("—")[1] - }`, - }, - }); - } - - if (start < min) { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, start: min }, - }); - } - }} - /> - - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - range: `${question.content.range.split("—")[0]}—${ - target.value - }`, - }, - }); - }} - onBlur={({ target }) => { - const start = question.content.start; - const step = question.content.step; - const min = Number(question.content.range.split("—")[0]); - const max = Number(target.value); - const range = max - min; - - if (max <= min) { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - range: `${question.content.range.split("—")[0]}—${ - min + 1 >= 100 ? 100 : min + 1 - }`, - }, - }); - } - - if (start > max) { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, start: max }, - }); - } - - if (step > max) { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, step: max }, - }); - - if (range % step) { - setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`); - } else { - setStepError(""); - } - } - }} - /> - - - - - - Начальное значение - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, start: Number(target.value) }, - }); - }} - /> - - - + - Шаг - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, step: Number(target.value) }, - }); - }} - onBlur={({ target }) => { - const min = Number(question.content.range.split("—")[0]); - const max = Number(question.content.range.split("—")[1]); - const range = max - min; - const step = Number(target.value); + + + Выбор значения из диапазона + + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; - if (step > max) { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, step: max }, - }); - } + question.content.range = `${target.value}—${question.content.range.split("—")[1]}`; + }); + }} + onBlur={({ target }) => { + const start = question.content.start; + const min = Number(target.value); + const max = Number(question.content.range.split("—")[1]); - if (range % step) { - setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`); - } else { - setStepError(""); - } - }} - /> - - - - - - - ); + if (min >= max) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.range = `${max - 1 >= 0 ? max - 1 : 0}—${question.content.range.split("—")[1]}`; + }); + } + + if (start < min) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.start = min; + }); + } + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.range = `${question.content.range.split("—")[0]}—${target.value}`; + }); + }} + onBlur={({ target }) => { + const start = question.content.start; + const step = question.content.step; + const min = Number(question.content.range.split("—")[0]); + const max = Number(target.value); + const range = max - min; + + if (max <= min) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.range = `${min}—${min + 1 >= 100 ? 100 : min + 1}`; + }); + } + + if (start > max) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.start = max; + }); + } + + if (step > max) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.step = min; + }); + + if (range % step) { + setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`); + } else { + setStepError(""); + } + } + }} + /> + + + + + + Начальное значение + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.start = Number(target.value); + }); + }} + /> + + + + Шаг + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.step = Number(target.value); + }); + }} + onBlur={({ target }) => { + const min = Number(question.content.range.split("—")[0]); + const max = Number(question.content.range.split("—")[1]); + const range = max - min; + const step = Number(target.value); + + if (step > max) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.step = max; + }); + } + + if (range % step) { + setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`); + } else { + setStepError(""); + } + }} + /> + + + + + + + ); } diff --git a/src/pages/Questions/SliderOptions/settingSlider.tsx b/src/pages/Questions/SliderOptions/settingSlider.tsx index b0a89cee..1c8d32f6 100644 --- a/src/pages/Questions/SliderOptions/settingSlider.tsx +++ b/src/pages/Questions/SliderOptions/settingSlider.tsx @@ -1,132 +1,130 @@ -import { useParams } from "react-router-dom"; -import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; -import { useDebouncedCallback } from "use-debounce"; +import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; +import { useDebouncedCallback } from "use-debounce"; import InfoIcon from "../../../assets/icons/InfoIcon"; -import { questionStore, updateQuestionsList } from "@root/questions"; - import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; + type SettingSliderProps = { - totalIndex: number; + question: QuizQuestionNumber; }; -export default function SettingSlider({ totalIndex }: SettingSliderProps) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); - const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber; - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); +export default function SettingSlider({ question }: SettingSliderProps) { + const theme = useTheme(); + const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); + const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); - return ( - - - - Настройки ползунка - - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, chooseRange: target.checked }, - }); - }} - /> - - - - Настройки вопросов - - { - updateQuestionsList(quizId, totalIndex, { - required: !e.target.checked, - }); - }} - /> + const setInnerName = useDebouncedCallback((value) => { + setQuestionInnerName(question.id, value); + }, 1000); + + return ( - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); - }} - /> - - - + > + + + Настройки ползунка + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.chooseRange = target.checked; + }); + }} + /> + + + + Настройки вопросов + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.required = !e.target.checked; + }); + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "number") return; + + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + }); + }} + /> + + + + + + + {question.content.innerNameCheck && ( + setInnerName(target.value)} + /> + )} - - {question.content.innerNameCheck && ( - debounced(target.value)} - /> - )} - - - ); + ); } diff --git a/src/pages/Questions/SliderOptions/switchSlider.tsx b/src/pages/Questions/SliderOptions/switchSlider.tsx index 54e450ec..069324b1 100644 --- a/src/pages/Questions/SliderOptions/switchSlider.tsx +++ b/src/pages/Questions/SliderOptions/switchSlider.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; -import HelpQuestions from "../helpQuestions"; +import { QuizQuestionNumber } from "@model/questionTypes/number"; import BranchingQuestions from "../branchingQuestions"; +import HelpQuestions from "../helpQuestions"; import SettingSlider from "./settingSlider"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionNumber; } export default function SwitchSlider({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/UploadFile/UploadFile.tsx b/src/pages/Questions/UploadFile/UploadFile.tsx index e6abb06e..a0342b7b 100644 --- a/src/pages/Questions/UploadFile/UploadFile.tsx +++ b/src/pages/Questions/UploadFile/UploadFile.tsx @@ -1,202 +1,199 @@ -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; import { - Box, - FormControl, - MenuItem, - Select, - SelectChangeEvent, - Typography, - Tooltip, - useMediaQuery, - useTheme, + Box, + FormControl, + MenuItem, + Select, + SelectChangeEvent, + Tooltip, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; -import ButtonsOptions from "../ButtonsOptions"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - -import InfoIcon from "../../../assets/icons/InfoIcon"; +import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; +import { useEffect, useState } from "react"; import ArrowDown from "../../../assets/icons/ArrowDownIcon"; +import InfoIcon from "../../../assets/icons/InfoIcon"; +import type { + QuizQuestionFile, + UploadFileType, +} from "../../../model/questionTypes/file"; +import ButtonsOptions from "../ButtonsOptions"; import SwitchUpload from "./switchUpload"; -import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; -import type { - QuizQuestionFile, - UploadFileType, -} from "../../../model/questionTypes/file"; - -interface Props { - totalIndex: number; -} type DesignItem = { - name: string; - value: UploadFileType; + name: string; + value: UploadFileType; }; const DESIGN_TYPES: DesignItem[] = [ - { name: "Все типы файлов", value: "all" }, - { name: "Изображения", value: "picture" }, - { name: "Видео", value: "video" }, - { name: "Аудио", value: "audio" }, - { name: "Документ", value: "document" }, + { name: "Все типы файлов", value: "all" }, + { name: "Изображения", value: "picture" }, + { name: "Видео", value: "video" }, + { name: "Аудио", value: "audio" }, + { name: "Документ", value: "document" }, ]; -export default function UploadFile({ totalIndex }: Props) { - const [switchState, setSwitchState] = useState("setting"); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); - const isTablet = useMediaQuery(theme.breakpoints.down(980)); - const question = listQuestions[quizId][totalIndex] as QuizQuestionFile; - const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - - const SSHC = (data: string) => { - setSwitchState(data); - }; - - const handleChange = ({ target }: SelectChangeEvent) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, type: target.value as UploadFileType }, - }); - }; - - useEffect(() => { - const isTypeSetted = DESIGN_TYPES.find( - ({ value }) => value === question.content.type - ); - - if (!isTypeSetted) { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, type: DESIGN_TYPES[0].value }, - }); - } - }, []); - - return ( - <> - - - - - - - - - Пользователь может загрузить любой собственный файл - - - - - - - - - - - - ); +interface Props { + question: QuizQuestionFile; +} + +export default function UploadFile({ question }: Props) { + const [switchState, setSwitchState] = useState("setting"); + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down(980)); + const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + + const SSHC = (data: string) => { + setSwitchState(data); + }; + + const handleChange = ({ target }: SelectChangeEvent) => { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "file") return; + + question.content.type = target.value as UploadFileType; + }); + }; + + useEffect(() => { + const isTypeSetted = DESIGN_TYPES.find( + ({ value }) => value === question.content.type + ); + + if (!isTypeSetted) { + updateQuestionWithFnOptimistic(question.id, question => { + if (question.type !== "file") return; + + question.content.type = DESIGN_TYPES[0].value; + }); + } + }, []); + + return ( + <> + + + + + + + + + Пользователь может загрузить любой собственный файл + + + + + + + + + + + + ); } diff --git a/src/pages/Questions/UploadFile/settingUpload.tsx b/src/pages/Questions/UploadFile/settingUpload.tsx index 77680e36..579ff12b 100644 --- a/src/pages/Questions/UploadFile/settingUpload.tsx +++ b/src/pages/Questions/UploadFile/settingUpload.tsx @@ -1,120 +1,110 @@ -import { useParams } from "react-router-dom"; import { - Box, - Typography, - useMediaQuery, - useTheme, - Tooltip, + Box, + Tooltip, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; +import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomTextField from "@ui_kit/CustomTextField"; import { useDebouncedCallback } from "use-debounce"; - -import { questionStore, updateQuestionsList } from "@root/questions"; - import InfoIcon from "../../../assets/icons/InfoIcon"; - import type { QuizQuestionFile } from "../../../model/questionTypes/file"; + type SettingsUploadProps = { - totalIndex: number; + question: QuizQuestionFile; }; -export default function SettingsUpload({ totalIndex }: SettingsUploadProps) { - const theme = useTheme(); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex] as QuizQuestionFile; - const debounced = useDebouncedCallback((value) => { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, innerName: value }, - }); - }, 1000); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); +export default function SettingsUpload({ question }: SettingsUploadProps) { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); - return ( - - Настройки вопроса - { - updateQuestionsList(quizId, totalIndex, { - content: { ...question.content, autofill: target.checked }, - }); - }} - /> - { - updateQuestionsList(quizId, totalIndex, { - required: !e.target.checked, - }); - }} - /> - - { - updateQuestionsList(quizId, totalIndex, { - content: { - ...question.content, - innerNameCheck: target.checked, - innerName: target.checked ? question.content.innerName : "", - }, - }); - }} - /> - { + setQuestionInnerName(question.id, value); + }, 1000); + + return ( + - - - - - - {question.content.innerNameCheck && ( - debounced(target.value)} - sx={{ paddingRight: "20px" }} - /> - )} - - ); + Настройки вопроса + { + updateQuestionWithFnOptimistic(question.id, question => { + question.content.autofill = target.checked; + }); + }} + /> + { + updateQuestionWithFnOptimistic(question.id, question => { + question.required = !e.target.checked; + }); + }} + /> + + { + updateQuestionWithFnOptimistic(question.id, question => { + question.content.innerNameCheck = target.checked; + question.content.innerName = target.checked ? question.content.innerName : ""; + }); + }} + /> + + + + + + + {question.content.innerNameCheck && ( + setInnerName(target.value)} + sx={{ paddingRight: "20px" }} + /> + )} + + ); } diff --git a/src/pages/Questions/UploadFile/switchUpload.tsx b/src/pages/Questions/UploadFile/switchUpload.tsx index 83d19b2c..8aef0129 100644 --- a/src/pages/Questions/UploadFile/switchUpload.tsx +++ b/src/pages/Questions/UploadFile/switchUpload.tsx @@ -1,28 +1,26 @@ -import * as React from "react"; +import { QuizQuestionFile } from "@model/questionTypes/file"; import BranchingQuestions from "../branchingQuestions"; import HelpQuestions from "../helpQuestions"; import SettingsUpload from "./settingUpload"; + interface Props { - switchState: string; - totalIndex: number; + switchState: string; + question: QuizQuestionFile; } export default function SwitchUpload({ - switchState = "setting", - totalIndex, + switchState = "setting", + question, }: Props) { - switch (switchState) { - case "setting": - return ; - break; - case "help": - return ; - break; - case "branching": - return ; - break; - default: - return <>; - } + switch (switchState) { + case "setting": + return ; + case "help": + return ; + case "branching": + return ; + default: + return <>; + } } diff --git a/src/pages/Questions/UploadImage/UploadImageModal.tsx b/src/pages/Questions/UploadImage/UploadImageModal.tsx index 730026ea..397a047b 100644 --- a/src/pages/Questions/UploadImage/UploadImageModal.tsx +++ b/src/pages/Questions/UploadImage/UploadImageModal.tsx @@ -14,19 +14,17 @@ import * as React from "react"; import UnsplashIcon from "../../../assets/icons/Unsplash.svg"; import type { DragEvent } from "react"; +import { closeImageUploadModal, useImageUploadModalStore } from "@root/imageUploadModal"; interface ModalkaProps { - open: boolean; - onClose: () => void; imgHC: (imgInp: FileList | null) => void; } export const UploadImageModal: React.FC = ({ - open, - onClose, imgHC, }) => { const theme = useTheme(); + const isOpen = useImageUploadModalStore(state => state.isOpen) const dropZone = React.useRef(null); const [ready, setReady] = React.useState(false); @@ -45,8 +43,8 @@ export const UploadImageModal: React.FC = ({ return ( diff --git a/src/pages/Questions/UploadImage/index.tsx b/src/pages/Questions/UploadImage/index.tsx index 88a2b9ee..44b52517 100644 --- a/src/pages/Questions/UploadImage/index.tsx +++ b/src/pages/Questions/UploadImage/index.tsx @@ -1,21 +1,21 @@ -import { QuizQuestionVariant } from "@model/questionTypes/variant"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { Box, ButtonBase, Typography, useTheme } from "@mui/material"; import { openCropModal } from "@root/cropModal"; +import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; import { setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions/actions"; import { CropModal } from "@ui_kit/Modal/CropModal"; import UploadBox from "@ui_kit/UploadBox"; -import { useState, type DragEvent } from "react"; +import { type DragEvent } from "react"; import UploadIcon from "../../../assets/icons/UploadIcon"; import { UploadImageModal } from "./UploadImageModal"; type UploadImageProps = { - question: QuizQuestionVariant; + question: AnyQuizQuestion; }; export default function UploadImage({ question }: UploadImageProps) { const theme = useTheme(); - const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false); const handleImageUpload = (files: FileList | null) => { if (!files?.length) return; @@ -26,7 +26,7 @@ export default function UploadImage({ question }: UploadImageProps) { setQuestionBackgroundImage(question.id, url); setQuestionOriginalBackgroundImage(question.id, url); - setIsUploadImageModalOpen(false); + closeImageUploadModal(); openCropModal(url, url); }; @@ -54,7 +54,7 @@ export default function UploadImage({ question }: UploadImageProps) { Загрузить изображение setIsUploadImageModalOpen(true)} + onClick={openImageUploadModal} sx={{ width: "100%", maxWidth: "260px", @@ -81,11 +81,7 @@ export default function UploadImage({ question }: UploadImageProps) { /> } - setIsUploadImageModalOpen(false)} - imgHC={handleImageUpload} - /> + ); diff --git a/src/pages/Questions/answerOptions/AnswerOptions.tsx b/src/pages/Questions/answerOptions/AnswerOptions.tsx index 66ab0cbd..8df98d4b 100755 --- a/src/pages/Questions/answerOptions/AnswerOptions.tsx +++ b/src/pages/Questions/answerOptions/AnswerOptions.tsx @@ -37,10 +37,7 @@ export default function AnswerOptions({ question }: Props) { Добавьте ответ ) : ( - + )} { - updateQuestionWithFnOptimistic(question.id, question => { - question.content.innerName = value; - }); + setQuestionInnerName(question.id, value); }, 1000); return ( diff --git a/src/pages/Questions/branchingQuestions.tsx b/src/pages/Questions/branchingQuestions.tsx index 2eee5fb8..7905f44b 100644 --- a/src/pages/Questions/branchingQuestions.tsx +++ b/src/pages/Questions/branchingQuestions.tsx @@ -1,6 +1,6 @@ import InfoIcon from "@icons/Info"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; -import { QuizQuestionVariant } from "@model/questionTypes/variant"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { Box, Button, @@ -24,7 +24,7 @@ import { Select } from "./Select"; type BranchingQuestionsProps = { - question: QuizQuestionVariant; + question: AnyQuizQuestion; }; const ACTIONS = ["Показать", "Скрыть"]; diff --git a/src/pages/Questions/helpQuestions.tsx b/src/pages/Questions/helpQuestions.tsx index 394a0d8a..c70c5608 100644 --- a/src/pages/Questions/helpQuestions.tsx +++ b/src/pages/Questions/helpQuestions.tsx @@ -1,4 +1,4 @@ -import { QuizQuestionVariant } from "@model/questionTypes/variant"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { Box, ButtonBase, Typography } from "@mui/material"; import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import CustomTextField from "@ui_kit/CustomTextField"; @@ -13,7 +13,7 @@ import { UploadVideoModal } from "./UploadVideoModal"; type BackgroundType = "text" | "video"; type HelpQuestionsProps = { - question: QuizQuestionVariant; + question: AnyQuizQuestion; }; export default function HelpQuestions({ question }: HelpQuestionsProps) { diff --git a/src/pages/Result/DescriptionForm/SwitchResult.tsx b/src/pages/Result/DescriptionForm/SwitchResult.tsx index 0ae4a74e..b92f518c 100644 --- a/src/pages/Result/DescriptionForm/SwitchResult.tsx +++ b/src/pages/Result/DescriptionForm/SwitchResult.tsx @@ -46,7 +46,8 @@ export default function SwitchResult({ return ; break; case "branching": - return ; + // return ; + return null break; case "points": return ; diff --git a/src/stores/imageUploadModal.ts b/src/stores/imageUploadModal.ts new file mode 100644 index 00000000..46215719 --- /dev/null +++ b/src/stores/imageUploadModal.ts @@ -0,0 +1,29 @@ +import { create } from "zustand"; +import { devtools, persist } from "zustand/middleware"; + + +type ImageUploadModalStore = { + isOpen: boolean; +}; + +const initialState: ImageUploadModalStore = { + isOpen: false, +}; + +export const useImageUploadModalStore = create()( + persist( + devtools( + () => initialState, + { + name: "ImageUploadModalStore", + } + ), + { + name: "ImageUploadModalStore", + } + ) +); + +export const openImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: true }); + +export const closeImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: false }); diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 9edba00a..152420cc 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -1,7 +1,7 @@ import { questionApi } from "@api/question"; import { devlog } from "@frontend/kitui"; import { questionToEditQuestionRequest } from "@model/question/edit"; -import { RawQuestion, rawQuestionToQuestion } from "@model/question/question"; +import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; @@ -11,44 +11,37 @@ import { QuestionsStore, useQuestionsStore } from "./store"; export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { - state.questionsById = {}; - if (questions === null) return; - - questions.forEach(question => state.questionsById[question.id] = rawQuestionToQuestion(question)); + state.questions = questions?.map(rawQuestionToQuestion) ?? []; }, { type: "setQuestions", questions, }); -export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => { - state.questionsById[question.id] = question; +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, }); -export const removeQuestion = (questionId: number) => setProducedState(state => { - delete state.questionsById[questionId]; +const removeQuestion = (questionId: number) => setProducedState(state => { + const index = state.questions.findIndex(q => q.id === questionId); + state.questions.splice(index, 1); }, { type: "removeQuestion", questionId, }); -export const setQuestionField = ( +const setQuestionField = ( questionId: number, field: T, value: AnyQuizQuestion[T], ) => setProducedState(state => { - const question = state.questionsById[questionId]; + const question = state.questions.find(q => q.id === questionId); if (!question) return; - const oldId = question.id; question[field] = value; - - if (field === "id") { - delete state.questionsById[oldId]; - state.questionsById[value as number] = question; - } }, { type: "setQuestionField", questionId, @@ -56,15 +49,31 @@ export const setQuestionField = ( value, }); +export const reorderQuestions = ( + sourceIndex: number, + destinationIndex: number, +) => { + if (sourceIndex === destinationIndex) return; + + setProducedState(state => { + const [removed] = state.questions.splice(sourceIndex, 1); + state.questions.splice(destinationIndex, 0, removed); + }); +}; + export const toggleExpandQuestion = (questionId: number) => setProducedState(state => { - const question = state.questionsById[questionId]; + const question = state.questions.find(q => q.id === questionId); if (!question) return; question.expanded = !question.expanded; }); +export const collapseAllQuestions = () => setProducedState(state => { + state.questions.forEach(question => question.expanded = false); +}); + export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => { - const question = state.questionsById[questionId]; + const question = state.questions.find(q => q.id === questionId); if (!question) return; question.openedModalSettings = !question.openedModalSettings; @@ -183,6 +192,84 @@ export const setQuestionOriginalBackgroundImage = ( }); }; +export const setVariantImageUrl = ( + questionId: number, + variantId: string, + url: string, +) => { + updateQuestionWithFnOptimistic(questionId, question => { + if (!("variants" in question.content)) return; + + const variant = question.content.variants.find(variant => variant.id === variantId); + if (!variant || !("originalImageUrl" in variant)) return; + + if (variant.extendedText === url) return; + + if (variant.extendedText !== variant.originalImageUrl) URL.revokeObjectURL(variant.extendedText); + variant.extendedText = url; + }); +}; + +export const setVariantOriginalImageUrl = ( + questionId: number, + variantId: string, + url: string, +) => { + updateQuestionWithFnOptimistic(questionId, question => { + if (!("variants" in question.content)) return; + + const variant = question.content.variants.find( + variant => variant.id === variantId + ) as ImageQuestionVariant | undefined; + if (!variant || !("originalImageUrl" in variant)) return; + + if (variant.originalImageUrl === url) return; + + URL.revokeObjectURL(variant.originalImageUrl); + variant.originalImageUrl = url; + }); +}; + +export const setPageQuestionPicture = ( + questionId: number, + url: string, +) => { + updateQuestionWithFnOptimistic(questionId, question => { + if (question.type !== "page") return; + + if (question.content.picture === url) return; + + if ( + question.content.picture !== question.content.originalPicture + ) URL.revokeObjectURL(question.content.picture); + question.content.picture = url; + }); +}; + +export const setPageQuestionOriginalPicture = ( + questionId: number, + url: string, +) => { + updateQuestionWithFnOptimistic(questionId, question => { + if (question.type !== "page") return; + + if (question.content.originalPicture === url) return; + + URL.revokeObjectURL(question.content.originalPicture); + question.content.originalPicture = url; + }); +}; + +export const setQuestionInnerName = ( + questionId: number, + name: string, +) => { + updateQuestionWithFnOptimistic(questionId, question => { + question.content.innerName = name; + }); +}; + + let savedOriginalQuestion: AnyQuizQuestion | null = null; let controller: AbortController | null = null; @@ -191,7 +278,7 @@ export const updateQuestionWithFnOptimistic = async ( questionId: number, updateFn: (question: AnyQuizQuestion) => void, ) => { - const question = useQuestionsStore.getState().questionsById[questionId] ?? null; + const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); if (!question) return; const currentUpdatedQuestion = produce(question, updateFn); @@ -228,10 +315,11 @@ export const updateQuestionWithFnOptimistic = async ( } }; -export const createQuestion = async (quizId: number) => { +export const createQuestion = async (quizId: number, type: QuestionType = "variant") => { try { const question = await questionApi.create({ quiz_id: quizId, + type, }); setQuestion(rawQuestionToQuestion(question)); @@ -257,10 +345,12 @@ export const copyQuestion = async (questionId: number, quizId: number) => { const { updated: newQuestionId } = await questionApi.copy(questionId, quizId); setProducedState(state => { - const question = state.questionsById[questionId]; + const question = state.questions.find(q => q.id === questionId); if (!question) return; - state.questionsById[newQuestionId] = question; + const copiedQuestion = structuredClone(question); + copiedQuestion.id = newQuestionId; + state.questions.push(copiedQuestion); }, { type: "copyQuestion", questionId, diff --git a/src/stores/questions/hooks.ts b/src/stores/questions/hooks.ts deleted file mode 100644 index b1bb321c..00000000 --- a/src/stores/questions/hooks.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useQuestionsStore } from "./store"; - - -export function useQuestionArray() { - const questions = useQuestionsStore(state => state.questionsById); - - return Object.values(questions).flatMap(question => question ? [question] : []); -} diff --git a/src/stores/questions/store.ts b/src/stores/questions/store.ts index f543f641..347b7e08 100644 --- a/src/stores/questions/store.ts +++ b/src/stores/questions/store.ts @@ -4,11 +4,11 @@ import { devtools } from "zustand/middleware"; export type QuestionsStore = { - questionsById: Record; + questions: AnyQuizQuestion[]; }; const initialState: QuestionsStore = { - questionsById: {}, + questions: [], }; export const useQuestionsStore = create()(