WIP use new store & resolve type conflicts

This commit is contained in:
nflnkr 2023-11-16 19:41:25 +03:00
parent 2103fe8977
commit f463270a9b
56 changed files with 3734 additions and 3939 deletions

@ -1,4 +1,4 @@
import { DefiniteQuestionType } from "@model/questionTypes/shared"; import { QuestionType } from "./question";
export interface CreateQuestionRequest { export interface CreateQuestionRequest {
@ -9,7 +9,7 @@ export interface CreateQuestionRequest {
/** description of question. html/text */ /** description of question. html/text */
description?: string; description?: string;
/** type of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */ /** 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 */ /** set true if user MUST answer this question */
required?: boolean; required?: boolean;
/** page of question */ /** page of question */

@ -86,6 +86,13 @@ export type AnyQuizQuestion =
| QuizQuestionRating; | QuizQuestionRating;
// | QuizQuestionInitial; // | QuizQuestionInitial;
type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[] | ImageQuestionVariant[]; };
} ? T : never;
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyQuizQuestion>;
export const createQuestionVariant: () => QuestionVariant = () => ({ export const createQuestionVariant: () => QuestionVariant = () => ({
id: nanoid(), id: nanoid(),
answer: "", answer: "",

@ -1,21 +1,19 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { DragDropContext, Droppable } from "react-beautiful-dnd"; import { reorderQuestionVariants } from "@root/questions/actions";
import { AnswerItem } from "./AnswerItem";
import { type ReactNode } from "react"; import { type ReactNode } from "react";
import type { DropResult } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd";
import type { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared"; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { reorderQuestionVariants } from "@root/questions/actions"; import type { ImageQuestionVariant, QuestionVariant, QuizQuestionsWithVariants } from "../../../model/questionTypes/shared";
import { AnswerItem } from "./AnswerItem";
type AnswerDraggableListProps = { type AnswerDraggableListProps = {
variants: QuestionVariant[]; question: QuizQuestionsWithVariants;
question: AnyQuizQuestion;
additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
}; };
export const AnswerDraggableList = ({ export const AnswerDraggableList = ({
variants,
question, question,
additionalContent, additionalContent,
additionalMobile, additionalMobile,
@ -31,7 +29,7 @@ export const AnswerDraggableList = ({
<Droppable droppableId="droppable-answer-list"> <Droppable droppableId="droppable-answer-list">
{(provided) => ( {(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{variants.map((variant, index) => ( {question.content.variants.map((variant, index) => (
<AnswerItem <AnswerItem
key={variant.id} key={variant.id}
index={index} index={index}

@ -1,73 +1,48 @@
import { useState, useEffect } from "react";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { useParams } from "react-router-dom";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import Clue from "../../assets/icons/questionsPage/clue";
import Branching from "../../assets/icons/questionsPage/branching";
import {
Box,
Typography,
Tooltip,
IconButton,
useTheme,
useMediaQuery,
} from "@mui/material";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import {
questionStore,
copyQuestion,
removeQuestionForce,
updateQuestionsList,
removeQuestion,
} from "@root/questions";
import { quizStore } from "@root/quizes";
import { DoubleTick } from "@icons/questionsPage/DoubleTick";
import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight"; import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight";
import { DoubleTick } from "@icons/questionsPage/DoubleTick";
import { VectorQuestions } from "@icons/questionsPage/VectorQuestions"; import { VectorQuestions } from "@icons/questionsPage/VectorQuestions";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { SxProps } from "@mui/material"; import type { SxProps } from "@mui/material";
import type { QuizQuestionBase } from "../../model/questionTypes/shared"; import {
Box,
IconButton,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, deleteQuestion, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching";
import Clue from "../../assets/icons/questionsPage/clue";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import type { AnyQuizQuestion } from "../../model/questionTypes/shared";
interface Props { interface Props {
switchState: string; switchState: string;
SSHC: (data: string) => void; SSHC: (data: string) => void;
totalIndex: number; question: AnyQuizQuestion;
sx?: SxProps; sx?: SxProps;
} }
export default function ButtonsOptions({ export default function ButtonsOptions({
SSHC, SSHC,
switchState, switchState,
totalIndex, question,
}: Props) { }: Props) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const { listQuizes } = quizStore();
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
useState<boolean>(false);
const quize = listQuizes[quizId];
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
useEffect(() => {
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [listQuestions]);
const openedModal = () => {
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
openedModalSettings: true,
});
};
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const openedModal = () => {
updateQuestionWithFnOptimistic(question.id, question => {
question.openedModalSettings = true;
});
};
const buttonSetting: { const buttonSetting: {
icon: JSX.Element; icon: JSX.Element;
title: string; title: string;
@ -237,7 +212,7 @@ export default function ButtonsOptions({
))} ))}
<> <>
<MiniButtonSetting <MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)} onClick={undefined} // TODO
sx={{ sx={{
minWidth: "30px", minWidth: "30px",
height: "30px", height: "30px",
@ -247,7 +222,7 @@ export default function ButtonsOptions({
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} /> <DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting> </MiniButtonSetting>
<MiniButtonSetting <MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)} onClick={undefined} // TODO
sx={{ sx={{
minWidth: "30px", minWidth: "30px",
height: "30px", height: "30px",
@ -257,7 +232,7 @@ export default function ButtonsOptions({
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} /> <DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting> </MiniButtonSetting>
<MiniButtonSetting <MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)} onClick={undefined} // TODO
sx={{ sx={{
minWidth: "30px", minWidth: "30px",
height: "30px", height: "30px",
@ -279,28 +254,30 @@ export default function ButtonsOptions({
</IconButton> </IconButton>
<IconButton <IconButton
sx={{ borderRadius: "6px", padding: "2px" }} sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => copyQuestion(quizId, totalIndex)} onClick={() => copyQuestion(question.id, question.quizId)}
> >
<CopyIcon color={"#4D4D4D"} /> <CopyIcon color={"#4D4D4D"} />
</IconButton> </IconButton>
<IconButton <IconButton
sx={{ borderRadius: "6px", padding: "2px" }} sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { onClick={() => { // TODO
const removedId = question.id; // const removedId = question.id;
if (question.deleteTimeoutId) { // if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId); // clearTimeout(question.deleteTimeoutId);
} // }
removeQuestion(quizId, totalIndex); // removeQuestion(quizId, totalIndex);
const newTimeoutId = window.setTimeout(() => { // const newTimeoutId = window.setTimeout(() => {
removeQuestionForce(quizId, removedId); // removeQuestionForce(quizId, removedId);
}, 5000); // }, 5000);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, { // updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
...question, // ...question,
deleteTimeoutId: newTimeoutId, // deleteTimeoutId: newTimeoutId,
}); // });
deleteQuestion(question.id);
}} }}
> >
<DeleteIcon color={"#4D4D4D"} /> <DeleteIcon color={"#4D4D4D"} />

@ -1,6 +1,7 @@
import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight"; import { DoubleArrowRight } from "@icons/questionsPage/DoubleArrowRight";
import { DoubleTick } from "@icons/questionsPage/DoubleTick"; import { DoubleTick } from "@icons/questionsPage/DoubleTick";
import { VectorQuestions } from "@icons/questionsPage/VectorQuestions"; import { VectorQuestions } from "@icons/questionsPage/VectorQuestions";
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import { import {
Box, Box,
IconButton, IconButton,
@ -20,13 +21,13 @@ import { DeleteIcon } from "../../assets/icons/questionsPage/deleteIcon";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon"; import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import ImgIcon from "../../assets/icons/questionsPage/imgIcon"; import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon"; import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import type { AnyQuizQuestion } from "../../model/questionTypes/shared"; import { QuizQuestionVariant } from "@model/questionTypes/variant";
interface Props { interface Props {
switchState: string; switchState: string;
SSHC: (data: string) => void; SSHC: (data: string) => void;
question: AnyQuizQuestion; question: QuizQuestionVariant | QuizQuestionVarImg;
} }
export default function ButtonsOptionsAndPict({ export default function ButtonsOptionsAndPict({

@ -3,13 +3,14 @@ import { useState } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import SwitchData from "./switchData"; import SwitchData from "./switchData";
import { QuizQuestionDate } from "@model/questionTypes/date";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionDate;
} }
export default function DataOptions({ totalIndex }: Props) { export default function DataOptions({ question }: Props) {
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -49,8 +50,8 @@ export default function DataOptions({ totalIndex }: Props) {
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} /> <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchData switchState={switchState} totalIndex={totalIndex} /> <SwitchData switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,30 +1,24 @@
import { useParams } from "react-router-dom"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionDate } from "../../../model/questionTypes/date"; import type { QuizQuestionDate } from "../../../model/questionTypes/date";
type SettingsDataProps = { type SettingsDataProps = {
totalIndex: number; question: QuizQuestionDate;
}; };
export default function SettingsData({ totalIndex }: SettingsDataProps) { export default function SettingsData({ question }: SettingsDataProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionDate;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
return ( return (
@ -54,8 +48,10 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
label={"Выбор диапазона дат"} label={"Выбор диапазона дат"}
checked={question.content.dateRange} checked={question.content.dateRange}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, dateRange: target.checked }, if (question.type !== "date") return;
question.content.dateRange = target.checked;
}); });
}} }}
/> />
@ -64,8 +60,10 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
label={"Выбор времени"} label={"Выбор времени"}
checked={question.content.time} checked={question.content.time}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, time: target.checked }, if (question.type !== "date") return;
question.content.time = target.checked;
}); });
}} }}
/> />
@ -90,8 +88,8 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !target.checked, question.required = !target.checked;
}); });
}} }}
/> />
@ -111,12 +109,9 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionDate>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { question.content.innerNameCheck = target.checked;
...question.content, question.content.innerName = target.checked ? question.content.innerName : "";
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
}); });
}} }}
/> />
@ -130,7 +125,7 @@ export default function SettingsData({ totalIndex }: SettingsDataProps) {
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
/> />
)} )}
</Box> </Box>

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionDate } from "@model/questionTypes/date";
import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingData from "./settingData"; import SettingData from "./settingData";
import BranchingQuestions from "../branchingQuestions";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionDate;
} }
export default function SwitchData({ export default function SwitchData({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingData totalIndex={totalIndex} />; return <SettingData question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,31 +1,22 @@
import { useState } from "react";
import { import {
Box, Box,
Typography,
Popper,
Grow,
Paper,
MenuList,
MenuItem,
ClickAwayListener,
Modal,
Button, Button,
ClickAwayListener,
Grow,
MenuItem,
MenuList,
Modal,
Paper,
Popper,
Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react";
import {
updateQuestionsList,
removeQuestionForce,
createQuestion,
} from "@root/questions";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import type { RefObject } from "react"; import type { RefObject } from "react";
import type { import type { AnyQuizQuestion } from "../../../model/questionTypes/shared";
QuizQuestionType, import { QuestionType } from "@model/question/question";
QuizQuestionBase,
AnyQuizQuestion,
} from "../../../model/questionTypes/shared";
type ChooseAnswerModalProps = { type ChooseAnswerModalProps = {
open: boolean; open: boolean;
@ -43,7 +34,7 @@ export const ChooseAnswerModal = ({
switchState, switchState,
}: ChooseAnswerModalProps) => { }: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false); const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuizQuestionType>("text"); const [selectedValue, setSelectedValue] = useState<QuestionType>("text");
const theme = useTheme(); const theme = useTheme();
return ( return (

@ -31,7 +31,7 @@ import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating"; import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider"; import Slider from "@icons/questionsPage/slider";
import ExpandLessIcon from "@mui/icons-material/ExpandLess"; 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 type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; import type { AnyQuizQuestion } from "../../../model/questionTypes/shared";
@ -307,7 +307,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
}} }}
> >
<Box <Box
onClick={() => createQuestion(quizId, "nonselected", totalIndex + 1)} onClick={() => createQuestion(question.quizId)}
sx={{ sx={{
display: plusVisible && !isDragging ? "flex" : "none", display: plusVisible && !isDragging ? "flex" : "none",
width: "100%", width: "100%",

@ -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 { questionApi } from "@api/question";
import { setQuestions } from "@root/questions/actions";
import { isAxiosError } from "axios";
import { devlog } from "@frontend/kitui"; 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 { 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 = () => { export const DraggableList = () => {
@ -23,18 +23,10 @@ export const DraggableList = () => {
enqueueSnackbar(`Не удалось получить вопросы. ${message}`); enqueueSnackbar(`Не удалось получить вопросы. ${message}`);
} }
}); });
const questions = useQuestionArray(); const questions = useQuestionsStore(state => state.questions);
const onDragEnd = ({ destination, source }: DropResult) => { // TODO const onDragEnd = ({ destination, source }: DropResult) => {
// if (destination) { if (destination) reorderQuestions(source.index, destination.index);
// const newItems = reorder(
// listQuestions[quizId],
// source.index,
// destination.index
// );
// updateQuestionsListDragAndDrop(quizId, newItems);
// }
}; };
return ( return (

@ -1,41 +1,26 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom";
import { Box, Typography, Link, useTheme, useMediaQuery } from "@mui/material"; import { Box, Typography, Link, useTheme, useMediaQuery } from "@mui/material";
import { AnswerDraggableList } from "../AnswerDraggableList"; import { AnswerDraggableList } from "../AnswerDraggableList";
import { questionStore, updateQuestionsList } from "@root/questions";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import SwitchDropDown from "./switchDropDown"; import SwitchDropDown from "./switchDropDown";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import { addQuestionVariant } from "@root/questions/actions";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionSelect;
} }
export default function DropDown({ totalIndex }: Props) { export default function DropDown({ question }: Props) {
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionSelect;
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
const addNewAnswer = () => {
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", extendedText: "", hints: "" });
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
return ( return (
<> <>
<Box <Box
@ -56,10 +41,7 @@ export default function DropDown({ totalIndex }: Props) {
Добавьте ответ Добавьте ответ
</Typography> </Typography>
) : ( ) : (
<AnswerDraggableList <AnswerDraggableList question={question} />
variants={question.content.variants}
question={totalIndex}
/>
)} )}
<Box <Box
sx={{ sx={{
@ -78,7 +60,7 @@ export default function DropDown({ totalIndex }: Props) {
mr: "4px", mr: "4px",
height: "19px", height: "19px",
}} }}
onClick={addNewAnswer} onClick={() => addQuestionVariant(question.id)}
> >
Добавьте ответ Добавьте ответ
</Link> </Link>
@ -108,9 +90,9 @@ export default function DropDown({ totalIndex }: Props) {
<ButtonsOptions <ButtonsOptions
switchState={switchState} switchState={switchState}
SSHC={SSHC} SSHC={SSHC}
totalIndex={totalIndex} question={question}
/> />
<SwitchDropDown switchState={switchState} totalIndex={totalIndex} /> <SwitchDropDown switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,43 +1,36 @@
import { useParams } from "react-router-dom";
import { import {
Box, Box,
Typography,
Tooltip, Tooltip,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
type SettingDropDownProps = { type SettingDropDownProps = {
totalIndex: number; question: QuizQuestionSelect;
}; };
export default function SettingDropDown({ totalIndex }: SettingDropDownProps) { export default function SettingDropDown({ question }: SettingDropDownProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); 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 QuizQuestionSelect;
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
const debounceAnswer = useDebouncedCallback((value) => { const debounceAnswer = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, default: value }, if (question.type !== "select") return;
question.content.default = value;
}); });
}, 1000); }, 1000);
@ -80,8 +73,10 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, multi: target.checked }, if (question.type !== "select") return;
question.content.multi = target.checked;
}) })
} }
/> />
@ -135,8 +130,8 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !e.target.checked, question.required = !e.target.checked;
}); });
}} }}
/> />
@ -146,12 +141,9 @@ export default function SettingDropDown({ totalIndex }: SettingDropDownProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { question.content.innerNameCheck = target.checked;
...question.content, question.content.innerName = target.checked ? question.content.innerName : "";
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
}); });
}} }}
/> />

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionSelect } from "@model/questionTypes/select";
import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingDropDown from "./settingDropDown"; import SettingDropDown from "./settingDropDown";
import BranchingQuestions from "../branchingQuestions";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionSelect;
} }
export default function SwitchDropDown({ export default function SwitchDropDown({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingDropDown totalIndex={totalIndex} />; return <SettingDropDown question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,41 +1,36 @@
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 { EmojiIcons } from "@icons/EmojiIocns";
import AddEmoji from "@icons/questionsPage/addEmoji"; import AddEmoji from "@icons/questionsPage/addEmoji";
import PlusImage from "@icons/questionsPage/plus"; import PlusImage from "@icons/questionsPage/plus";
import {
import { questionStore, updateQuestionsList } from "@root/questions"; 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 type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../ButtonsOptions";
import SwitchEmoji from "./switchEmoji";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionEmoji;
} }
export default function Emoji({ totalIndex }: Props) { export default function Emoji({ question }: Props) {
const [switchState, setSwitchState] = useState<string>("setting"); const [switchState, setSwitchState] = useState<string>("setting");
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>( const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(
null null
); );
const [currentIndex, setCurrentIndex] = useState<number>(0); const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
const { listQuestions } = questionStore();
const quizId = Number(useParams().quizId);
const theme = useTheme(); const theme = useTheme();
const question = listQuestions[quizId][totalIndex] as QuizQuestionEmoji;
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
@ -47,9 +42,8 @@ export default function Emoji({ totalIndex }: Props) {
<> <>
<Box sx={{ padding: "20px" }}> <Box sx={{ padding: "20px" }}>
<AnswerDraggableList <AnswerDraggableList
variants={question.content.variants} question={question}
question={totalIndex} additionalContent={(variant) => (
additionalContent={(variant, index) => (
<> <>
{!isTablet && ( {!isTablet && (
<Box sx={{ cursor: "pointer" }}> <Box sx={{ cursor: "pointer" }}>
@ -57,7 +51,7 @@ export default function Emoji({ totalIndex }: Props) {
data-cy="choose-emoji-button" data-cy="choose-emoji-button"
onClick={({ currentTarget }) => { onClick={({ currentTarget }) => {
setAnchorElement(currentTarget); setAnchorElement(currentTarget);
setCurrentIndex(index); setSelectedVariant(variant.id);
setOpen(true); setOpen(true);
}} }}
> >
@ -104,13 +98,13 @@ export default function Emoji({ totalIndex }: Props) {
)} )}
</> </>
)} )}
additionalMobile={(variant, index) => ( additionalMobile={(variant) => (
<> <>
{isTablet && ( {isTablet && (
<Box <Box
onClick={({ currentTarget }) => { onClick={({ currentTarget }) => {
setAnchorElement(currentTarget); setAnchorElement(currentTarget);
setCurrentIndex(index); setSelectedVariant(variant.id);
setOpen(true); setOpen(true);
}} }}
sx={{ sx={{
@ -187,15 +181,13 @@ export default function Emoji({ totalIndex }: Props) {
<EmojiPicker <EmojiPicker
onEmojiSelect={({ native }) => { onEmojiSelect={({ native }) => {
setOpen(false); setOpen(false);
const cloneVariants = [...question.content.variants]; updateQuestionWithFnOptimistic(question.id, question => {
if (question.type !== "emoji") return;
cloneVariants[currentIndex] = { const variant = question.content.variants.find(v => v.id === selectedVariant);
...cloneVariants[currentIndex], if (!variant) return;
extendedText: native,
};
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { variant.extendedText = native;
content: { ...question.content, variants: cloneVariants },
}); });
}} }}
/> />
@ -212,14 +204,7 @@ export default function Emoji({ totalIndex }: Props) {
component="button" component="button"
variant="body2" variant="body2"
sx={{ color: theme.palette.brightPurple.main }} sx={{ color: theme.palette.brightPurple.main }}
onClick={() => { onClick={() => addQuestionVariant(question.id)}
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", extendedText: "", hints: "" });
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
}}
> >
Добавьте ответ Добавьте ответ
</Link> </Link>
@ -249,9 +234,9 @@ export default function Emoji({ totalIndex }: Props) {
<ButtonsOptions <ButtonsOptions
switchState={switchState} switchState={switchState}
SSHC={SSHC} SSHC={SSHC}
totalIndex={totalIndex} question={question}
/> />
<SwitchEmoji switchState={switchState} totalIndex={totalIndex} /> <SwitchEmoji switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,31 +1,25 @@
import { useParams } from "react-router-dom"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
type SettingEmojiProps = { type SettingEmojiProps = {
totalIndex: number; question: QuizQuestionEmoji;
}; };
export default function SettingEmoji({ totalIndex }: SettingEmojiProps) { export default function SettingEmoji({ question }: SettingEmojiProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isTablet = useMediaQuery(theme.breakpoints.down(985)); const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); 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) => { const setInnerName = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
return ( return (
@ -56,21 +50,21 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
label={"Можно несколько"} label={"Можно несколько"}
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { if (question.type !== "emoji") return;
content: { ...question.content, multi: target.checked },
}); question.content.multi = target.checked;
}} })}
/> />
<CustomCheckbox <CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { if (question.type !== "emoji") return;
content: { ...question.content, own: target.checked },
}); question.content.own = target.checked;
}} })}
/> />
</Box> </Box>
<Box <Box
@ -92,11 +86,11 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { if (question.type !== "emoji") return;
required: !e.target.checked,
}); question.content.required = !e.target.checked;
}} })}
/> />
<Box <Box
sx={{ sx={{
@ -113,15 +107,10 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, { question.content.innerNameCheck = target.checked;
content: { question.content.innerName = target.checked ? question.content.innerName : "";
...question.content, })}
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
});
}}
/> />
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top"> <Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Box> <Box>
@ -133,7 +122,7 @@ export default function SettingEmoji({ totalIndex }: SettingEmojiProps) {
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
/> />
)} )}
</Box> </Box>

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingEmoji from "./settingEmoji"; import SettingEmoji from "./settingEmoji";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionEmoji;
} }
export default function SwitchEmoji({ export default function SwitchEmoji({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingEmoji totalIndex={totalIndex} />; return <SettingEmoji question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -76,9 +76,8 @@ export default memo(
> >
<QuestionsPageCard <QuestionsPageCard
key={index} key={index}
totalIndex={index} question={question}
draggableProps={provided.dragHandleProps} draggableProps={provided.dragHandleProps}
isDragging={isDragging}
/> />
</Box> </Box>
)} )}

@ -1,36 +1,115 @@
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 { PointsIcon } from "@icons/questionsPage/PointsIcon";
import Answer from "@icons/questionsPage/answer"; import Answer from "@icons/questionsPage/answer";
import OptionsPict from "@icons/questionsPage/options_pict"; import AnswerGroup from "@icons/questionsPage/answerGroup";
import OptionsAndPict from "@icons/questionsPage/options_and_pict"; 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 Emoji from "@icons/questionsPage/emoji";
import Input from "@icons/questionsPage/input"; import Input from "@icons/questionsPage/input";
import DropDown from "@icons/questionsPage/drop_down"; import OptionsAndPict from "@icons/questionsPage/options_and_pict";
import Date from "@icons/questionsPage/date"; import OptionsPict from "@icons/questionsPage/options_pict";
import Slider from "@icons/questionsPage/slider";
import Download from "@icons/questionsPage/download";
import Page from "@icons/questionsPage/page"; import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating"; 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 { 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 { interface Props {
totalIndex: number; question: AnyQuizQuestion;
draggableProps: DraggableProvidedDragHandleProps | null | undefined; draggableProps: DraggableProvidedDragHandleProps | null | undefined;
isDragging: boolean; }
export default function QuestionsPageCard({
question,
draggableProps,
}: Props) {
const [open, setOpen] = useState<boolean>(false);
const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => {
updateQuestionWithFnOptimistic(question.id, question => {
question.title = title;
});
}, 1000);
return (
<>
<Paper
sx={{
overflow: "hidden",
maxWidth: "796px",
width: "100%",
backgroundColor: "white",
border: "none",
boxShadow: "none",
paddingBottom: "20px",
borderRadius: "0",
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: 0,
}}
>
<CustomTextField
placeholder={`Заголовок ${totalIndex + 1} вопроса`}
text={question.title}
onChange={({ target }) => setTitle(target.value)}
sx={{ margin: "20px", width: "auto" }}
InputProps={{
startAdornment: (
<Box>
<InputAdornment
ref={anchorRef}
position="start"
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(question.type)}
</InputAdornment>
<ChooseAnswerModal
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
question={question}
switchState={question.type}
/>
</Box>
),
endAdornment: (
<Box {...draggableProps}>
{totalIndex !== 0 && (
<InputAdornment position="start">
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
</InputAdornment>
)}
</Box>
),
}}
/>
{/* {question.type === "" ? (
<FormTypeQuestions totalIndex={totalIndex} />
) : ( */}
<SwitchQuestionsPage question={question} />
{/* )} */}
</Box>
</Paper>
</>
);
} }
const IconAndrom = (switchState: string) => { const IconAndrom = (switchState: string) => {
@ -76,95 +155,3 @@ const IconAndrom = (switchState: string) => {
); );
} }
}; };
export default function QuestionsPageCard({
totalIndex,
draggableProps,
isDragging,
}: Props) {
const [open, setOpen] = useState<boolean>(false);
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex];
const anchorRef = useRef(null);
const debounced = useDebouncedCallback((title) => {
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, { title });
}, 1000);
useEffect(() => {
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
}
}, [question]);
return (
<>
<Paper
id={String(totalIndex)}
sx={{
overflow: "hidden",
maxWidth: "796px",
width: "100%",
backgroundColor: "white",
border: "none",
boxShadow: "none",
paddingBottom: "20px",
borderRadius: "0",
borderTopLeftRadius: "12px",
borderTopRightRadius: "12px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: 0,
}}
>
<CustomTextField
placeholder={`Заголовок ${totalIndex + 1} вопроса`}
text={question.title}
onChange={({ target }) => debounced(target.value)}
sx={{ margin: "20px", width: "auto" }}
InputProps={{
startAdornment: (
<Box>
<InputAdornment
ref={anchorRef}
position="start"
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(question.type)}
</InputAdornment>
<ChooseAnswerModal
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
totalIndex={totalIndex}
switchState={question.type}
/>
</Box>
),
endAdornment: (
<Box {...draggableProps}>
{totalIndex !== 0 && (
<InputAdornment position="start">
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
</InputAdornment>
)}
</Box>
),
}}
/>
{/* {question.type === "" ? (
<FormTypeQuestions totalIndex={totalIndex} />
) : ( */}
<SwitchQuestionsPage totalIndex={totalIndex} />
{/* )} */}
</Box>
</Paper>
</>
);
}

@ -1,11 +0,0 @@
export const reorder = <T>(
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;
};

@ -1,33 +1,16 @@
import { useParams } from "react-router-dom";
import { Box } from "@mui/material"; 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 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 = () => { export const FormDraggableList = () => {
const quizId = Number(useParams().quizId); const questions = useQuestionsStore(state => state.questions);
const { listQuestions } = questionStore();
const onDragEnd = ({ destination, source }: DropResult) => { const onDragEnd = ({ destination, source }: DropResult) => {
if (destination?.index === 0) { if (destination) reorderQuestions(source.index, destination.index);
return;
}
if (destination) {
const newItems = reorder(
listQuestions[quizId],
source.index,
destination.index
);
updateQuestionsListDragAndDrop(quizId, newItems);
}
}; };
return ( return (
@ -35,7 +18,7 @@ export const FormDraggableList = () => {
<Droppable droppableId="droppable-list"> <Droppable droppableId="droppable-list">
{(provided, snapshot) => ( {(provided, snapshot) => (
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{listQuestions[quizId]?.map((question, index) => ( {questions.map((question, index) => (
<FormDraggableListItem <FormDraggableListItem
key={index} key={index}
index={index} index={index}

@ -1,43 +1,19 @@
import { Box, Button, Typography, useTheme } from "@mui/material"; import { Box, Button, Typography, useTheme } from "@mui/material";
import { useParams } from "react-router-dom"; import { incrementCurrentStep } from "@root/quizes/actions";
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 QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom"; 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() { 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 collapseEverything = () => {
listQuestions[quizId].forEach((item, index) => {
updateQuestionsList<AnyQuizQuestion>(quizId, index, {
...item,
expanded: false,
});
});
};
const theme = useTheme(); const theme = useTheme();
const { quiz } = useCurrentQuiz();
if (!quiz) return null;
return ( return (
<> <>
@ -60,7 +36,7 @@ export default function FormQuestionsPage() {
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main, textDecorationColor: theme.palette.brightPurple.main,
}} }}
onClick={collapseEverything} onClick={collapseAllQuestions}
> >
Свернуть всё Свернуть всё
</Button> </Button>
@ -92,7 +68,7 @@ export default function FormQuestionsPage() {
}, },
}} }}
onClick={() => { onClick={() => {
createQuestion(quizId); createQuestion(quiz.id);
}} }}
> >
<AddAnswer color="#EEE4FC" /> <AddAnswer color="#EEE4FC" />
@ -124,7 +100,7 @@ export default function FormQuestionsPage() {
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
fontSize: "18px", fontSize: "18px",
}} }}
onClick={handleNext} onClick={incrementCurrentStep}
> >
Следующий шаг Следующий шаг
</Button> </Button>

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
@ -14,21 +13,12 @@ import Date from "../../../assets/icons/questionsPage/date";
import Slider from "../../../assets/icons/questionsPage/slider"; import Slider from "../../../assets/icons/questionsPage/slider";
import Download from "../../../assets/icons/questionsPage/download"; import Download from "../../../assets/icons/questionsPage/download";
import {
questionStore,
updateQuestionsList,
createQuestion,
removeQuestionForce,
} from "@root/questions";
import type { import type {
QuizQuestionBase, AnyQuizQuestion,
} from "../../../model/questionTypes/shared"; } from "../../../model/questionTypes/shared";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
interface Props {
totalIndex: number;
}
type ButtonTypeQuestion = { type ButtonTypeQuestion = {
icon: JSX.Element; 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 [switchState, setSwitchState] = useState("");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
return ( return (
<Box> <Box>
@ -85,21 +76,16 @@ export default function FormTypeQuestions({ totalIndex }: Props) {
margin: "20px", margin: "20px",
}} }}
> >
{(totalIndex === 0 {(true /* TODO какое-то непонятное условие */
? BUTTON_TYPE_QUESTIONS ? BUTTON_TYPE_QUESTIONS
: BUTTON_TYPE_SHORT_QUESTIONS : BUTTON_TYPE_SHORT_QUESTIONS
).map(({ icon, title, value }) => ( ).map(({ icon, title, value: questionType }) => (
<QuestionsMiniButton <QuestionsMiniButton
key={title} key={title}
onClick={() => { onClick={() => {
const clonedQuestion = { ...question }; updateQuestionWithFnOptimistic(question.id, question => {
question.type = questionType;
removeQuestionForce(quizId, clonedQuestion.id); })
createQuestion(quizId, value, totalIndex);
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
expanded: clonedQuestion.expanded,
type: value,
});
}} }}
icon={icon} icon={icon}
text={title} text={title}
@ -109,9 +95,10 @@ export default function FormTypeQuestions({ totalIndex }: Props) {
<ButtonsOptions <ButtonsOptions
switchState={switchState} switchState={switchState}
SSHC={setSwitchState} SSHC={setSwitchState}
totalIndex={totalIndex} question={question}
/> />
<SwitchAnswerOptions switchState={switchState} totalIndex={totalIndex} /> {/* TODO конфликт типов */}
{/* <SwitchAnswerOptions switchState={switchState} question={question} /> */}
</Box> </Box>
); );
} }

@ -1,78 +1,72 @@
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 { ImageAddIcons } from "@icons/ImageAddIcons";
import { MessageIcon } from "@icons/messagIcon"; import { MessageIcon } from "@icons/messagIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon"; import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; 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 { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict"; import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
import { UploadImageModal } from "../UploadImage/UploadImageModal";
import SwitchOptionsAndPict from "./switchOptionsAndPict"; import SwitchOptionsAndPict from "./switchOptionsAndPict";
import { openCropModal } from "@root/cropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionVarImg;
} }
export default function OptionsAndPicture({ totalIndex }: Props) { export default function OptionsAndPicture({ question }: Props) {
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const [currentIndex, setCurrentIndex] = useState<number>(0); const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg;
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
const handleImageUpload = (files: FileList | null) => { const handleImageUpload = (files: FileList | null) => {
if (!files?.length) return; if (!files?.length || !selectedVariantId) return;
const [file] = Array.from(files); const [file] = Array.from(files);
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
setVariantImageUrl(quizId, totalIndex, currentIndex, url); setVariantImageUrl(question.id, selectedVariantId, url);
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url); setVariantOriginalImageUrl(question.id, selectedVariantId, url);
setIsUploadImageModalOpen(false); closeImageUploadModal();
openCropModal(url, url); openCropModal(url, url);
}; };
function handleCropModalSaveClick(url: string) { function handleCropModalSaveClick(url: string) {
setVariantImageUrl(quizId, totalIndex, currentIndex, url); if (!selectedVariantId) return;
setVariantImageUrl(question.id, selectedVariantId, url);
} }
return ( return (
<> <>
<Box sx={{ pl: "20px", pr: "20px" }}> <Box sx={{ pl: "20px", pr: "20px" }}>
<AnswerDraggableList <AnswerDraggableList
variants={question.content.variants} question={question}
question={totalIndex} additionalContent={(variant) => (
additionalContent={(variant, index) => (
<> <>
{!isMobile && ( {!isMobile && (
<AddOrEditImageButton <AddOrEditImageButton
@ -80,24 +74,24 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
onImageClick={() => { onImageClick={() => {
if (!("originalImageUrl" in variant)) return; if (!("originalImageUrl" in variant)) return;
setCurrentIndex(index); setSelectedVariantId(variant.id);
if (variant.extendedText) return openCropModal( if (variant.extendedText) return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl
); );
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
onPlusClick={() => { onPlusClick={() => {
setCurrentIndex(index); setSelectedVariantId(variant.id);
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
sx={{ mx: "10px" }} sx={{ mx: "10px" }}
/> />
)} )}
</> </>
)} )}
additionalMobile={(variant, index) => ( additionalMobile={(variant) => (
<> <>
{isMobile && ( {isMobile && (
<AddOrEditImageButton <AddOrEditImageButton
@ -105,17 +99,17 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
onImageClick={() => { onImageClick={() => {
if (!("originalImageUrl" in variant)) return; if (!("originalImageUrl" in variant)) return;
setCurrentIndex(index); setSelectedVariantId(variant.id);
if (variant.extendedText) return openCropModal( if (variant.extendedText) return openCropModal(
variant.extendedText, variant.extendedText,
variant.originalImageUrl variant.originalImageUrl
); );
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
onPlusClick={() => { onPlusClick={() => {
setCurrentIndex(index); setSelectedVariantId(variant.id);
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
sx={{ m: "8px", width: "auto" }} sx={{ m: "8px", width: "auto" }}
/> />
@ -123,11 +117,7 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
</> </>
)} )}
/> />
<UploadImageModal <UploadImageModal imgHC={handleImageUpload} />
open={isUploadImageModalOpen}
onClose={() => setIsUploadImageModalOpen(false)}
imgHC={handleImageUpload}
/>
<CropModal onSaveImageClick={handleCropModalSaveClick} /> <CropModal onSaveImageClick={handleCropModalSaveClick} />
<Box <Box
sx={{ sx={{
@ -322,16 +312,7 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
height: "19px", height: "19px",
}} }}
onClick={() => { onClick={() => {
const clonedContent = { ...question.content }; addQuestionVariant(question.id)
clonedContent.variants.push({
answer: "",
hints: "",
extendedText: "",
originalImageUrl: "",
});
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
content: clonedContent,
});
}} }}
> >
Добавьте ответ Добавьте ответ
@ -362,9 +343,9 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
<ButtonsOptionsAndPict <ButtonsOptionsAndPict
switchState={switchState} switchState={switchState}
SSHC={SSHC} SSHC={SSHC}
totalIndex={totalIndex} question={question}
/> />
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} /> <SwitchOptionsAndPict switchState={switchState} question={question} />
</> </>
); );

@ -1,36 +1,32 @@
import { useParams } from "react-router-dom"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
type SettingOptionsAndPictProps = { type SettingOptionsAndPictProps = {
totalIndex: number; question: QuizQuestionVarImg;
}; };
export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndPictProps) { export default function SettingOptionsAndPict({ question }: SettingOptionsAndPictProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); 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) => { const setReplText = useDebouncedCallback((replText) => {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, replText }, if (question.type !== "varimg") return;
question.content.replText = replText;
}); });
}, 1000); }, 1000);
const debounceDescription = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, { const setDescription = useDebouncedCallback((value) => {
content: { ...question.content, innerName: value }, setQuestionInnerName(question.id, value);
});
}, 1000); }, 1000);
return ( return (
@ -63,11 +59,11 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, { if (question.type !== "varimg") return;
content: { ...question.content, own: target.checked },
}); question.content.own = target.checked;
}} })}
/> />
{!isWrappColumn && ( {!isWrappColumn && (
<Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}> <Box sx={{ mt: isMobile ? "11px" : "6px", width: "100%" }}>
@ -90,7 +86,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
}} }}
placeholder={"Пример текста"} placeholder={"Пример текста"}
text={question.content.replText} text={question.content.replText}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setReplText(target.value)}
/> />
</Box> </Box>
)} )}
@ -116,11 +112,11 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
sx={{ mr: isMobile ? "0px" : "16px" }} sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={question.content.required} checked={question.content.required}
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, { if (question.type !== "varimg") return;
content: { ...question.content, required: target.checked },
}); question.content.required = target.checked;
}} })}
/> />
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<CustomCheckbox <CustomCheckbox
@ -130,15 +126,10 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, { question.content.innerNameCheck = target.checked;
content: { question.content.innerName = "";
...question.content, })}
innerNameCheck: target.checked,
innerName: "",
},
});
}}
/> />
<Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top"> <Tooltip title="Будет отображаться как заголовок вопроса в приходящих заявках." placement="top">
<Box> <Box>
@ -150,7 +141,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounceDescription(target.value)} onChange={({ target }) => setDescription(target.value)}
/> />
)} )}
{isWrappColumn && ( {isWrappColumn && (
@ -164,7 +155,7 @@ export default function SettingOptionsAndPict({ totalIndex }: SettingOptionsAndP
sx={{ maxWidth: "360px", width: "100%" }} sx={{ maxWidth: "360px", width: "100%" }}
placeholder={"Пример текста"} placeholder={"Пример текста"}
text={question.content.replText} text={question.content.replText}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setReplText(target.value)}
/> />
</> </>
)} )}

@ -1,31 +1,28 @@
import * as React from "react"; import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import BranchingQuestions from "../branchingQuestions";
import SettingOptionsAndPict from "./SettingOptionsAndPict";
import HelpQuestions from "../helpQuestions";
import UploadImage from "../UploadImage"; import UploadImage from "../UploadImage";
import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions";
import SettingOptionsAndPict from "./SettingOptionsAndPict";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionVarImg;
} }
export default function SwitchOptionsAndPict({ export default function SwitchOptionsAndPict({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingOptionsAndPict totalIndex={totalIndex} />; return <SettingOptionsAndPict question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
case "image": case "image":
return <UploadImage totalIndex={totalIndex} />; return <UploadImage question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -6,11 +6,11 @@ import {
useTheme useTheme
} from "@mui/material"; } from "@mui/material";
import { openCropModal } from "@root/cropModal"; 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 AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal } from "@ui_kit/Modal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon"; import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { AnswerDraggableList } from "../AnswerDraggableList"; import { AnswerDraggableList } from "../AnswerDraggableList";
@ -24,49 +24,39 @@ interface Props {
} }
export default function OptionsPicture({ question }: Props) { export default function OptionsPicture({ question }: Props) {
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const quizId = Number(useParams().quizId);
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
const handleImageUpload = (files: FileList | null) => { const handleImageUpload = (files: FileList | null) => {
if (!files?.length) return; if (!files?.length || !selectedVariantId) return;
const [file] = Array.from(files); const [file] = Array.from(files);
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
setVariantImageUrl(quizId, totalIndex, currentIndex, url); setVariantImageUrl(question.id, selectedVariantId, url);
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url); setVariantOriginalImageUrl(question.id, selectedVariantId, url);
setIsUploadImageModalOpen(false); closeImageUploadModal();
openCropModal(url, url); openCropModal(url, url);
}; };
const addNewAnswer = () => {
const answerNew = question.content.variants.slice();
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
content: { ...question.content, variants: answerNew },
});
};
function handleCropModalSaveClick(url: string) { function handleCropModalSaveClick(url: string) {
setVariantImageUrl(quizId, totalIndex, currentIndex, url); if (!selectedVariantId) return;
setVariantImageUrl(question.id, selectedVariantId, url);
} }
return ( return (
<> <>
<Box sx={{ padding: "20px" }}> <Box sx={{ padding: "20px" }}>
<AnswerDraggableList <AnswerDraggableList
variants={question.content.variants} question={question}
question={totalIndex} additionalContent={(variant) => (
additionalContent={(variant, index) => (
<> <>
{!isMobile && ( {!isMobile && (
<AddOrEditImageButton <AddOrEditImageButton
@ -74,7 +64,7 @@ export default function OptionsPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
if (!("originalImageUrl" in variant)) return; if (!("originalImageUrl" in variant)) return;
setCurrentIndex(index); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
return openCropModal( return openCropModal(
variant.extendedText, variant.extendedText,
@ -82,18 +72,18 @@ export default function OptionsPicture({ question }: Props) {
); );
} }
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
onPlusClick={() => { onPlusClick={() => {
setCurrentIndex(index); setSelectedVariantId(variant.id);
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
sx={{ mx: "10px" }} sx={{ mx: "10px" }}
/> />
)} )}
</> </>
)} )}
additionalMobile={(variant, index) => ( additionalMobile={(variant) => (
<> <>
{isMobile && ( {isMobile && (
<AddOrEditImageButton <AddOrEditImageButton
@ -101,7 +91,7 @@ export default function OptionsPicture({ question }: Props) {
onImageClick={() => { onImageClick={() => {
if (!("originalImageUrl" in variant)) return; if (!("originalImageUrl" in variant)) return;
setCurrentIndex(index); setSelectedVariantId(variant.id);
if (variant.extendedText) { if (variant.extendedText) {
return openCropModal( return openCropModal(
variant.extendedText, variant.extendedText,
@ -109,11 +99,11 @@ export default function OptionsPicture({ question }: Props) {
); );
} }
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
onPlusClick={() => { onPlusClick={() => {
setCurrentIndex(index); setSelectedVariantId(variant.id);
setIsUploadImageModalOpen(true); openImageUploadModal();
}} }}
sx={{ m: "8px", width: "auto" }} sx={{ m: "8px", width: "auto" }}
/> />
@ -121,18 +111,14 @@ export default function OptionsPicture({ question }: Props) {
</> </>
)} )}
/> />
<UploadImageModal <UploadImageModal imgHC={handleImageUpload} />
open={isUploadImageModalOpen}
onClose={() => setIsUploadImageModalOpen(false)}
imgHC={handleImageUpload}
/>
<CropModal onSaveImageClick={handleCropModalSaveClick} /> <CropModal onSaveImageClick={handleCropModalSaveClick} />
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Link <Link
component="button" component="button"
variant="body2" variant="body2"
sx={{ color: theme.palette.brightPurple.main }} sx={{ color: theme.palette.brightPurple.main }}
onClick={addNewAnswer} onClick={() => addQuestionVariant(question.id)}
> >
Добавьте ответ Добавьте ответ
</Link> </Link>
@ -159,8 +145,8 @@ export default function OptionsPicture({ question }: Props) {
)} )}
</Box> </Box>
</Box> </Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} /> <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchAnswerOptionsPict switchState={switchState} totalIndex={totalIndex} /> <SwitchAnswerOptionsPict switchState={switchState} question={question} />
</> </>
); );

@ -1,44 +1,29 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { import {
Box, Box,
Button, Button,
Typography,
Tooltip, Tooltip,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2";
import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1"; import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2";
import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIcon11"; import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21";
import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12"; import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21";
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; 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 Proportion = "1:1" | "2:1" | "1:2";
type ProportionItem = { type ProportionItem = {
value: Proportion; value: Proportion;
icon: (props: { color: string }) => JSX.Element; icon: (props: { color: string; }) => JSX.Element;
}; };
const PROPORTIONS: ProportionItem[] = [ const PROPORTIONS: ProportionItem[] = [
@ -47,75 +32,25 @@ const PROPORTIONS: ProportionItem[] = [
{ value: "1:2", icon: ProportionsIcon12 }, { value: "1:2", icon: ProportionsIcon12 },
]; ];
export function SelectIconButton({ Icon, isActive = false, onClick }: Props) { type SettingOpytionsPictProps = {
const theme = useTheme(); question: QuizQuestionImages;
};
return ( export default function SettingOpytionsPict({ question }: SettingOpytionsPictProps) {
<Button
onClick={onClick}
variant="outlined"
startIcon={
<Icon
color={
isActive
? theme.palette.navbarbg.main
: theme.palette.brightPurple.main
}
/>
}
sx={{
backgroundColor: isActive ? theme.palette.brightPurple.main : "#eee4fc",
borderRadius: 0,
border: "none",
color: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
p: "7px",
width: "40px",
height: "40px",
minWidth: 0,
"& .MuiButton-startIcon": {
mr: 0,
ml: 0,
},
"&:hover": {
border: "none",
borderColor: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
},
}}
/>
);
}
export default function SettingOpytionsPict({
totalIndex,
}: SettingOpytionsPictProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(985)); const isTablet = useMediaQuery(theme.breakpoints.down(985));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
useEffect(() => {
if (!question.content.xy) {
updateProportions("1:1");
}
}, []);
const updateProportions = (proportions: Proportion) => { const updateProportions = (proportions: Proportion) => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, xy: proportions }, if (question.type !== "images") return;
question.content.xy = proportions;
}); });
}; };
@ -180,9 +115,10 @@ export default function SettingOpytionsPict({
label={"Можно несколько"} label={"Можно несколько"}
checked={question.content.multi} checked={question.content.multi}
dataCy="multiple-answers-checkbox" dataCy="multiple-answers-checkbox"
handleChange={({ target }) => handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: { ...question.content, multi: target.checked },
question.content.multi = target.checked;
}) })
} }
/> />
@ -193,11 +129,10 @@ export default function SettingOpytionsPict({
label={"Большие картинки"} label={"Большие картинки"}
checked={question.content.largeCheck} checked={question.content.largeCheck}
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "images") return;
...question.content,
largeCheck: target.checked, question.content.largeCheck = target.checked;
},
}) })
} }
/> />
@ -206,9 +141,10 @@ export default function SettingOpytionsPict({
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }} sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'} label={'Вариант "свой ответ"'}
checked={question.content.own} checked={question.content.own}
handleChange={({ target }) => handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: { ...question.content, own: target.checked },
question.content.own = target.checked;
}) })
} }
/> />
@ -247,18 +183,20 @@ export default function SettingOpytionsPict({
Формат Формат
</Typography> </Typography>
<SelectIconButton <SelectIconButton
onClick={() => onClick={() => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: { ...question.content, format: "carousel" },
question.content.format = "carousel";
}) })
} }
isActive={question.content.format === "carousel"} isActive={question.content.format === "carousel"}
Icon={FormatIcon2} Icon={FormatIcon2}
/> />
<SelectIconButton <SelectIconButton
onClick={() => onClick={() => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: { ...question.content, format: "masonry" },
question.content.format = "masonry";
}) })
} }
isActive={question.content.format === "masonry"} isActive={question.content.format === "masonry"}
@ -274,9 +212,10 @@ export default function SettingOpytionsPict({
sx={{ alignItems: isMobile ? "flex-start" : "" }} sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={question.content.required} checked={question.content.required}
handleChange={({ target }) => handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: { ...question.content, required: target.checked },
question.content.required = target.checked;
}) })
} }
/> />
@ -294,13 +233,11 @@ export default function SettingOpytionsPict({
}} }}
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => handleChange={({ target }) => updateQuestionWithFnOptimistic(question.id, question => {
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, { if (question.type !== "images") return;
content: {
...question.content, question.content.innerNameCheck = target.checked;
innerNameCheck: target.checked, question.content.innerName = "";
innerName: "",
},
}) })
} }
/> />
@ -325,3 +262,53 @@ export default function SettingOpytionsPict({
</> </>
); );
} }
interface Props {
Icon: (props: { color: string; }) => JSX.Element;
// Icon: React.ElementType;
isActive?: boolean;
onClick: () => void;
}
export function SelectIconButton({ Icon, isActive = false, onClick }: Props) {
const theme = useTheme();
return (
<Button
onClick={onClick}
variant="outlined"
startIcon={
<Icon
color={
isActive
? theme.palette.navbarbg.main
: theme.palette.brightPurple.main
}
/>
}
sx={{
backgroundColor: isActive ? theme.palette.brightPurple.main : "#eee4fc",
borderRadius: 0,
border: "none",
color: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
p: "7px",
width: "40px",
height: "40px",
minWidth: 0,
"& .MuiButton-startIcon": {
mr: 0,
ml: 0,
},
"&:hover": {
border: "none",
borderColor: isActive
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
},
}}
/>
);
}

@ -2,26 +2,24 @@ import * as React from "react";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import SettingOpytionsPict from "./settingOpytionsPict"; import SettingOpytionsPict from "./settingOpytionsPict";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import { QuizQuestionImages } from "@model/questionTypes/images";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionImages;
} }
export default function SwitchAnswerOptionsPict({ export default function SwitchAnswerOptionsPict({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingOpytionsPict totalIndex={totalIndex} />; return <SettingOpytionsPict question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,32 +1,29 @@
import { useState } from "react"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useParams } from "react-router-dom"; import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { Box, Typography, Tooltip, useTheme, useMediaQuery } from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import SwitchTextField from "./switchTextField"; import SwitchTextField from "./switchTextField";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionText;
} }
export default function OwnTextField({ totalIndex }: Props) {
export default function OwnTextField({ question }: Props) {
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const question = listQuestions[quizId][totalIndex] as QuizQuestionText;
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { const setPlaceholder = useDebouncedCallback((value) => {
content: { ...question.content, placeholder: value }, updateQuestionWithFnOptimistic(question.id, question => {
if (question.type !== "text") return;
question.content.placeholder = value;
}); });
}, 1000); }, 1000);
@ -51,7 +48,7 @@ export default function OwnTextField({ totalIndex }: Props) {
<CustomTextField <CustomTextField
placeholder={"Пример ответа"} placeholder={"Пример ответа"}
text={question.content.placeholder} text={question.content.placeholder}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setPlaceholder(target.value)}
sx={{ maxWidth: isFigmaTablte ? "549px" : "640px", width: "100%", mt: isMobile ? "15px" : "0px" }} sx={{ maxWidth: isFigmaTablte ? "549px" : "640px", width: "100%", mt: isMobile ? "15px" : "0px" }}
/> />
<Box sx={{ display: "flex", alignItems: isMobile ? "flex-start" : "center", gap: "12px" }}> <Box sx={{ display: "flex", alignItems: isMobile ? "flex-start" : "center", gap: "12px" }}>
@ -72,8 +69,8 @@ export default function OwnTextField({ totalIndex }: Props) {
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} /> <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchTextField switchState={switchState} totalIndex={totalIndex} /> <SwitchTextField switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,29 +1,26 @@
import { useParams } from "react-router-dom";
import { import {
Box, Box,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
Radio, Radio,
RadioGroup, RadioGroup,
Typography,
Tooltip, Tooltip,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useDebouncedCallback } from "use-debounce"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { questionStore, updateQuestionsList } from "@root/questions";
import CheckedIcon from "@ui_kit/RadioCheck"; import CheckedIcon from "@ui_kit/RadioCheck";
import CheckIcon from "@ui_kit/RadioIcon"; import CheckIcon from "@ui_kit/RadioIcon";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text"; import type { QuizQuestionText } from "../../../model/questionTypes/text";
type SettingTextFieldProps = { type SettingTextFieldProps = {
totalIndex: number; question: QuizQuestionText;
}; };
type Answer = { type Answer = {
@ -37,20 +34,15 @@ const ANSWER_TYPES: Answer[] = [
]; ];
export default function SettingTextField({ export default function SettingTextField({
totalIndex, question,
}: SettingTextFieldProps) { }: SettingTextFieldProps) {
const { listQuestions } = questionStore();
const quizId = Number(useParams().quizId);
const theme = useTheme(); const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionText;
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const debounced = useDebouncedCallback((value) => { const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
return ( return (
@ -93,11 +85,10 @@ export default function SettingTextField({
({ value }) => question.content.answerType === value ({ value }) => question.content.answerType === value
)} )}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "text") return;
...question.content,
answerType: ANSWER_TYPES[Number(target.value)].value, question.content.answerType = ANSWER_TYPES[Number(target.value)].value;
},
}); });
}} }}
> >
@ -128,8 +119,10 @@ export default function SettingTextField({
label={"Только числа"} label={"Только числа"}
checked={question.content.onlyNumbers} checked={question.content.onlyNumbers}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, onlyNumbers: target.checked }, if (question.type !== "text") return;
question.content.onlyNumbers = target.checked;
}); });
}} }}
/> />
@ -164,8 +157,8 @@ export default function SettingTextField({
label={"Автозаполнение адреса"} label={"Автозаполнение адреса"}
checked={question.content.autofill} checked={question.content.autofill}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, autofill: target.checked }, question.content.autofill = target.checked;
}); });
}} }}
/> />
@ -178,8 +171,8 @@ export default function SettingTextField({
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !e.target.checked, question.required = !e.target.checked;
}); });
}} }}
/> />
@ -200,12 +193,11 @@ export default function SettingTextField({
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionText>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { question.content.innerNameCheck = target.checked;
...question.content, question.content.innerName = target.checked
innerNameCheck: target.checked, ? question.content.innerName
innerName: target.checked ? question.content.innerName : "", : "";
},
}); });
}} }}
/> />

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionText } from "@model/questionTypes/text";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingTextField from "./settingTextField"; import SettingTextField from "./settingTextField";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionText;
} }
export default function SwitchTextField({ export default function SwitchTextField({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingTextField totalIndex={totalIndex} />; return <SettingTextField question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,39 +1,38 @@
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon"; import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { questionStore, setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestionsList } from "@root/questions"; import { openCropModal } from "@root/cropModal";
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
import { setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal } from "@ui_kit/Modal/CropModal";
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import { UploadImageModal } from "../UploadImage/UploadImageModal"; import { UploadImageModal } from "../UploadImage/UploadImageModal";
import { UploadVideoModal } from "../UploadVideoModal"; import { UploadVideoModal } from "../UploadVideoModal";
import SwitchPageOptions from "./switchPageOptions"; import SwitchPageOptions from "./switchPageOptions";
import { openCropModal } from "@root/cropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { CropModal } from "@ui_kit/Modal/CropModal";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
type Props = { type Props = {
disableInput?: boolean; disableInput?: boolean;
totalIndex: number; question: QuizQuestionPage;
}; };
export default function PageOptions({ disableInput, totalIndex }: Props) { export default function PageOptions({ disableInput, question }: Props) {
const [openImageModal, setOpenImageModal] = useState<boolean>(false);
const [openVideoModal, setOpenVideoModal] = useState<boolean>(false); const [openVideoModal, setOpenVideoModal] = useState<boolean>(false);
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980)); const isTablet = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(780)); const isMobile = useMediaQuery(theme.breakpoints.down(780));
const question = listQuestions[quizId][totalIndex] as QuizQuestionPage;
const debounced = useDebouncedCallback((value) => { const setText = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, text: value }, if (question.type !== "page") return;
question.content.text = value;
}); });
}, 1000); }, 1000);
@ -46,14 +45,14 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
const url = URL.createObjectURL(fileList[0]); const url = URL.createObjectURL(fileList[0]);
setPageQuestionPicture(quizId, totalIndex, url); setPageQuestionPicture(question.id, url);
setPageQuestionOriginalPicture(quizId, totalIndex, url); setPageQuestionOriginalPicture(question.id, url);
setOpenImageModal(false); closeImageUploadModal();
openCropModal(url, url); openCropModal(url, url);
} }
function handleCropModalSaveClick(url: string) { function handleCropModalSaveClick(url: string) {
setPageQuestionPicture(quizId, totalIndex, url); setPageQuestionPicture(question.id, url);
} }
return ( return (
@ -72,7 +71,7 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
<CustomTextField <CustomTextField
placeholder={"Можно добавить текст"} placeholder={"Можно добавить текст"}
text={question.content.text} text={question.content.text}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setText(target.value)}
/> />
</Box> </Box>
@ -104,10 +103,10 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
); );
} }
setOpenImageModal(true); openImageUploadModal();
}} }}
onPlusClick={() => { onPlusClick={() => {
setOpenImageModal(true); openImageUploadModal();
}} }}
/> />
@ -123,11 +122,7 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
Изображение Изображение
</Typography> </Typography>
</Box> </Box>
<UploadImageModal <UploadImageModal imgHC={handleImageUpload} />
open={openImageModal}
onClose={() => setOpenImageModal(false)}
imgHC={handleImageUpload}
/>
<CropModal onSaveImageClick={handleCropModalSaveClick} /> <CropModal onSaveImageClick={handleCropModalSaveClick} />
<Typography> или</Typography> <Typography> или</Typography>
<Box <Box
@ -230,15 +225,17 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
onClose={() => setOpenVideoModal(false)} onClose={() => setOpenVideoModal(false)}
video={question.content.video} video={question.content.video}
onUpload={(url) => { onUpload={(url) => {
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, video: url }, if (question.type !== "page") return;
question.content.video = url;
}); });
}} }}
/> />
</Box> </Box>
</Box> </Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} /> <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchPageOptions switchState={switchState} totalIndex={totalIndex} /> <SwitchPageOptions switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,37 +1,30 @@
import { useParams } from "react-router-dom";
import { import {
Box, Box,
Typography,
Tooltip, Tooltip,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionPage } from "../../../model/questionTypes/page"; import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
type SettingPageOptionsProps = { type SettingPageOptionsProps = {
totalIndex: number; question: QuizQuestionPage;
}; };
export default function SettingPageOptions({ export default function SettingPageOptions({
totalIndex, question,
}: SettingPageOptionsProps) { }: SettingPageOptionsProps) {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore(); const setInnerName = useDebouncedCallback((value) => {
const question = listQuestions[quizId][totalIndex] as QuizQuestionPage; setQuestionInnerName(question.id, value);
const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
return ( return (
@ -67,12 +60,9 @@ export default function SettingPageOptions({
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => handleChange={({ target }) =>
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { question.content.innerNameCheck = target.checked;
...question.content, question.content.innerName = "";
innerNameCheck: target.checked,
innerName: "",
},
}) })
} }
/> />
@ -89,7 +79,7 @@ export default function SettingPageOptions({
<CustomTextField <CustomTextField
placeholder={"Внутреннее описание вопроса"} placeholder={"Внутреннее описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
/> />
)} )}
</Box> </Box>

@ -1,26 +1,25 @@
import { QuizQuestionPage } from "@model/questionTypes/page";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingPageOptions from "./SettingPageOptions"; import SettingPageOptions from "./SettingPageOptions";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionPage;
} }
export default function SwitchPageOptions({ export default function SwitchPageOptions({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingPageOptions totalIndex={totalIndex} />; return <SettingPageOptions question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -6,7 +6,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { createQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createQuestion } from "@root/questions/actions";
import { incrementCurrentStep } from "@root/quizes/actions"; import { incrementCurrentStep } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
@ -21,15 +21,6 @@ export default function QuestionsPage() {
const isMobile = useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
const { quiz } = useCurrentQuiz(); const { quiz } = useCurrentQuiz();
const collapseEverything = () => { // TODO
// listQuestions[quizId].forEach((item, index) => {
// updateQuestionsList<AnyQuizQuestion>(quizId, index, {
// ...item,
// expanded: false,
// });
// });
};
if (!quiz) return null; if (!quiz) return null;
return ( return (
@ -53,7 +44,7 @@ export default function QuestionsPage() {
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main, textDecorationColor: theme.palette.brightPurple.main,
}} }}
onClick={collapseEverything} onClick={collapseAllQuestions}
> >
Свернуть всё Свернуть всё
</Button> </Button>

@ -8,10 +8,8 @@ import {
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { questionStore, updateQuestionsList } from "@root/questions";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import SwitchRating from "./switchRating"; import SwitchRating from "./switchRating";
import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon"; import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon";
import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon"; import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon";
import HeartIcon from "../../../assets/icons/questionsPage/heartIcon"; import HeartIcon from "../../../assets/icons/questionsPage/heartIcon";
@ -19,11 +17,12 @@ import LikeIcon from "../../../assets/icons/questionsPage/likeIcon";
import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon"; import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon";
import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon"; import HashtagIcon from "../../../assets/icons/questionsPage/hashtagIcon";
import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini"; import StarIconMini from "../../../assets/icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionRating;
} }
export type ButtonRatingFrom = { export type ButtonRatingFrom = {
@ -31,33 +30,30 @@ export type ButtonRatingFrom = {
icon: JSX.Element; icon: JSX.Element;
}; };
export default function RatingOptions({ totalIndex }: Props) { export default function RatingOptions({ question }: Props) {
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const [negativeText, setNegativeText] = useState<string>(""); const [negativeText, setNegativeText] = useState<string>("");
const [positiveText, setPositiveText] = useState<string>(""); const [positiveText, setPositiveText] = useState<string>("");
const [negativeTextWidth, setNegativeTextWidth] = useState<number>(0); const [negativeTextWidth, setNegativeTextWidth] = useState<number>(0);
const [positiveTextWidth, setPositiveTextWidth] = useState<number>(0); const [positiveTextWidth, setPositiveTextWidth] = useState<number>(0);
const quizId = Number(useParams().quizId); const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const question = listQuestions[quizId][totalIndex] as QuizQuestionRating;
const negativeRef = useRef<HTMLDivElement>(null); const negativeRef = useRef<HTMLDivElement>(null);
const positiveRef = useRef<HTMLDivElement>(null); const positiveRef = useRef<HTMLDivElement>(null);
const debounceNegativeDescription = useDebouncedCallback((value) => { const debounceNegativeDescription = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionRating>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "rating") return;
...question.content,
ratingNegativeDescription: value.substring(0, 15), question.content.ratingNegativeDescription = value.substring(0, 15);
},
}); });
}, 500); }, 500);
const debouncePositiveDescription = useDebouncedCallback((value) => { const debouncePositiveDescription = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionRating>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "rating") return;
...question.content,
ratingPositiveDescription: value.substring(0, 15), question.content.ratingPositiveDescription = value.substring(0, 15);
},
}); });
}, 500); }, 500);
@ -124,16 +120,11 @@ export default function RatingOptions({ totalIndex }: Props) {
{...(itemNumber === 0 || itemNumber === question.content.steps - 1 {...(itemNumber === 0 || itemNumber === question.content.steps - 1
? { ? {
onClick: () => { onClick: () => {
updateQuestionsList<QuizQuestionRating>( updateQuestionWithFnOptimistic(question.id, question => {
quizId, if (question.type !== "rating") return;
totalIndex,
{ question.content.ratingExpanded = true;
content: { });
...question.content,
ratingExpanded: true,
},
}
);
}, },
sx: { sx: {
cursor: "pointer", cursor: "pointer",
@ -270,9 +261,9 @@ export default function RatingOptions({ totalIndex }: Props) {
<ButtonsOptions <ButtonsOptions
switchState={switchState} switchState={switchState}
SSHC={SSHC} SSHC={SSHC}
totalIndex={totalIndex} question={question}
/> />
<SwitchRating switchState={switchState} totalIndex={totalIndex} /> <SwitchRating switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,39 +1,32 @@
import { useParams } from "react-router-dom"; import { QuizQuestionRating } from "@model/questionTypes/rating";
import { Box, ButtonBase, Slider, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; import { Box, ButtonBase, Slider, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useDebouncedCallback } from "use-debounce"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { questionStore, updateQuestionsList } from "@root/questions"; import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon";
import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon"; 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 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 { ButtonRatingFrom } from "./RatingOptions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
type SettingSliderProps = { type SettingSliderProps = {
totalIndex: number; question: QuizQuestionRating;
}; };
export default function SettingSlider({ totalIndex }: SettingSliderProps) { export default function SettingSlider({ question }: SettingSliderProps) {
const quizId = Number(useParams().quizId);
const theme = useTheme(); const theme = useTheme();
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber; const setInnerName = useDebouncedCallback((value) => {
const debounced = useDebouncedCallback((value) => { setQuestionInnerName(question.id, value);
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
const buttonRatingForm: ButtonRatingFrom[] = [ const buttonRatingForm: ButtonRatingFrom[] = [
@ -86,8 +79,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<ButtonBase <ButtonBase
key={index} key={index}
onClick={() => { onClick={() => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, form: name }, if (question.type !== "rating") return;
question.content.form = name;
}); });
}} }}
sx={{ sx={{
@ -126,8 +121,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
valueLabelDisplay="auto" valueLabelDisplay="auto"
sx={{ color: theme.palette.brightPurple.main, padding: "0" }} sx={{ color: theme.palette.brightPurple.main, padding: "0" }}
onChange={(_, value) => { onChange={(_, value) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, steps: Number(value) || 1 }, if (question.type !== "rating") return;
question.content.steps = Number(value) || 1;
}); });
}} }}
/> />
@ -153,8 +150,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !e.target.checked, if (question.type !== "rating") return;
question.required = !e.target.checked;
}); });
}} }}
/> />
@ -170,12 +169,11 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "rating") return;
...question.content,
innerNameCheck: target.checked, question.content.innerNameCheck = target.checked;
innerName: target.checked ? question.content.innerName : "", question.content.innerName = target.checked ? question.content.innerName : "";
},
}); });
}} }}
/> />
@ -189,7 +187,7 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
/> />
)} )}
</Box> </Box>

@ -1,26 +1,25 @@
import { QuizQuestionRating } from "@model/questionTypes/rating";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingRating from "./settingRating"; import SettingRating from "./settingRating";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionRating;
} }
export default function SwitchRating({ export default function SwitchRating({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingRating totalIndex={totalIndex} />; return <SettingRating question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,26 +1,22 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import ButtonsOptions from "../ButtonsOptions"; import ButtonsOptions from "../ButtonsOptions";
import CustomNumberField from "@ui_kit/CustomNumberField"; import CustomNumberField from "@ui_kit/CustomNumberField";
import SwitchSlider from "./switchSlider"; import SwitchSlider from "./switchSlider";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
interface Props { interface Props {
totalIndex: number; question: QuizQuestionNumber;
} }
export default function SliderOptions({ totalIndex }: Props) { export default function SliderOptions({ question }: Props) {
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980)); const isTablet = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const [stepError, setStepError] = useState(""); const [stepError, setStepError] = useState("");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionNumber;
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
@ -60,13 +56,10 @@ export default function SliderOptions({ totalIndex }: Props) {
max={99} max={99}
value={question.content.range.split("—")[0]} value={question.content.range.split("—")[0]}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "number") return;
...question.content,
range: `${target.value}${ question.content.range = `${target.value}${question.content.range.split("—")[1]}`;
question.content.range.split("—")[1]
}`,
},
}); });
}} }}
onBlur={({ target }) => { onBlur={({ target }) => {
@ -75,19 +68,18 @@ export default function SliderOptions({ totalIndex }: Props) {
const max = Number(question.content.range.split("—")[1]); const max = Number(question.content.range.split("—")[1]);
if (min >= max) { if (min >= max) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "number") return;
...question.content,
range: `${max - 1 >= 0 ? max - 1 : 0}${ question.content.range = `${max - 1 >= 0 ? max - 1 : 0}${question.content.range.split("—")[1]}`;
question.content.range.split("—")[1]
}`,
},
}); });
} }
if (start < min) { if (start < min) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, start: min }, if (question.type !== "number") return;
question.content.start = min;
}); });
} }
}} }}
@ -100,13 +92,10 @@ export default function SliderOptions({ totalIndex }: Props) {
max={100} max={100}
value={question.content.range.split("—")[1]} value={question.content.range.split("—")[1]}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "number") return;
...question.content,
range: `${question.content.range.split("—")[0]}${ question.content.range = `${question.content.range.split("—")[0]}${target.value}`;
target.value
}`,
},
}); });
}} }}
onBlur={({ target }) => { onBlur={({ target }) => {
@ -117,25 +106,26 @@ export default function SliderOptions({ totalIndex }: Props) {
const range = max - min; const range = max - min;
if (max <= min) { if (max <= min) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "number") return;
...question.content,
range: `${question.content.range.split("—")[0]}${ question.content.range = `${min}${min + 1 >= 100 ? 100 : min + 1}`;
min + 1 >= 100 ? 100 : min + 1
}`,
},
}); });
} }
if (start > max) { if (start > max) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, start: max }, if (question.type !== "number") return;
question.content.start = max;
}); });
} }
if (step > max) { if (step > max) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, step: max }, if (question.type !== "number") return;
question.content.step = min;
}); });
if (range % step) { if (range % step) {
@ -168,8 +158,10 @@ export default function SliderOptions({ totalIndex }: Props) {
max={Number(question.content.range.split("—")[1])} max={Number(question.content.range.split("—")[1])}
value={String(question.content.start)} value={String(question.content.start)}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, start: Number(target.value) }, if (question.type !== "number") return;
question.content.start = Number(target.value);
}); });
}} }}
/> />
@ -193,8 +185,10 @@ export default function SliderOptions({ totalIndex }: Props) {
error={stepError} error={stepError}
value={String(question.content.step)} value={String(question.content.step)}
onChange={({ target }) => { onChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, step: Number(target.value) }, if (question.type !== "number") return;
question.content.step = Number(target.value);
}); });
}} }}
onBlur={({ target }) => { onBlur={({ target }) => {
@ -204,8 +198,10 @@ export default function SliderOptions({ totalIndex }: Props) {
const step = Number(target.value); const step = Number(target.value);
if (step > max) { if (step > max) {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, step: max }, if (question.type !== "number") return;
question.content.step = max;
}); });
} }
@ -219,8 +215,8 @@ export default function SliderOptions({ totalIndex }: Props) {
</Box> </Box>
</Box> </Box>
</Box> </Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} /> <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchSlider switchState={switchState} totalIndex={totalIndex} /> <SwitchSlider switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,29 +1,24 @@
import { useParams } from "react-router-dom"; import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { Box, Typography, Tooltip, useMediaQuery, useTheme } from "@mui/material"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import { questionStore, updateQuestionsList } from "@root/questions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
type SettingSliderProps = { type SettingSliderProps = {
totalIndex: number; question: QuizQuestionNumber;
}; };
export default function SettingSlider({ totalIndex }: SettingSliderProps) { export default function SettingSlider({ question }: SettingSliderProps) {
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980)); const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); 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 QuizQuestionNumber;
const debounced = useDebouncedCallback((value) => { const setInnerName = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { setQuestionInnerName(question.id, value);
content: { ...question.content, innerName: value },
});
}, 1000); }, 1000);
return ( return (
@ -57,8 +52,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
label={"Выбор диапозона (два ползунка)"} label={"Выбор диапозона (два ползунка)"}
checked={question.content.chooseRange} checked={question.content.chooseRange}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, chooseRange: target.checked }, if (question.type !== "number") return;
question.content.chooseRange = target.checked;
}); });
}} }}
/> />
@ -83,8 +80,10 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !e.target.checked, if (question.type !== "number") return;
question.required = !e.target.checked;
}); });
}} }}
/> />
@ -104,12 +103,11 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionNumber>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { if (question.type !== "number") return;
...question.content,
innerNameCheck: target.checked, question.content.innerNameCheck = target.checked;
innerName: target.checked ? question.content.innerName : "", question.content.innerName = target.checked ? question.content.innerName : "";
},
}); });
}} }}
/> />
@ -123,7 +121,7 @@ export default function SettingSlider({ totalIndex }: SettingSliderProps) {
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
/> />
)} )}
</Box> </Box>

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionNumber } from "@model/questionTypes/number";
import HelpQuestions from "../helpQuestions";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions";
import SettingSlider from "./settingSlider"; import SettingSlider from "./settingSlider";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionNumber;
} }
export default function SwitchSlider({ export default function SwitchSlider({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingSlider totalIndex={totalIndex} />; return <SettingSlider question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -1,33 +1,25 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { import {
Box, Box,
FormControl, FormControl,
MenuItem, MenuItem,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
Typography,
Tooltip, Tooltip,
Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import ButtonsOptions from "../ButtonsOptions"; import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
import { useEffect, useState } from "react";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon";
import ArrowDown from "../../../assets/icons/ArrowDownIcon"; import ArrowDown from "../../../assets/icons/ArrowDownIcon";
import SwitchUpload from "./switchUpload"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared";
import type { import type {
QuizQuestionFile, QuizQuestionFile,
UploadFileType, UploadFileType,
} from "../../../model/questionTypes/file"; } from "../../../model/questionTypes/file";
import ButtonsOptions from "../ButtonsOptions";
import SwitchUpload from "./switchUpload";
interface Props {
totalIndex: number;
}
type DesignItem = { type DesignItem = {
name: string; name: string;
@ -42,13 +34,14 @@ const DESIGN_TYPES: DesignItem[] = [
{ name: "Документ", value: "document" }, { name: "Документ", value: "document" },
]; ];
export default function UploadFile({ totalIndex }: Props) { interface Props {
question: QuizQuestionFile;
}
export default function UploadFile({ question }: Props) {
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980)); const isTablet = useMediaQuery(theme.breakpoints.down(980));
const question = listQuestions[quizId][totalIndex] as QuizQuestionFile;
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -57,8 +50,10 @@ export default function UploadFile({ totalIndex }: Props) {
}; };
const handleChange = ({ target }: SelectChangeEvent) => { const handleChange = ({ target }: SelectChangeEvent) => {
updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, type: target.value as UploadFileType }, if (question.type !== "file") return;
question.content.type = target.value as UploadFileType;
}); });
}; };
@ -68,8 +63,10 @@ export default function UploadFile({ totalIndex }: Props) {
); );
if (!isTypeSetted) { if (!isTypeSetted) {
updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, type: DESIGN_TYPES[0].value }, if (question.type !== "file") return;
question.content.type = DESIGN_TYPES[0].value;
}); });
} }
}, []); }, []);
@ -194,9 +191,9 @@ export default function UploadFile({ totalIndex }: Props) {
<ButtonsOptions <ButtonsOptions
switchState={switchState} switchState={switchState}
SSHC={SSHC} SSHC={SSHC}
totalIndex={totalIndex} question={question}
/> />
<SwitchUpload switchState={switchState} totalIndex={totalIndex} /> <SwitchUpload switchState={switchState} question={question} />
</> </>
); );
} }

@ -1,37 +1,30 @@
import { useParams } from "react-router-dom";
import { import {
Box, Box,
Tooltip,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { questionStore, updateQuestionsList } from "@root/questions";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionFile } from "../../../model/questionTypes/file"; import type { QuizQuestionFile } from "../../../model/questionTypes/file";
type SettingsUploadProps = { type SettingsUploadProps = {
totalIndex: number; question: QuizQuestionFile;
}; };
export default function SettingsUpload({ totalIndex }: SettingsUploadProps) { export default function SettingsUpload({ question }: SettingsUploadProps) {
const theme = useTheme(); const theme = useTheme();
const quizId = Number(useParams().quizId);
const { listQuestions } = questionStore();
const question = listQuestions[quizId][totalIndex] as QuizQuestionFile;
const debounced = useDebouncedCallback((value) => {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, {
content: { ...question.content, innerName: value },
});
}, 1000);
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const setInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value);
}, 1000);
return ( return (
<Box <Box
sx={{ sx={{
@ -55,8 +48,8 @@ export default function SettingsUpload({ totalIndex }: SettingsUploadProps) {
label={"Автозаполнение адреса"} label={"Автозаполнение адреса"}
checked={question.content.autofill} checked={question.content.autofill}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { ...question.content, autofill: target.checked }, question.content.autofill = target.checked;
}); });
}} }}
/> />
@ -68,8 +61,8 @@ export default function SettingsUpload({ totalIndex }: SettingsUploadProps) {
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
required: !e.target.checked, question.required = !e.target.checked;
}); });
}} }}
/> />
@ -89,12 +82,9 @@ export default function SettingsUpload({ totalIndex }: SettingsUploadProps) {
label={"Внутреннее название вопроса"} label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck} checked={question.content.innerNameCheck}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestionsList<QuizQuestionFile>(quizId, totalIndex, { updateQuestionWithFnOptimistic(question.id, question => {
content: { question.content.innerNameCheck = target.checked;
...question.content, question.content.innerName = target.checked ? question.content.innerName : "";
innerNameCheck: target.checked,
innerName: target.checked ? question.content.innerName : "",
},
}); });
}} }}
/> />
@ -111,7 +101,7 @@ export default function SettingsUpload({ totalIndex }: SettingsUploadProps) {
<CustomTextField <CustomTextField
placeholder={"Развёрнутое описание вопроса"} placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName} text={question.content.innerName}
onChange={({ target }) => debounced(target.value)} onChange={({ target }) => setInnerName(target.value)}
sx={{ paddingRight: "20px" }} sx={{ paddingRight: "20px" }}
/> />
)} )}

@ -1,27 +1,25 @@
import * as React from "react"; import { QuizQuestionFile } from "@model/questionTypes/file";
import BranchingQuestions from "../branchingQuestions"; import BranchingQuestions from "../branchingQuestions";
import HelpQuestions from "../helpQuestions"; import HelpQuestions from "../helpQuestions";
import SettingsUpload from "./settingUpload"; import SettingsUpload from "./settingUpload";
interface Props { interface Props {
switchState: string; switchState: string;
totalIndex: number; question: QuizQuestionFile;
} }
export default function SwitchUpload({ export default function SwitchUpload({
switchState = "setting", switchState = "setting",
totalIndex, question,
}: Props) { }: Props) {
switch (switchState) { switch (switchState) {
case "setting": case "setting":
return <SettingsUpload totalIndex={totalIndex} />; return <SettingsUpload question={question} />;
break;
case "help": case "help":
return <HelpQuestions totalIndex={totalIndex} />; return <HelpQuestions question={question} />;
break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; return <BranchingQuestions question={question} />;
break;
default: default:
return <></>; return <></>;
} }

@ -14,19 +14,17 @@ import * as React from "react";
import UnsplashIcon from "../../../assets/icons/Unsplash.svg"; import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
import type { DragEvent } from "react"; import type { DragEvent } from "react";
import { closeImageUploadModal, useImageUploadModalStore } from "@root/imageUploadModal";
interface ModalkaProps { interface ModalkaProps {
open: boolean;
onClose: () => void;
imgHC: (imgInp: FileList | null) => void; imgHC: (imgInp: FileList | null) => void;
} }
export const UploadImageModal: React.FC<ModalkaProps> = ({ export const UploadImageModal: React.FC<ModalkaProps> = ({
open,
onClose,
imgHC, imgHC,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const isOpen = useImageUploadModalStore(state => state.isOpen)
const dropZone = React.useRef<HTMLDivElement>(null); const dropZone = React.useRef<HTMLDivElement>(null);
const [ready, setReady] = React.useState(false); const [ready, setReady] = React.useState(false);
@ -45,8 +43,8 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({
return ( return (
<Modal <Modal
open={open} open={isOpen}
onClose={onClose} onClose={closeImageUploadModal}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
> >

@ -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 { Box, ButtonBase, Typography, useTheme } from "@mui/material";
import { openCropModal } from "@root/cropModal"; import { openCropModal } from "@root/cropModal";
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
import { setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions/actions"; import { setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions/actions";
import { CropModal } from "@ui_kit/Modal/CropModal"; import { CropModal } from "@ui_kit/Modal/CropModal";
import UploadBox from "@ui_kit/UploadBox"; import UploadBox from "@ui_kit/UploadBox";
import { useState, type DragEvent } from "react"; import { type DragEvent } from "react";
import UploadIcon from "../../../assets/icons/UploadIcon"; import UploadIcon from "../../../assets/icons/UploadIcon";
import { UploadImageModal } from "./UploadImageModal"; import { UploadImageModal } from "./UploadImageModal";
type UploadImageProps = { type UploadImageProps = {
question: QuizQuestionVariant; question: AnyQuizQuestion;
}; };
export default function UploadImage({ question }: UploadImageProps) { export default function UploadImage({ question }: UploadImageProps) {
const theme = useTheme(); const theme = useTheme();
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
const handleImageUpload = (files: FileList | null) => { const handleImageUpload = (files: FileList | null) => {
if (!files?.length) return; if (!files?.length) return;
@ -26,7 +26,7 @@ export default function UploadImage({ question }: UploadImageProps) {
setQuestionBackgroundImage(question.id, url); setQuestionBackgroundImage(question.id, url);
setQuestionOriginalBackgroundImage(question.id, url); setQuestionOriginalBackgroundImage(question.id, url);
setIsUploadImageModalOpen(false); closeImageUploadModal();
openCropModal(url, url); openCropModal(url, url);
}; };
@ -54,7 +54,7 @@ export default function UploadImage({ question }: UploadImageProps) {
Загрузить изображение Загрузить изображение
</Typography> </Typography>
<ButtonBase <ButtonBase
onClick={() => setIsUploadImageModalOpen(true)} onClick={openImageUploadModal}
sx={{ sx={{
width: "100%", width: "100%",
maxWidth: "260px", maxWidth: "260px",
@ -81,11 +81,7 @@ export default function UploadImage({ question }: UploadImageProps) {
/> />
} }
</ButtonBase> </ButtonBase>
<UploadImageModal <UploadImageModal imgHC={handleImageUpload} />
open={isUploadImageModalOpen}
onClose={() => setIsUploadImageModalOpen(false)}
imgHC={handleImageUpload}
/>
<CropModal onSaveImageClick={handleCropModalSaveClick} /> <CropModal onSaveImageClick={handleCropModalSaveClick} />
</Box> </Box>
); );

@ -37,10 +37,7 @@ export default function AnswerOptions({ question }: Props) {
Добавьте ответ Добавьте ответ
</Typography> </Typography>
) : ( ) : (
<AnswerDraggableList <AnswerDraggableList question={question} />
variants={question.content.variants}
question={question}
/>
)} )}
<Box <Box

@ -5,7 +5,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -25,9 +25,7 @@ export default function ResponseSettings({ question }: Props) {
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const updateQuestionInnerName = useDebouncedCallback((value) => { const updateQuestionInnerName = useDebouncedCallback((value) => {
updateQuestionWithFnOptimistic(question.id, question => { setQuestionInnerName(question.id, value);
question.content.innerName = value;
});
}, 1000); }, 1000);
return ( return (

@ -1,6 +1,6 @@
import InfoIcon from "@icons/Info"; import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { QuizQuestionVariant } from "@model/questionTypes/variant"; import { AnyQuizQuestion } from "@model/questionTypes/shared";
import { import {
Box, Box,
Button, Button,
@ -24,7 +24,7 @@ import { Select } from "./Select";
type BranchingQuestionsProps = { type BranchingQuestionsProps = {
question: QuizQuestionVariant; question: AnyQuizQuestion;
}; };
const ACTIONS = ["Показать", "Скрыть"]; const ACTIONS = ["Показать", "Скрыть"];

@ -1,4 +1,4 @@
import { QuizQuestionVariant } from "@model/questionTypes/variant"; import { AnyQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography } from "@mui/material"; import { Box, ButtonBase, Typography } from "@mui/material";
import { updateQuestionWithFnOptimistic } from "@root/questions/actions"; import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -13,7 +13,7 @@ import { UploadVideoModal } from "./UploadVideoModal";
type BackgroundType = "text" | "video"; type BackgroundType = "text" | "video";
type HelpQuestionsProps = { type HelpQuestionsProps = {
question: QuizQuestionVariant; question: AnyQuizQuestion;
}; };
export default function HelpQuestions({ question }: HelpQuestionsProps) { export default function HelpQuestions({ question }: HelpQuestionsProps) {

@ -46,7 +46,8 @@ export default function SwitchResult({
return <ResponseSettings />; return <ResponseSettings />;
break; break;
case "branching": case "branching":
return <BranchingQuestions totalIndex={totalIndex} />; // return <BranchingQuestions question={question} />;
return null
break; break;
case "points": case "points":
return <PointsQuestions />; return <PointsQuestions />;

@ -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<ImageUploadModalStore>()(
persist(
devtools(
() => initialState,
{
name: "ImageUploadModalStore",
}
),
{
name: "ImageUploadModalStore",
}
)
);
export const openImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: true });
export const closeImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: false });

@ -1,7 +1,7 @@
import { questionApi } from "@api/question"; import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui"; import { devlog } from "@frontend/kitui";
import { questionToEditQuestionRequest } from "@model/question/edit"; import { questionToEditQuestionRequest } from "@model/question/edit";
import { RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared"; import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared";
import { produce } from "immer"; import { produce } from "immer";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -11,44 +11,37 @@ import { QuestionsStore, useQuestionsStore } from "./store";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
state.questionsById = {}; state.questions = questions?.map(rawQuestionToQuestion) ?? [];
if (questions === null) return;
questions.forEach(question => state.questionsById[question.id] = rawQuestionToQuestion(question));
}, { }, {
type: "setQuestions", type: "setQuestions",
questions, questions,
}); });
export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => { const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
state.questionsById[question.id] = question; const index = state.questions.findIndex(q => q.id === question.id);
state.questions.splice(index, 1, question);
}, { }, {
type: "setQuestion", type: "setQuestion",
question, question,
}); });
export const removeQuestion = (questionId: number) => setProducedState(state => { const removeQuestion = (questionId: number) => setProducedState(state => {
delete state.questionsById[questionId]; const index = state.questions.findIndex(q => q.id === questionId);
state.questions.splice(index, 1);
}, { }, {
type: "removeQuestion", type: "removeQuestion",
questionId, questionId,
}); });
export const setQuestionField = <T extends keyof AnyQuizQuestion>( const setQuestionField = <T extends keyof AnyQuizQuestion>(
questionId: number, questionId: number,
field: T, field: T,
value: AnyQuizQuestion[T], value: AnyQuizQuestion[T],
) => setProducedState(state => { ) => setProducedState(state => {
const question = state.questionsById[questionId]; const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
const oldId = question.id;
question[field] = value; question[field] = value;
if (field === "id") {
delete state.questionsById[oldId];
state.questionsById[value as number] = question;
}
}, { }, {
type: "setQuestionField", type: "setQuestionField",
questionId, questionId,
@ -56,15 +49,31 @@ export const setQuestionField = <T extends keyof AnyQuizQuestion>(
value, 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 => { export const toggleExpandQuestion = (questionId: number) => setProducedState(state => {
const question = state.questionsById[questionId]; const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
question.expanded = !question.expanded; question.expanded = !question.expanded;
}); });
export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false);
});
export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => { export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => {
const question = state.questionsById[questionId]; const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
question.openedModalSettings = !question.openedModalSettings; 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 savedOriginalQuestion: AnyQuizQuestion | null = null;
let controller: AbortController | null = null; let controller: AbortController | null = null;
@ -191,7 +278,7 @@ export const updateQuestionWithFnOptimistic = async (
questionId: number, questionId: number,
updateFn: (question: AnyQuizQuestion) => void, updateFn: (question: AnyQuizQuestion) => void,
) => { ) => {
const question = useQuestionsStore.getState().questionsById[questionId] ?? null; const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
const currentUpdatedQuestion = produce(question, updateFn); 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 { try {
const question = await questionApi.create({ const question = await questionApi.create({
quiz_id: quizId, quiz_id: quizId,
type,
}); });
setQuestion(rawQuestionToQuestion(question)); setQuestion(rawQuestionToQuestion(question));
@ -257,10 +345,12 @@ export const copyQuestion = async (questionId: number, quizId: number) => {
const { updated: newQuestionId } = await questionApi.copy(questionId, quizId); const { updated: newQuestionId } = await questionApi.copy(questionId, quizId);
setProducedState(state => { setProducedState(state => {
const question = state.questionsById[questionId]; const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
state.questionsById[newQuestionId] = question; const copiedQuestion = structuredClone(question);
copiedQuestion.id = newQuestionId;
state.questions.push(copiedQuestion);
}, { }, {
type: "copyQuestion", type: "copyQuestion",
questionId, questionId,

@ -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] : []);
}

@ -4,11 +4,11 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = { export type QuestionsStore = {
questionsById: Record<number, AnyQuizQuestion | undefined>; questions: AnyQuizQuestion[];
}; };
const initialState: QuestionsStore = { const initialState: QuestionsStore = {
questionsById: {}, questions: [],
}; };
export const useQuestionsStore = create<QuestionsStore>()( export const useQuestionsStore = create<QuestionsStore>()(