WIP use new store actions
This commit is contained in:
parent
dd46a3833f
commit
2103fe8977
@ -28,6 +28,7 @@
|
|||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"nanoid": "^5.0.3",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
@ -38,10 +38,10 @@ function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyQuestion(copyQuestionBody: CopyQuestionRequest) {
|
function copyQuestion(questionId: number, quizId: number) {
|
||||||
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
||||||
url: `${baseUrl}/question/copy`,
|
url: `${baseUrl}/question/copy`,
|
||||||
body: copyQuestionBody,
|
body: { id: questionId, quiz_id: quizId },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { QuestionType } from "@model/question/question";
|
import { QuestionType } from "@model/question/question";
|
||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
import { QUIZ_QUESTION_DATE } from "./date";
|
import { QUIZ_QUESTION_DATE } from "./date";
|
||||||
import { QUIZ_QUESTION_EMOJI } from "./emoji";
|
import { QUIZ_QUESTION_EMOJI } from "./emoji";
|
||||||
import { QUIZ_QUESTION_FILE } from "./file";
|
import { QUIZ_QUESTION_FILE } from "./file";
|
||||||
@ -10,19 +11,18 @@ import { QUIZ_QUESTION_SELECT } from "./select";
|
|||||||
import { QUIZ_QUESTION_TEXT } from "./text";
|
import { QUIZ_QUESTION_TEXT } from "./text";
|
||||||
import { QUIZ_QUESTION_VARIANT } from "./variant";
|
import { QUIZ_QUESTION_VARIANT } from "./variant";
|
||||||
import { QUIZ_QUESTION_VARIMG } from "./varimg";
|
import { QUIZ_QUESTION_VARIMG } from "./varimg";
|
||||||
import { AnyQuestionContent } from "@model/questionTypes/shared";
|
|
||||||
|
|
||||||
|
|
||||||
export const defaultQuestionContentByType: Record<QuestionType, AnyQuestionContent> = {
|
export const defaultQuestionByType: Record<QuestionType, Omit<AnyQuizQuestion, "id">> = {
|
||||||
"date": QUIZ_QUESTION_DATE.content,
|
"date": QUIZ_QUESTION_DATE,
|
||||||
"emoji": QUIZ_QUESTION_EMOJI.content,
|
"emoji": QUIZ_QUESTION_EMOJI,
|
||||||
"file": QUIZ_QUESTION_FILE.content,
|
"file": QUIZ_QUESTION_FILE,
|
||||||
"images": QUIZ_QUESTION_IMAGES.content,
|
"images": QUIZ_QUESTION_IMAGES,
|
||||||
"number": QUIZ_QUESTION_NUMBER.content,
|
"number": QUIZ_QUESTION_NUMBER,
|
||||||
"page": QUIZ_QUESTION_PAGE.content,
|
"page": QUIZ_QUESTION_PAGE,
|
||||||
"rating": QUIZ_QUESTION_RATING.content,
|
"rating": QUIZ_QUESTION_RATING,
|
||||||
"select": QUIZ_QUESTION_SELECT.content,
|
"select": QUIZ_QUESTION_SELECT,
|
||||||
"text": QUIZ_QUESTION_TEXT.content,
|
"text": QUIZ_QUESTION_TEXT,
|
||||||
"variant": QUIZ_QUESTION_VARIANT.content,
|
"variant": QUIZ_QUESTION_VARIANT,
|
||||||
"varimg": QUIZ_QUESTION_VARIMG.content,
|
"varimg": QUIZ_QUESTION_VARIMG,
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { AnyQuizQuestion, DefiniteQuestionType } from "@model/questionTypes/shared";
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
import { QuestionType } from "./question";
|
||||||
|
|
||||||
|
|
||||||
export interface EditQuestionRequest {
|
export interface EditQuestionRequest {
|
||||||
id: number;
|
id: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
type?: DefiniteQuestionType;
|
type?: QuestionType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
page?: number;
|
page?: number;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
import { defaultQuestionContentByType } from "../../constants/default";
|
import { defaultQuestionByType } from "../../constants/default";
|
||||||
|
|
||||||
|
|
||||||
export type QuestionType =
|
export type QuestionType =
|
||||||
@ -44,7 +44,7 @@ export interface RawQuestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion {
|
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion {
|
||||||
let content = defaultQuestionContentByType[rawQuestion.type];
|
let content = defaultQuestionByType[rawQuestion.type].content;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(rawQuestion.content);
|
content = JSON.parse(rawQuestion.content);
|
||||||
|
@ -10,6 +10,7 @@ import type { QuizQuestionSelect } from "./select";
|
|||||||
import type { QuizQuestionText } from "./text";
|
import type { QuizQuestionText } from "./text";
|
||||||
import type { QuizQuestionVariant } from "./variant";
|
import type { QuizQuestionVariant } from "./variant";
|
||||||
import type { QuizQuestionVarImg } from "./varimg";
|
import type { QuizQuestionVarImg } from "./varimg";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
|
||||||
export interface QuestionBranchingRule {
|
export interface QuestionBranchingRule {
|
||||||
@ -32,6 +33,7 @@ export interface QuestionHint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QuestionVariant = {
|
export type QuestionVariant = {
|
||||||
|
id: string;
|
||||||
/** Текст */
|
/** Текст */
|
||||||
answer: string;
|
answer: string;
|
||||||
/** Текст подсказки */
|
/** Текст подсказки */
|
||||||
@ -84,8 +86,14 @@ export type AnyQuizQuestion =
|
|||||||
| QuizQuestionRating;
|
| QuizQuestionRating;
|
||||||
// | QuizQuestionInitial;
|
// | QuizQuestionInitial;
|
||||||
|
|
||||||
export type QuizQuestionType = AnyQuizQuestion["type"];
|
export const createQuestionVariant: () => QuestionVariant = () => ({
|
||||||
|
id: nanoid(),
|
||||||
|
answer: "",
|
||||||
|
extendedText: "",
|
||||||
|
hints: "",
|
||||||
|
});
|
||||||
|
|
||||||
export type AnyQuestionContent = AnyQuizQuestion["content"];
|
export const createQuestionImageVariant: () => ImageQuestionVariant = () => ({
|
||||||
|
...createQuestionVariant(),
|
||||||
export type DefiniteQuestionType = Exclude<QuizQuestionType, "nonselected">;
|
originalImageUrl: "",
|
||||||
|
});
|
||||||
|
@ -1,68 +1,51 @@
|
|||||||
import { useState } from "react";
|
import { MessageIcon } from "@icons/messagIcon";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
TextField,
|
|
||||||
FormControl,
|
|
||||||
InputAdornment,
|
|
||||||
IconButton,
|
|
||||||
Popover,
|
|
||||||
useTheme,
|
|
||||||
useMediaQuery,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
|
||||||
|
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
|
|
||||||
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 { MessageIcon } from "@icons/messagIcon";
|
|
||||||
import TextareaAutosize from "@mui/base/TextareaAutosize";
|
import TextareaAutosize from "@mui/base/TextareaAutosize";
|
||||||
import type { ChangeEvent, KeyboardEvent, ReactNode } from "react";
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
import type { DroppableProvided } from "react-beautiful-dnd";
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
Popover,
|
||||||
|
TextField,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField } from "@root/questions/actions";
|
||||||
|
import type { KeyboardEvent, ReactNode } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import type { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
import type { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
type AnswerItemProps = {
|
type AnswerItemProps = {
|
||||||
index: number;
|
index: number;
|
||||||
totalIndex: number;
|
questionId: number;
|
||||||
variants: (QuestionVariant | ImageQuestionVariant)[];
|
|
||||||
variant: QuestionVariant | ImageQuestionVariant;
|
variant: QuestionVariant | ImageQuestionVariant;
|
||||||
provided: DroppableProvided;
|
largeCheck: boolean;
|
||||||
additionalContent?: ReactNode;
|
additionalContent?: ReactNode;
|
||||||
additionalMobile?: ReactNode;
|
additionalMobile?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AnswerItem = ({
|
export const AnswerItem = ({
|
||||||
index,
|
index,
|
||||||
totalIndex,
|
|
||||||
variants,
|
|
||||||
variant,
|
variant,
|
||||||
provided,
|
questionId,
|
||||||
|
largeCheck,
|
||||||
additionalContent,
|
additionalContent,
|
||||||
additionalMobile,
|
additionalMobile,
|
||||||
}: AnswerItemProps) => {
|
}: AnswerItemProps) => {
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const question = listQuestions[quizId][totalIndex];
|
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const debounced = useDebouncedCallback((value) => {
|
|
||||||
const answerNew = variants.slice();
|
|
||||||
answerNew[index].answer = value;
|
|
||||||
updateQuestionsList(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
variants: answerNew,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const setQuestionVariantAnswer = useDebouncedCallback((value) => {
|
||||||
|
setQuestionVariantField(questionId, variant.id,"answer", value);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
@ -72,41 +55,6 @@ export const AnswerItem = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const addNewAnswer = () => {
|
|
||||||
const answerNew = variants.slice();
|
|
||||||
|
|
||||||
if (["images", "varimg"].includes(question.type)) {
|
|
||||||
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
|
|
||||||
} else {
|
|
||||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuestionsList(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, variants: answerNew },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAnswer = () => {
|
|
||||||
const answerNew = variants.slice();
|
|
||||||
answerNew.splice(index, 1);
|
|
||||||
|
|
||||||
updateQuestionsList(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, variants: answerNew },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeAnswerHint = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const answerNew = variants.slice();
|
|
||||||
answerNew[index].hints = event.target.value;
|
|
||||||
|
|
||||||
updateQuestionsList(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, variants: answerNew },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const largeCheck = ("largeCheck" in question.content) ? question.content.largeCheck : false
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={String(index)} index={index}>
|
<Draggable draggableId={String(index)} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
@ -128,10 +76,10 @@ export const AnswerItem = ({
|
|||||||
focused={false}
|
focused={false}
|
||||||
placeholder={"Добавьте ответ"}
|
placeholder={"Добавьте ответ"}
|
||||||
multiline={largeCheck}
|
multiline={largeCheck}
|
||||||
onChange={({ target }) => debounced(target.value)}
|
onChange={({ target }) => setQuestionVariantAnswer(target.value)}
|
||||||
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.code === "Enter" && !largeCheck) {
|
if (event.code === "Enter" && !largeCheck) {
|
||||||
addNewAnswer();
|
addQuestionVariant(questionId);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
@ -174,13 +122,16 @@ export const AnswerItem = ({
|
|||||||
style={{ margin: "10px" }}
|
style={{ margin: "10px" }}
|
||||||
placeholder="Подсказка для этого ответа"
|
placeholder="Подсказка для этого ответа"
|
||||||
value={variant.hints}
|
value={variant.hints}
|
||||||
onChange={changeAnswerHint}
|
onChange={e => setQuestionVariantField(questionId, variant.id, "hints", e.target.value)}
|
||||||
onKeyDown={(
|
onKeyDown={(
|
||||||
event: KeyboardEvent<HTMLTextAreaElement>
|
event: KeyboardEvent<HTMLTextAreaElement>
|
||||||
) => event.stopPropagation()}
|
) => event.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
<IconButton sx={{ padding: "0" }} onClick={deleteAnswer}>
|
<IconButton
|
||||||
|
sx={{ padding: "0" }}
|
||||||
|
onClick={() => deleteQuestionVariant(questionId, variant.id)}
|
||||||
|
>
|
||||||
<DeleteIcon
|
<DeleteIcon
|
||||||
style={{
|
style={{
|
||||||
color: theme.palette.grey2.main,
|
color: theme.palette.grey2.main,
|
||||||
|
@ -1,35 +1,28 @@
|
|||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||||
|
|
||||||
import { AnswerItem } from "./AnswerItem";
|
import { AnswerItem } from "./AnswerItem";
|
||||||
|
|
||||||
import { reorderVariants } from "@root/questions";
|
|
||||||
|
|
||||||
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 { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
import type { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
||||||
|
import { reorderQuestionVariants } from "@root/questions/actions";
|
||||||
|
|
||||||
|
|
||||||
type AnswerDraggableListProps = {
|
type AnswerDraggableListProps = {
|
||||||
variants: QuestionVariant[];
|
variants: QuestionVariant[];
|
||||||
totalIndex: number;
|
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,
|
variants,
|
||||||
totalIndex,
|
question,
|
||||||
additionalContent,
|
additionalContent,
|
||||||
additionalMobile,
|
additionalMobile,
|
||||||
}: AnswerDraggableListProps) => {
|
}: AnswerDraggableListProps) => {
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
|
|
||||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
reorderVariants(quizId, totalIndex, source.index, destination.index);
|
reorderQuestionVariants(question.id, source.index, destination.index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,12 +33,11 @@ export const AnswerDraggableList = ({
|
|||||||
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{variants.map((variant, index) => (
|
{variants.map((variant, index) => (
|
||||||
<AnswerItem
|
<AnswerItem
|
||||||
key={index}
|
key={variant.id}
|
||||||
index={index}
|
index={index}
|
||||||
totalIndex={totalIndex}
|
questionId={question.id}
|
||||||
variants={variants}
|
largeCheck={("largeCheck" in question.content) ? question.content.largeCheck : false}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
provided={provided}
|
|
||||||
additionalContent={additionalContent?.(variant, index)}
|
additionalContent={additionalContent?.(variant, index)}
|
||||||
additionalMobile={additionalMobile?.(variant, index)}
|
additionalMobile={additionalMobile?.(variant, index)}
|
||||||
/>
|
/>
|
||||||
|
@ -1,347 +1,332 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
|
|
||||||
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
|
|
||||||
import Clue from "../../assets/icons/questionsPage/clue";
|
|
||||||
import Branching from "../../assets/icons/questionsPage/branching";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
|
|
||||||
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
|
|
||||||
import { DeleteIcon } from "../../assets/icons/questionsPage/deleteIcon";
|
|
||||||
import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { quizStore } from "@root/quizes";
|
|
||||||
import {
|
|
||||||
questionStore,
|
|
||||||
copyQuestion,
|
|
||||||
removeQuestion,
|
|
||||||
removeQuestionForce,
|
|
||||||
updateQuestionsList,
|
|
||||||
} from "@root/questions";
|
|
||||||
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 {
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { copyQuestion, deleteQuestion, updateQuestionWithFnOptimistic } from "@root/questions/actions";
|
||||||
|
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
|
||||||
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
|
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
|
||||||
|
import Branching from "../../assets/icons/questionsPage/branching";
|
||||||
|
import Clue from "../../assets/icons/questionsPage/clue";
|
||||||
|
import { DeleteIcon } from "../../assets/icons/questionsPage/deleteIcon";
|
||||||
|
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
|
||||||
|
import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
|
||||||
|
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
|
||||||
|
import type { AnyQuizQuestion } from "../../model/questionTypes/shared";
|
||||||
|
|
||||||
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
switchState: string;
|
switchState: string;
|
||||||
SSHC: (data: string) => void;
|
SSHC: (data: string) => void;
|
||||||
totalIndex: number;
|
question: AnyQuizQuestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ButtonsOptionsAndPict({
|
export default function ButtonsOptionsAndPict({
|
||||||
SSHC,
|
SSHC,
|
||||||
switchState,
|
switchState,
|
||||||
totalIndex,
|
question,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [buttonHover, setButtonHover] = useState<string>("");
|
const [buttonHover, setButtonHover] = useState<string>("");
|
||||||
const quizId = Number(useParams().quizId);
|
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
|
||||||
const { listQuizes } = quizStore();
|
useState<boolean>(false);
|
||||||
const { listQuestions } = questionStore();
|
const theme = useTheme();
|
||||||
const [openedReallyChangingModal, setOpenedReallyChangingModal] =
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
useState<boolean>(false);
|
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
|
||||||
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
|
|
||||||
const quize = listQuizes[quizId];
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (question.deleteTimeoutId) {
|
if (question.deleteTimeoutId) {
|
||||||
clearTimeout(question.deleteTimeoutId);
|
clearTimeout(question.deleteTimeoutId);
|
||||||
}
|
}
|
||||||
}, [listQuestions]);
|
}, [question]);
|
||||||
|
|
||||||
const openedModal = () => {
|
return (
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
<Box
|
||||||
openedModalSettings: true,
|
sx={{
|
||||||
});
|
display: "flex",
|
||||||
};
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
return (
|
width: "100%",
|
||||||
<Box
|
background: "#f2f3f7",
|
||||||
sx={{
|
height: isMobile ? "92px" : "70px",
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
width: "100%",
|
|
||||||
background: "#f2f3f7",
|
|
||||||
height: isMobile ? "92px" : "70px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
padding: isMobile ? " 3px 12px 11px" : "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: isMobile ? "wrap" : "nowrap",
|
|
||||||
gap: "6px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MiniButtonSetting
|
|
||||||
onMouseEnter={() => setButtonHover("setting")}
|
|
||||||
onMouseLeave={() => setButtonHover("")}
|
|
||||||
onClick={() => {
|
|
||||||
SSHC("setting");
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
maxWidth: "104px",
|
|
||||||
minWidth: isIconMobile ? "30px" : "64px",
|
|
||||||
height: "30px",
|
|
||||||
backgroundColor:
|
|
||||||
switchState === "setting"
|
|
||||||
? theme.palette.brightPurple.main
|
|
||||||
: "transparent",
|
|
||||||
color:
|
|
||||||
switchState === "setting" ? "#ffffff" : theme.palette.grey3.main,
|
|
||||||
"&:hover": {
|
|
||||||
color:
|
|
||||||
switchState === "setting" ? theme.palette.grey3.main : null,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SettingIcon
|
|
||||||
color={
|
|
||||||
buttonHover === "setting"
|
|
||||||
? theme.palette.grey3.main
|
|
||||||
: switchState === "setting"
|
|
||||||
? "#ffffff"
|
|
||||||
: theme.palette.grey3.main
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isIconMobile ? null : "Настройки"}
|
|
||||||
</MiniButtonSetting>
|
|
||||||
<MiniButtonSetting
|
|
||||||
onMouseEnter={() => setButtonHover("help")}
|
|
||||||
onMouseLeave={() => setButtonHover("")}
|
|
||||||
onClick={() => {
|
|
||||||
SSHC("help");
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
minWidth: isIconMobile ? "30px" : "64px",
|
|
||||||
maxWidth: "102px",
|
|
||||||
height: "30px",
|
|
||||||
backgroundColor:
|
|
||||||
switchState === "help"
|
|
||||||
? theme.palette.brightPurple.main
|
|
||||||
: "transparent",
|
|
||||||
color:
|
|
||||||
switchState === "help" ? "#ffffff" : theme.palette.grey3.main,
|
|
||||||
"&:hover": {
|
|
||||||
color: switchState === "help" ? theme.palette.grey3.main : null,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Clue
|
|
||||||
color={
|
|
||||||
buttonHover === "help"
|
|
||||||
? theme.palette.grey3.main
|
|
||||||
: switchState === "help"
|
|
||||||
? "#ffffff"
|
|
||||||
: theme.palette.grey3.main
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isIconMobile ? null : "Подсказка"}
|
|
||||||
</MiniButtonSetting>
|
|
||||||
<>
|
|
||||||
<Tooltip
|
|
||||||
arrow
|
|
||||||
placement="right"
|
|
||||||
componentsProps={{
|
|
||||||
tooltip: {
|
|
||||||
sx: {
|
|
||||||
background: "#fff",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "#9A9AAF",
|
|
||||||
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
|
||||||
"& .MuiTooltip-arrow": {
|
|
||||||
color: "#FFF",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
title={
|
>
|
||||||
<Box>
|
<Box
|
||||||
<Typography
|
sx={{
|
||||||
sx={{
|
padding: isMobile ? " 3px 12px 11px" : "20px",
|
||||||
color: "#4D4D4D",
|
display: "flex",
|
||||||
fontWeight: "bold",
|
flexWrap: isMobile ? "wrap" : "nowrap",
|
||||||
fontSize: "14px",
|
gap: "6px",
|
||||||
marginBottom: "10px",
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
Будет показан при условии
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>
|
|
||||||
Название
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
fontSize: "12px",
|
|
||||||
marginBottom: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Условие 1, Условие 2
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>
|
|
||||||
Все условия обязательны
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MiniButtonSetting
|
|
||||||
onMouseEnter={() => setButtonHover("branching")}
|
|
||||||
onMouseLeave={() => setButtonHover("")}
|
|
||||||
onClick={() => {
|
|
||||||
SSHC("branching");
|
|
||||||
openedModal();
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
height: "30px",
|
|
||||||
maxWidth: "103px",
|
|
||||||
minWidth: isIconMobile ? "30px" : "64px",
|
|
||||||
backgroundColor:
|
|
||||||
switchState === "branching"
|
|
||||||
? theme.palette.brightPurple.main
|
|
||||||
: "transparent",
|
|
||||||
color:
|
|
||||||
switchState === "branching"
|
|
||||||
? "#ffffff"
|
|
||||||
: theme.palette.grey3.main,
|
|
||||||
"&:hover": {
|
|
||||||
color:
|
|
||||||
switchState === "branching"
|
|
||||||
? theme.palette.grey3.main
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Branching
|
<MiniButtonSetting
|
||||||
color={
|
onMouseEnter={() => setButtonHover("setting")}
|
||||||
buttonHover === "branching"
|
onMouseLeave={() => setButtonHover("")}
|
||||||
? theme.palette.grey3.main
|
onClick={() => {
|
||||||
: switchState === "branching"
|
SSHC("setting");
|
||||||
? "#ffffff"
|
}}
|
||||||
: theme.palette.grey3.main
|
sx={{
|
||||||
}
|
maxWidth: "104px",
|
||||||
/>
|
minWidth: isIconMobile ? "30px" : "64px",
|
||||||
{isIconMobile ? null : "Ветвление"}
|
height: "30px",
|
||||||
</MiniButtonSetting>
|
backgroundColor:
|
||||||
</Tooltip>
|
switchState === "setting"
|
||||||
<MiniButtonSetting
|
? theme.palette.brightPurple.main
|
||||||
onMouseEnter={() => setButtonHover("image")}
|
: "transparent",
|
||||||
onMouseLeave={() => setButtonHover("")}
|
color:
|
||||||
onClick={() => {
|
switchState === "setting" ? "#ffffff" : theme.palette.grey3.main,
|
||||||
SSHC("image");
|
"&:hover": {
|
||||||
}}
|
color:
|
||||||
sx={{
|
switchState === "setting" ? theme.palette.grey3.main : null,
|
||||||
height: "30px",
|
},
|
||||||
maxWidth: "123px",
|
}}
|
||||||
minWidth: isIconMobile ? "30px" : "64px",
|
>
|
||||||
backgroundColor:
|
<SettingIcon
|
||||||
switchState === "image"
|
color={
|
||||||
? theme.palette.brightPurple.main
|
buttonHover === "setting"
|
||||||
: "transparent",
|
? theme.palette.grey3.main
|
||||||
color:
|
: switchState === "setting"
|
||||||
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
|
? "#ffffff"
|
||||||
"&:hover": {
|
: theme.palette.grey3.main
|
||||||
color:
|
}
|
||||||
switchState === "image" ? theme.palette.grey3.main : null,
|
/>
|
||||||
},
|
{isIconMobile ? null : "Настройки"}
|
||||||
}}
|
</MiniButtonSetting>
|
||||||
>
|
<MiniButtonSetting
|
||||||
<ImgIcon
|
onMouseEnter={() => setButtonHover("help")}
|
||||||
color={
|
onMouseLeave={() => setButtonHover("")}
|
||||||
buttonHover === "image"
|
onClick={() => {
|
||||||
? theme.palette.grey3.main
|
SSHC("help");
|
||||||
: switchState === "image"
|
}}
|
||||||
? "#ffffff"
|
sx={{
|
||||||
: theme.palette.grey3.main
|
minWidth: isIconMobile ? "30px" : "64px",
|
||||||
}
|
maxWidth: "102px",
|
||||||
|
height: "30px",
|
||||||
|
backgroundColor:
|
||||||
|
switchState === "help"
|
||||||
|
? theme.palette.brightPurple.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
switchState === "help" ? "#ffffff" : theme.palette.grey3.main,
|
||||||
|
"&:hover": {
|
||||||
|
color: switchState === "help" ? theme.palette.grey3.main : null,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Clue
|
||||||
|
color={
|
||||||
|
buttonHover === "help"
|
||||||
|
? theme.palette.grey3.main
|
||||||
|
: switchState === "help"
|
||||||
|
? "#ffffff"
|
||||||
|
: theme.palette.grey3.main
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isIconMobile ? null : "Подсказка"}
|
||||||
|
</MiniButtonSetting>
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
arrow
|
||||||
|
placement="right"
|
||||||
|
componentsProps={{
|
||||||
|
tooltip: {
|
||||||
|
sx: {
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: "6px",
|
||||||
|
color: "#9A9AAF",
|
||||||
|
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||||
|
"& .MuiTooltip-arrow": {
|
||||||
|
color: "#FFF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: "#4D4D4D",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: "14px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Будет показан при условии
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>
|
||||||
|
Название
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: "12px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Условие 1, Условие 2
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>
|
||||||
|
Все условия обязательны
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MiniButtonSetting
|
||||||
|
onMouseEnter={() => setButtonHover("branching")}
|
||||||
|
onMouseLeave={() => setButtonHover("")}
|
||||||
|
onClick={() => {
|
||||||
|
SSHC("branching");
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.openedModalSettings = true;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: "30px",
|
||||||
|
maxWidth: "103px",
|
||||||
|
minWidth: isIconMobile ? "30px" : "64px",
|
||||||
|
backgroundColor:
|
||||||
|
switchState === "branching"
|
||||||
|
? theme.palette.brightPurple.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
switchState === "branching"
|
||||||
|
? "#ffffff"
|
||||||
|
: theme.palette.grey3.main,
|
||||||
|
"&:hover": {
|
||||||
|
color:
|
||||||
|
switchState === "branching"
|
||||||
|
? theme.palette.grey3.main
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Branching
|
||||||
|
color={
|
||||||
|
buttonHover === "branching"
|
||||||
|
? theme.palette.grey3.main
|
||||||
|
: switchState === "branching"
|
||||||
|
? "#ffffff"
|
||||||
|
: theme.palette.grey3.main
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isIconMobile ? null : "Ветвление"}
|
||||||
|
</MiniButtonSetting>
|
||||||
|
</Tooltip>
|
||||||
|
<MiniButtonSetting
|
||||||
|
onMouseEnter={() => setButtonHover("image")}
|
||||||
|
onMouseLeave={() => setButtonHover("")}
|
||||||
|
onClick={() => {
|
||||||
|
SSHC("image");
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: "30px",
|
||||||
|
maxWidth: "123px",
|
||||||
|
minWidth: isIconMobile ? "30px" : "64px",
|
||||||
|
backgroundColor:
|
||||||
|
switchState === "image"
|
||||||
|
? theme.palette.brightPurple.main
|
||||||
|
: "transparent",
|
||||||
|
color:
|
||||||
|
switchState === "image" ? "#ffffff" : theme.palette.grey3.main,
|
||||||
|
"&:hover": {
|
||||||
|
color:
|
||||||
|
switchState === "image" ? theme.palette.grey3.main : null,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImgIcon
|
||||||
|
color={
|
||||||
|
buttonHover === "image"
|
||||||
|
? theme.palette.grey3.main
|
||||||
|
: switchState === "image"
|
||||||
|
? "#ffffff"
|
||||||
|
: theme.palette.grey3.main
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isIconMobile ? null : "Изображение"}
|
||||||
|
</MiniButtonSetting>
|
||||||
|
<MiniButtonSetting
|
||||||
|
onClick={() => setOpenedReallyChangingModal(true)}
|
||||||
|
sx={{
|
||||||
|
minWidth: "30px",
|
||||||
|
height: "30px",
|
||||||
|
backgroundColor: "#FEDFD0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
|
||||||
|
</MiniButtonSetting>
|
||||||
|
<MiniButtonSetting
|
||||||
|
onClick={() => setOpenedReallyChangingModal(true)}
|
||||||
|
sx={{
|
||||||
|
minWidth: "30px",
|
||||||
|
height: "30px",
|
||||||
|
backgroundColor: "#FEDFD0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
|
||||||
|
</MiniButtonSetting>
|
||||||
|
<MiniButtonSetting
|
||||||
|
onClick={() => setOpenedReallyChangingModal(true)}
|
||||||
|
sx={{
|
||||||
|
minWidth: "30px",
|
||||||
|
height: "30px",
|
||||||
|
backgroundColor: "#FEDFD0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
|
||||||
|
</MiniButtonSetting>
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton sx={{ borderRadius: "6px", padding: "0px 2px" }}>
|
||||||
|
<HideIcon style={{ color: "#4D4D4D" }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px" }}
|
||||||
|
onClick={() => copyQuestion(question.id, question.quizId)}
|
||||||
|
>
|
||||||
|
<CopyIcon style={{ color: "#4D4D4D" }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => { // TODO
|
||||||
|
// const removedId = question.id;
|
||||||
|
// if (question.deleteTimeoutId) {
|
||||||
|
// clearTimeout(question.deleteTimeoutId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// removeQuestion(quizId, totalIndex);
|
||||||
|
|
||||||
|
// const newTimeoutId = window.setTimeout(() => {
|
||||||
|
// removeQuestionForce(quizId, removedId);
|
||||||
|
// }, 5000);
|
||||||
|
|
||||||
|
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
||||||
|
// ...question,
|
||||||
|
// deleteTimeoutId: newTimeoutId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
deleteQuestion(question.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{ color: "#4D4D4D" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<ReallyChangingModal
|
||||||
|
opened={openedReallyChangingModal}
|
||||||
|
onClose={() => setOpenedReallyChangingModal(false)}
|
||||||
/>
|
/>
|
||||||
{isIconMobile ? null : "Изображение"}
|
</Box>
|
||||||
</MiniButtonSetting>
|
);
|
||||||
<MiniButtonSetting
|
|
||||||
onClick={() => setOpenedReallyChangingModal(true)}
|
|
||||||
sx={{
|
|
||||||
minWidth: "30px",
|
|
||||||
height: "30px",
|
|
||||||
backgroundColor: "#FEDFD0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DoubleTick style={{ color: "#FC712F", fontSize: "9px" }} />
|
|
||||||
</MiniButtonSetting>
|
|
||||||
<MiniButtonSetting
|
|
||||||
onClick={() => setOpenedReallyChangingModal(true)}
|
|
||||||
sx={{
|
|
||||||
minWidth: "30px",
|
|
||||||
height: "30px",
|
|
||||||
backgroundColor: "#FEDFD0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DoubleArrowRight style={{ color: "#FC712F", fontSize: "9px" }} />
|
|
||||||
</MiniButtonSetting>
|
|
||||||
<MiniButtonSetting
|
|
||||||
onClick={() => setOpenedReallyChangingModal(true)}
|
|
||||||
sx={{
|
|
||||||
minWidth: "30px",
|
|
||||||
height: "30px",
|
|
||||||
backgroundColor: "#FEDFD0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
|
|
||||||
</MiniButtonSetting>
|
|
||||||
</>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
padding: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton sx={{ borderRadius: "6px", padding: "0px 2px" }}>
|
|
||||||
<HideIcon style={{ color: "#4D4D4D" }} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
sx={{ borderRadius: "6px" }}
|
|
||||||
onClick={() => copyQuestion(quizId, totalIndex)}
|
|
||||||
>
|
|
||||||
<CopyIcon style={{ color: "#4D4D4D" }} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
sx={{ borderRadius: "6px", padding: "2px" }}
|
|
||||||
onClick={() => {
|
|
||||||
const removedId = question.id;
|
|
||||||
if (question.deleteTimeoutId) {
|
|
||||||
clearTimeout(question.deleteTimeoutId);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeQuestion(quizId, totalIndex);
|
|
||||||
|
|
||||||
const newTimeoutId = window.setTimeout(() => {
|
|
||||||
removeQuestionForce(quizId, removedId);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
...question,
|
|
||||||
deleteTimeoutId: newTimeoutId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon style={{ color: "#4D4D4D" }} />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<ReallyChangingModal
|
|
||||||
opened={openedReallyChangingModal}
|
|
||||||
onClose={() => setOpenedReallyChangingModal(false)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -11,21 +11,7 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
|
||||||
import TypeQuestions from "../TypeQuestions";
|
|
||||||
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
|
||||||
|
|
||||||
import {
|
|
||||||
copyQuestion,
|
|
||||||
createQuestion,
|
|
||||||
removeQuestion,
|
|
||||||
removeQuestionForce,
|
|
||||||
updateQuestionsList
|
|
||||||
} from "@root/questions";
|
|
||||||
|
|
||||||
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
|
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
|
||||||
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
|
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
|
||||||
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
|
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
|
||||||
@ -45,9 +31,12 @@ 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 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";
|
||||||
|
import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
||||||
|
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -59,7 +48,6 @@ interface Props {
|
|||||||
export default function QuestionsPageCard({ question, draggableProps, isDragging }: Props) {
|
export default function QuestionsPageCard({ question, draggableProps, isDragging }: Props) {
|
||||||
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
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));
|
||||||
@ -166,11 +154,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
sx={{ padding: "0", margin: "5px" }}
|
sx={{ padding: "0", margin: "5px" }}
|
||||||
disableRipple
|
disableRipple
|
||||||
data-cy="expand-question"
|
data-cy="expand-question"
|
||||||
onClick={() =>
|
onClick={() => toggleExpandQuestion(question.id)}
|
||||||
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, {
|
|
||||||
expanded: !question.expanded,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{question.expanded ? (
|
{question.expanded ? (
|
||||||
<ArrowDownIcon
|
<ArrowDownIcon
|
||||||
@ -231,7 +215,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
sx={{ padding: "0" }}
|
sx={{ padding: "0" }}
|
||||||
onClick={() => copyQuestion(quizId, totalIndex)}
|
onClick={() => copyQuestion(question.id, question.quizId)}
|
||||||
>
|
>
|
||||||
<CopyIcon
|
<CopyIcon
|
||||||
style={{ color: theme.palette.brightPurple.main }}
|
style={{ color: theme.palette.brightPurple.main }}
|
||||||
@ -244,22 +228,24 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
padding: "0",
|
padding: "0",
|
||||||
margin: "0 5px 0 10px",
|
margin: "0 5px 0 10px",
|
||||||
}}
|
}}
|
||||||
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<AnyQuizQuestion>(quizId, totalIndex, {
|
// updateQuestionsList<AnyQuizQuestion>(quizId, totalIndex, {
|
||||||
...question,
|
// ...question,
|
||||||
deleteTimeoutId: newTimeoutId,
|
// deleteTimeoutId: newTimeoutId,
|
||||||
});
|
// });
|
||||||
|
|
||||||
|
deleteQuestion(question.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon
|
<DeleteIcon
|
||||||
@ -301,11 +287,11 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{question.type === "nonselected" ? (
|
{/* {question.type === "nonselected" ? (
|
||||||
<TypeQuestions question={question} />
|
<TypeQuestions question={question} />
|
||||||
) : (
|
) : ( */}
|
||||||
<SwitchQuestionsPage question={question} />
|
<SwitchQuestionsPage question={question} />
|
||||||
)}
|
{/* )} */}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -58,7 +58,7 @@ export default function DropDown({ totalIndex }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
question={totalIndex}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box
|
||||||
|
@ -48,7 +48,7 @@ export default function Emoji({ totalIndex }: Props) {
|
|||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
question={totalIndex}
|
||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isTablet && (
|
{!isTablet && (
|
||||||
|
@ -24,9 +24,9 @@ import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
|
|||||||
|
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import type {
|
import type {
|
||||||
QuizQuestionType,
|
|
||||||
QuizQuestionBase,
|
QuizQuestionBase,
|
||||||
} from "../../../../model/questionTypes/shared";
|
} from "../../../../model/questionTypes/shared";
|
||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
|
|
||||||
type ChooseAnswerModalProps = {
|
type ChooseAnswerModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -44,7 +44,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 quizId = Number(useParams().quizId);
|
const quizId = Number(useParams().quizId);
|
||||||
const { listQuestions } = questionStore();
|
const { listQuestions } = questionStore();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
@ -158,11 +158,11 @@ export default function QuestionsPageCard({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{question.type === "nonselected" ? (
|
{/* {question.type === "" ? (
|
||||||
<FormTypeQuestions totalIndex={totalIndex} />
|
<FormTypeQuestions totalIndex={totalIndex} />
|
||||||
) : (
|
) : ( */}
|
||||||
<SwitchQuestionsPage totalIndex={totalIndex} />
|
<SwitchQuestionsPage totalIndex={totalIndex} />
|
||||||
)}
|
{/* )} */}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</>
|
</>
|
||||||
|
@ -22,9 +22,9 @@ import {
|
|||||||
} from "@root/questions";
|
} from "@root/questions";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
QuizQuestionType,
|
|
||||||
QuizQuestionBase,
|
QuizQuestionBase,
|
||||||
} from "../../../model/questionTypes/shared";
|
} from "../../../model/questionTypes/shared";
|
||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
@ -33,7 +33,7 @@ interface Props {
|
|||||||
type ButtonTypeQuestion = {
|
type ButtonTypeQuestion = {
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
title: string;
|
title: string;
|
||||||
value: QuizQuestionType;
|
value: QuestionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [
|
const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [
|
||||||
|
@ -71,7 +71,7 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
question={totalIndex}
|
||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
|
@ -1,44 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme
|
||||||
useMediaQuery,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
import { setVariantImageUrl, setVariantOriginalImageUrl, updateQuestionsList } from "@root/questions";
|
||||||
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||||
import { questionStore, setVariantImageUrl, updateQuestionsList, setVariantOriginalImageUrl } from "@root/questions";
|
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
import ButtonsOptions from "../ButtonsOptions";
|
import ButtonsOptions from "../ButtonsOptions";
|
||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
|
|
||||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
|
||||||
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
||||||
|
|
||||||
import { openCropModal } from "@root/cropModal";
|
|
||||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
|
||||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: QuizQuestionImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsPicture({ totalIndex }: Props) {
|
export default function OptionsPicture({ question }: Props) {
|
||||||
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
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 quizId = Number(useParams().quizId);
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
|
|
||||||
|
|
||||||
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) return;
|
||||||
@ -56,117 +51,117 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
|||||||
const answerNew = question.content.variants.slice();
|
const answerNew = question.content.variants.slice();
|
||||||
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
|
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(url: string) {
|
||||||
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
question={totalIndex}
|
||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
if (variant.extendedText) {
|
if (variant.extendedText) {
|
||||||
return openCropModal(
|
return openCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl
|
variant.originalImageUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploadImageModalOpen(true);
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
onPlusClick={() => {
|
onPlusClick={() => {
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setIsUploadImageModalOpen(true);
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
sx={{ mx: "10px" }}
|
sx={{ mx: "10px" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
additionalMobile={(variant, index) => (
|
additionalMobile={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
if (variant.extendedText) {
|
if (variant.extendedText) {
|
||||||
return openCropModal(
|
return openCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl
|
variant.originalImageUrl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsUploadImageModalOpen(true);
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
onPlusClick={() => {
|
onPlusClick={() => {
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setIsUploadImageModalOpen(true);
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
sx={{ m: "8px", width: "auto" }}
|
sx={{ m: "8px", width: "auto" }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<UploadImageModal
|
<UploadImageModal
|
||||||
open={isUploadImageModalOpen}
|
open={isUploadImageModalOpen}
|
||||||
onClose={() => setIsUploadImageModalOpen(false)}
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
imgHC={handleImageUpload}
|
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={addNewAnswer}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
</Link>
|
</Link>
|
||||||
{isMobile ? null : (
|
{isMobile ? null : (
|
||||||
<>
|
<>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
lineHeight: "21.33px",
|
lineHeight: "21.33px",
|
||||||
color: theme.palette.grey2.main,
|
color: theme.palette.grey2.main,
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
или нажмите Enter
|
или нажмите Enter
|
||||||
</Typography>
|
</Typography>
|
||||||
<EnterIcon
|
<EnterIcon
|
||||||
style={{
|
style={{
|
||||||
color: "#7E2AEA",
|
color: "#7E2AEA",
|
||||||
fontSize: "24px",
|
fontSize: "24px",
|
||||||
marginLeft: "6px",
|
marginLeft: "6px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
|
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
|
||||||
<SwitchAnswerOptionsPict switchState={switchState} totalIndex={totalIndex} />
|
<SwitchAnswerOptionsPict switchState={switchState} totalIndex={totalIndex} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,58 @@
|
|||||||
import * as React from "react";
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
import AnswerOptions from "./answerOptions/AnswerOptions";
|
|
||||||
import OptionsPicture from "./OptionsPicture/OptionsPicture";
|
|
||||||
import DataOptions from "./DataOptions/DataOptions";
|
import DataOptions from "./DataOptions/DataOptions";
|
||||||
import SliderOptions from "./SliderOptions/SliderOptions";
|
import DropDown from "./DropDown/DropDown";
|
||||||
|
import Emoji from "./Emoji/Emoji";
|
||||||
|
import OptionsAndPicture from "./OptionsAndPicture/OptionsAndPicture";
|
||||||
|
import OptionsPicture from "./OptionsPicture/OptionsPicture";
|
||||||
import OwnTextField from "./OwnTextField/OwnTextField";
|
import OwnTextField from "./OwnTextField/OwnTextField";
|
||||||
import PageOptions from "./PageOptions/PageOptions";
|
import PageOptions from "./PageOptions/PageOptions";
|
||||||
import OptionsAndPicture from "./OptionsAndPicture/OptionsAndPicture";
|
|
||||||
import RatingOptions from "./RatingOptions/RatingOptions";
|
import RatingOptions from "./RatingOptions/RatingOptions";
|
||||||
import Emoji from "./Emoji/Emoji";
|
import SliderOptions from "./SliderOptions/SliderOptions";
|
||||||
import DropDown from "./DropDown/DropDown";
|
|
||||||
import UploadFile from "./UploadFile/UploadFile";
|
import UploadFile from "./UploadFile/UploadFile";
|
||||||
import { useParams } from "react-router-dom";
|
import AnswerOptions from "./answerOptions/AnswerOptions";
|
||||||
import { questionStore } from "@root/questions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: AnyQuizQuestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SwitchQuestionsPage({ totalIndex }: Props) {
|
export default function SwitchQuestionsPage({ question }: Props) {
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
|
|
||||||
const switchState = listQuestions[quizId][totalIndex].type;
|
switch (question.type) {
|
||||||
switch (switchState) {
|
case "variant":
|
||||||
case "variant":
|
return <AnswerOptions question={question} />;
|
||||||
return <AnswerOptions totalIndex={totalIndex} />;
|
|
||||||
|
|
||||||
case "images":
|
case "images":
|
||||||
return <OptionsPicture totalIndex={totalIndex} />;
|
return <OptionsPicture question={question} />;
|
||||||
|
|
||||||
case "varimg":
|
case "varimg":
|
||||||
return <OptionsAndPicture totalIndex={totalIndex} />;
|
return <OptionsAndPicture question={question} />;
|
||||||
|
|
||||||
case "emoji":
|
case "emoji":
|
||||||
return <Emoji totalIndex={totalIndex} />;
|
return <Emoji question={question} />;
|
||||||
|
|
||||||
case "text":
|
case "text":
|
||||||
return <OwnTextField totalIndex={totalIndex} />;
|
return <OwnTextField question={question} />;
|
||||||
|
|
||||||
case "select":
|
case "select":
|
||||||
return <DropDown totalIndex={totalIndex} />;
|
return <DropDown question={question} />;
|
||||||
|
|
||||||
case "date":
|
case "date":
|
||||||
return <DataOptions totalIndex={totalIndex} />;
|
return <DataOptions question={question} />;
|
||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
return <SliderOptions totalIndex={totalIndex} />;
|
return <SliderOptions question={question} />;
|
||||||
|
|
||||||
case "file":
|
case "file":
|
||||||
return <UploadFile totalIndex={totalIndex} />;
|
return <UploadFile question={question} />;
|
||||||
|
|
||||||
case "page":
|
case "page":
|
||||||
return <PageOptions totalIndex={totalIndex} />;
|
return <PageOptions question={question} />;
|
||||||
|
|
||||||
case "rating":
|
case "rating":
|
||||||
return <RatingOptions totalIndex={totalIndex} />;
|
return <RatingOptions question={question} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,100 +11,102 @@ import OptionsPict from "../../assets/icons/questionsPage/options_pict";
|
|||||||
import Page from "../../assets/icons/questionsPage/page";
|
import Page from "../../assets/icons/questionsPage/page";
|
||||||
import RatingIcon from "../../assets/icons/questionsPage/rating";
|
import RatingIcon from "../../assets/icons/questionsPage/rating";
|
||||||
import Slider from "../../assets/icons/questionsPage/slider";
|
import Slider from "../../assets/icons/questionsPage/slider";
|
||||||
import { setQuestionFieldOptimistic } from "@root/questions/actions";
|
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
|
||||||
import type {
|
import type {
|
||||||
AnyQuizQuestion,
|
AnyQuizQuestion,
|
||||||
QuizQuestionType
|
|
||||||
} from "../../model/questionTypes/shared";
|
} from "../../model/questionTypes/shared";
|
||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
question: AnyQuizQuestion;
|
question: AnyQuizQuestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonTypeQuestion = {
|
type ButtonTypeQuestion = {
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
title: string;
|
title: string;
|
||||||
value: QuizQuestionType;
|
value: QuestionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TypeQuestions({ question }: Props) {
|
export default function TypeQuestions({ question }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: "20px",
|
gap: "20px",
|
||||||
padding: "8px 20px 20px",
|
padding: "8px 20px 20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
||||||
<QuestionsMiniButton
|
<QuestionsMiniButton
|
||||||
key={title}
|
key={title}
|
||||||
dataCy={`select-questiontype-${value}`}
|
dataCy={`select-questiontype-${value}`}
|
||||||
onClick={() => setQuestionFieldOptimistic(question.id, "type", value)}
|
onClick={() => updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
icon={icon}
|
question.type = value;
|
||||||
text={title}
|
})}
|
||||||
/>
|
icon={icon}
|
||||||
))}
|
text={title}
|
||||||
</Box>
|
/>
|
||||||
);
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
|
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
|
||||||
{
|
{
|
||||||
icon: <Answer color="#9A9AAF" />,
|
icon: <Answer color="#9A9AAF" />,
|
||||||
title: "Варианты ответов",
|
title: "Варианты ответов",
|
||||||
value: "variant",
|
value: "variant",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <OptionsPict color="#9A9AAF" />,
|
icon: <OptionsPict color="#9A9AAF" />,
|
||||||
title: "Варианты с картинками",
|
title: "Варианты с картинками",
|
||||||
value: "images",
|
value: "images",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <OptionsAndPict color="#9A9AAF" />,
|
icon: <OptionsAndPict color="#9A9AAF" />,
|
||||||
title: "Варианты и картинка",
|
title: "Варианты и картинка",
|
||||||
value: "varimg",
|
value: "varimg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Emoji color="#9A9AAF" />,
|
icon: <Emoji color="#9A9AAF" />,
|
||||||
title: "Эмоджи",
|
title: "Эмоджи",
|
||||||
value: "emoji",
|
value: "emoji",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Input color="#9A9AAF" />,
|
icon: <Input color="#9A9AAF" />,
|
||||||
title: "Своё поле для ввода",
|
title: "Своё поле для ввода",
|
||||||
value: "text",
|
value: "text",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DropDown color="#9A9AAF" />,
|
icon: <DropDown color="#9A9AAF" />,
|
||||||
title: "Выпадающий список",
|
title: "Выпадающий список",
|
||||||
value: "select",
|
value: "select",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Date color="#9A9AAF" />,
|
icon: <Date color="#9A9AAF" />,
|
||||||
title: "Дата",
|
title: "Дата",
|
||||||
value: "date",
|
value: "date",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Slider color="#9A9AAF" />,
|
icon: <Slider color="#9A9AAF" />,
|
||||||
title: "Ползунок",
|
title: "Ползунок",
|
||||||
value: "number",
|
value: "number",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Download color="#9A9AAF" />,
|
icon: <Download color="#9A9AAF" />,
|
||||||
title: "Загрузка файла",
|
title: "Загрузка файла",
|
||||||
value: "file",
|
value: "file",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Page color="#9A9AAF" />,
|
icon: <Page color="#9A9AAF" />,
|
||||||
title: "Страница",
|
title: "Страница",
|
||||||
value: "page",
|
value: "page",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <RatingIcon color="#9A9AAF" />,
|
icon: <RatingIcon color="#9A9AAF" />,
|
||||||
title: "Рейтинг",
|
title: "Рейтинг",
|
||||||
value: "rating",
|
value: "rating",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
|
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||||
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||||
import { questionStore, setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions";
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
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 * as React from "react";
|
import { useState, type DragEvent } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import UploadIcon from "../../../assets/icons/UploadIcon";
|
import UploadIcon from "../../../assets/icons/UploadIcon";
|
||||||
import { UploadImageModal } from "./UploadImageModal";
|
import { UploadImageModal } from "./UploadImageModal";
|
||||||
|
|
||||||
import { openCropModal } from "@root/cropModal";
|
|
||||||
import { QuizQuestionBase } from "model/questionTypes/shared";
|
|
||||||
import type { DragEvent } from "react";
|
|
||||||
|
|
||||||
type UploadImageProps = {
|
type UploadImageProps = {
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UploadImage({ totalIndex }: UploadImageProps) {
|
export default function UploadImage({ question }: UploadImageProps) {
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = React.useState(false);
|
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
|
|
||||||
|
|
||||||
const handleImageUpload = (files: FileList | null) => {
|
const handleImageUpload = (files: FileList | null) => {
|
||||||
if (!files?.length) return;
|
if (!files?.length) return;
|
||||||
@ -29,8 +24,8 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
|
|||||||
|
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
setQuestionBackgroundImage(quizId, totalIndex, url);
|
setQuestionBackgroundImage(question.id, url);
|
||||||
setQuestionOriginalBackgroundImage(quizId, totalIndex, url);
|
setQuestionOriginalBackgroundImage(question.id, url);
|
||||||
setIsUploadImageModalOpen(false);
|
setIsUploadImageModalOpen(false);
|
||||||
openCropModal(url, url);
|
openCropModal(url, url);
|
||||||
};
|
};
|
||||||
@ -43,7 +38,7 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(url: string) {
|
||||||
setQuestionBackgroundImage(quizId, totalIndex, url);
|
setQuestionBackgroundImage(question.id, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,111 +1,98 @@
|
|||||||
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Box, Typography, Link, useTheme, useMediaQuery } from "@mui/material";
|
|
||||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||||
import SwitchAnswerOptions from "./switchAnswerOptions";
|
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
import SwitchAnswerOptions from "./switchAnswerOptions";
|
||||||
|
|
||||||
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||||
|
import { addQuestionVariant } from "@root/questions/actions";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AnswerOptions({ totalIndex }: Props) {
|
export default function AnswerOptions({ question }: Props) {
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const quizId = Number(useParams().quizId);
|
const theme = useTheme();
|
||||||
const { listQuestions } = questionStore();
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionVariant;
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNewAnswer = () => {
|
return (
|
||||||
const answerNew = question.content.variants.slice();
|
<>
|
||||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
<Box sx={{ padding: "0 20px 20px 20px" }}>
|
||||||
|
{question.content.variants.length === 0 ? (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
padding: "0 0 33px 80px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "18px",
|
||||||
|
lineHeight: "21.33px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавьте ответ
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<AnswerDraggableList
|
||||||
|
variants={question.content.variants}
|
||||||
|
question={question}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
<Box
|
||||||
content: { ...question.content, variants: answerNew },
|
sx={{
|
||||||
});
|
display: "flex",
|
||||||
};
|
alignItems: "center",
|
||||||
|
marginBottom: "17px",
|
||||||
return (
|
}}
|
||||||
<>
|
>
|
||||||
<Box sx={{ padding: "0 20px 20px 20px" }}>
|
<Link
|
||||||
{question.content.variants.length === 0 ? (
|
component="button"
|
||||||
<Typography
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
padding: "0 0 33px 80px",
|
color: theme.palette.brightPurple.main,
|
||||||
fontWeight: 400,
|
fontWeight: "400",
|
||||||
fontSize: "18px",
|
fontSize: "16px",
|
||||||
lineHeight: "21.33px",
|
mr: "4px",
|
||||||
color: theme.palette.grey2.main,
|
height: "19px",
|
||||||
}}
|
}}
|
||||||
>
|
onClick={() => addQuestionVariant(question.id)}
|
||||||
Добавьте ответ
|
>
|
||||||
</Typography>
|
Добавьте ответ
|
||||||
) : (
|
</Link>
|
||||||
<AnswerDraggableList
|
{isMobile ? null : (
|
||||||
variants={question.content.variants}
|
<>
|
||||||
totalIndex={totalIndex}
|
<Typography
|
||||||
/>
|
sx={{
|
||||||
)}
|
fontWeight: 400,
|
||||||
|
lineHeight: "21.33px",
|
||||||
<Box
|
color: theme.palette.grey2.main,
|
||||||
sx={{
|
fontSize: "16px",
|
||||||
display: "flex",
|
}}
|
||||||
alignItems: "center",
|
>
|
||||||
marginBottom: "17px",
|
или нажмите Enter
|
||||||
}}
|
</Typography>
|
||||||
>
|
<EnterIcon
|
||||||
<Link
|
style={{
|
||||||
component="button"
|
color: "#7E2AEA",
|
||||||
variant="body2"
|
fontSize: "24px",
|
||||||
sx={{
|
marginLeft: "6px",
|
||||||
color: theme.palette.brightPurple.main,
|
}}
|
||||||
fontWeight: "400",
|
/>
|
||||||
fontSize: "16px",
|
</>
|
||||||
mr: "4px",
|
)}
|
||||||
height: "19px",
|
</Box>
|
||||||
}}
|
</Box>
|
||||||
onClick={addNewAnswer}
|
<ButtonsOptionsAndPict
|
||||||
>
|
switchState={switchState}
|
||||||
Добавьте ответ
|
SSHC={SSHC}
|
||||||
</Link>
|
question={question}
|
||||||
{isMobile ? null : (
|
/>
|
||||||
<>
|
<SwitchAnswerOptions switchState={switchState} question={question} />
|
||||||
<Typography
|
</>
|
||||||
sx={{
|
);
|
||||||
fontWeight: 400,
|
|
||||||
lineHeight: "21.33px",
|
|
||||||
color: theme.palette.grey2.main,
|
|
||||||
fontSize: "16px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
или нажмите Enter
|
|
||||||
</Typography>
|
|
||||||
<EnterIcon
|
|
||||||
style={{
|
|
||||||
color: "#7E2AEA",
|
|
||||||
fontSize: "24px",
|
|
||||||
marginLeft: "6px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<ButtonsOptionsAndPict
|
|
||||||
switchState={switchState}
|
|
||||||
SSHC={SSHC}
|
|
||||||
totalIndex={totalIndex}
|
|
||||||
/>
|
|
||||||
<SwitchAnswerOptions switchState={switchState} totalIndex={totalIndex} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,181 +1,178 @@
|
|||||||
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 { useDebouncedCallback } from "use-debounce";
|
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
|
||||||
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||||
import InfoIcon from "../../../assets/icons/InfoIcon";
|
|
||||||
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 type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ResponseSettings({ totalIndex }: Props) {
|
export default function ResponseSettings({ question }: Props) {
|
||||||
const quizId = Number(useParams().quizId);
|
const theme = useTheme();
|
||||||
const { listQuestions } = questionStore();
|
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
||||||
const theme = useTheme();
|
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
|
||||||
|
|
||||||
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 QuizQuestionVariant;
|
|
||||||
const debounced = useDebouncedCallback((value) => {
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, innerName: value },
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return (
|
const updateQuestionInnerName = useDebouncedCallback((value) => {
|
||||||
<Box
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
sx={{
|
question.content.innerName = value;
|
||||||
display: "flex",
|
});
|
||||||
justifyContent: "space-between",
|
}, 1000);
|
||||||
flexDirection: isTablet ? "column" : "none",
|
|
||||||
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
|
return (
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
pt: isMobile ? "25px" : "20px",
|
|
||||||
pb: isMobile ? "25px" : "20px",
|
|
||||||
pl: "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "14px",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
height: isMobile ? "18px" : "auto",
|
|
||||||
fontWeight: "500",
|
|
||||||
fontSize: "18px",
|
|
||||||
color: " #4D4D4D",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Настройки ответов
|
|
||||||
</Typography>
|
|
||||||
<CustomCheckbox
|
|
||||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
|
||||||
label={"Длинный текстовый ответ"}
|
|
||||||
checked={question.content.largeCheck}
|
|
||||||
handleChange={({ target }) => {
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
largeCheck: target.checked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CustomCheckbox
|
|
||||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
|
||||||
label={"Можно несколько"}
|
|
||||||
checked={question.content.multi}
|
|
||||||
dataCy="multiple-answers-checkbox"
|
|
||||||
handleChange={({ target }) => {
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, multi: target.checked },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<CustomCheckbox
|
|
||||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
|
||||||
label={'Вариант "свой ответ"'}
|
|
||||||
checked={question.content.own}
|
|
||||||
handleChange={({ target }) => {
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
|
||||||
content: { ...question.content, own: target.checked },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
boxSizing: "border-box",
|
|
||||||
pt: isMobile ? "0px" : "20px",
|
|
||||||
pb: "20px",
|
|
||||||
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
|
|
||||||
pr: isFigmaTablte ? "19px" : "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "14px",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
height: isMobile ? "18px" : "auto",
|
|
||||||
fontWeight: "500",
|
|
||||||
fontSize: "18px",
|
|
||||||
color: " #4D4D4D",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Настройки вопросов
|
|
||||||
</Typography>
|
|
||||||
<CustomCheckbox
|
|
||||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
|
||||||
label={"Необязательный вопрос"}
|
|
||||||
checked={!question.required}
|
|
||||||
handleChange={({ target }) => {
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
|
||||||
required: !target.checked,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
|
||||||
width: isMobile ? "90%" : "auto",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CustomCheckbox
|
|
||||||
sx={{
|
sx={{
|
||||||
mr: isMobile ? "0px" : "9px",
|
display: "flex",
|
||||||
height: isMobile ? "42px" : "26px",
|
justifyContent: "space-between",
|
||||||
alignItems: "start",
|
flexDirection: isTablet ? "column" : "none",
|
||||||
|
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
|
||||||
}}
|
}}
|
||||||
label={"Внутреннее название вопроса"}
|
>
|
||||||
checked={question.content.innerNameCheck}
|
<Box
|
||||||
handleChange={({ target }) => {
|
sx={{
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
pt: isMobile ? "25px" : "20px",
|
||||||
content: {
|
pb: isMobile ? "25px" : "20px",
|
||||||
...question.content,
|
pl: "20px",
|
||||||
innerNameCheck: target.checked,
|
display: "flex",
|
||||||
innerName: target.checked ? question.content.innerName : "",
|
flexDirection: "column",
|
||||||
},
|
gap: "14px",
|
||||||
});
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
{isMobile && (
|
|
||||||
<Tooltip
|
|
||||||
title="Будет отображаться как заголовок вопроса в приходящих заявках."
|
|
||||||
placement="top"
|
|
||||||
>
|
>
|
||||||
<Box>
|
<Typography
|
||||||
<InfoIcon />
|
sx={{
|
||||||
</Box>
|
height: isMobile ? "18px" : "auto",
|
||||||
</Tooltip>
|
fontWeight: "500",
|
||||||
)}
|
fontSize: "18px",
|
||||||
|
color: " #4D4D4D",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Настройки ответов
|
||||||
|
</Typography>
|
||||||
|
<CustomCheckbox
|
||||||
|
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||||
|
label={"Длинный текстовый ответ"}
|
||||||
|
checked={question.content.largeCheck}
|
||||||
|
handleChange={({ target }) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
if (!("largeCheck" in question.content)) return;
|
||||||
|
|
||||||
|
question.content.largeCheck = target.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CustomCheckbox
|
||||||
|
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||||
|
label={"Можно несколько"}
|
||||||
|
checked={question.content.multi}
|
||||||
|
dataCy="multiple-answers-checkbox"
|
||||||
|
handleChange={({ target }) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
if (!("multi" in question.content)) return;
|
||||||
|
|
||||||
|
question.content.multi = target.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CustomCheckbox
|
||||||
|
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||||
|
label={'Вариант "свой ответ"'}
|
||||||
|
checked={question.content.own}
|
||||||
|
handleChange={({ target }) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
if (!("own" in question.content)) return;
|
||||||
|
|
||||||
|
question.content.own = target.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
boxSizing: "border-box",
|
||||||
|
pt: isMobile ? "0px" : "20px",
|
||||||
|
pb: "20px",
|
||||||
|
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
|
||||||
|
pr: isFigmaTablte ? "19px" : "20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "14px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
height: isMobile ? "18px" : "auto",
|
||||||
|
fontWeight: "500",
|
||||||
|
fontSize: "18px",
|
||||||
|
color: " #4D4D4D",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Настройки вопросов
|
||||||
|
</Typography>
|
||||||
|
<CustomCheckbox
|
||||||
|
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||||
|
label={"Необязательный вопрос"}
|
||||||
|
checked={!question.required}
|
||||||
|
handleChange={({ target }) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.required = !target.checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: isMobile ? "90%" : "auto",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CustomCheckbox
|
||||||
|
sx={{
|
||||||
|
mr: isMobile ? "0px" : "9px",
|
||||||
|
height: isMobile ? "42px" : "26px",
|
||||||
|
alignItems: "start",
|
||||||
|
}}
|
||||||
|
label={"Внутреннее название вопроса"}
|
||||||
|
checked={question.content.innerNameCheck}
|
||||||
|
handleChange={({ target }) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.innerNameCheck = target.checked;
|
||||||
|
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isMobile && (
|
||||||
|
<Tooltip
|
||||||
|
title="Будет отображаться как заголовок вопроса в приходящих заявках."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<InfoIcon />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{question.content.innerNameCheck && (
|
||||||
|
<CustomTextField
|
||||||
|
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||||
|
placeholder={"Развёрнутое описание вопроса"}
|
||||||
|
text={question.content.innerName}
|
||||||
|
onChange={({ target }) => updateQuestionInnerName(target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{question.content.innerNameCheck && (
|
);
|
||||||
<CustomTextField
|
|
||||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
|
||||||
placeholder={"Развёрнутое описание вопроса"}
|
|
||||||
text={question.content.innerName}
|
|
||||||
onChange={({ target }) => debounced(target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
import * as React from "react";
|
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||||
import UploadImage from "../UploadImage";
|
import UploadImage from "../UploadImage";
|
||||||
|
import BranchingQuestions from "../branchingQuestions";
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import ResponseSettings from "./responseSettings";
|
import ResponseSettings from "./responseSettings";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
switchState: string;
|
switchState: string;
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SwitchAnswerOptions({
|
export default function SwitchAnswerOptions({
|
||||||
switchState = "setting",
|
switchState = "setting",
|
||||||
totalIndex,
|
question,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
switch (switchState) {
|
switch (switchState) {
|
||||||
case "setting":
|
case "setting":
|
||||||
return <ResponseSettings totalIndex={totalIndex} />;
|
return <ResponseSettings question={question} />;
|
||||||
break;
|
case "help":
|
||||||
case "help":
|
return <HelpQuestions question={question} />;
|
||||||
return <HelpQuestions totalIndex={totalIndex} />;
|
case "branching":
|
||||||
break;
|
return <BranchingQuestions question={question} />;
|
||||||
case "branching":
|
case "image":
|
||||||
return <BranchingQuestions totalIndex={totalIndex} />;
|
return <UploadImage question={question} />;
|
||||||
break;
|
default:
|
||||||
case "image":
|
return <></>;
|
||||||
return <UploadImage totalIndex={totalIndex} />;
|
}
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,394 +1,338 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
FormControl,
|
|
||||||
FormControlLabel,
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
Modal,
|
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
import { Select } from "./Select";
|
|
||||||
|
|
||||||
import RadioCheck from "@ui_kit/RadioCheck";
|
|
||||||
import RadioIcon from "@ui_kit/RadioIcon";
|
|
||||||
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 {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
|
||||||
|
import RadioCheck from "@ui_kit/RadioCheck";
|
||||||
|
import RadioIcon from "@ui_kit/RadioIcon";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Select } from "./Select";
|
||||||
|
|
||||||
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
type BranchingQuestionsProps = {
|
type BranchingQuestionsProps = {
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACTIONS = ["Показать", "Скрыть"];
|
const ACTIONS = ["Показать", "Скрыть"];
|
||||||
const STIPULATIONS = ["Условие 1", "Условие 2", "Условие 3"];
|
const STIPULATIONS = ["Условие 1", "Условие 2", "Условие 3"];
|
||||||
const ANSWERS = ["Ответ 1", "Ответ 2", "Ответ 3"];
|
const ANSWERS = ["Ответ 1", "Ответ 2", "Ответ 3"];
|
||||||
const CONDITIONS = [
|
const CONDITIONS = [
|
||||||
"Все условия обязательны",
|
"Все условия обязательны",
|
||||||
"Обязательно хотя бы одно условие",
|
"Обязательно хотя бы одно условие",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function BranchingQuestions({
|
export default function BranchingQuestions({
|
||||||
totalIndex,
|
question,
|
||||||
}: BranchingQuestionsProps) {
|
}: BranchingQuestionsProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [titleInputWidth, setTitleInputWidth] = useState<number>(0);
|
const [titleInputWidth, setTitleInputWidth] = useState<number>(0);
|
||||||
const quizId = Number(useParams().quizId);
|
const titleRef = useRef<HTMLDivElement>(null);
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const titleRef = useRef<HTMLDivElement>(null);
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitleInputWidth(titleRef.current?.offsetWidth || 0);
|
setTitleInputWidth(titleRef.current?.offsetWidth || 0);
|
||||||
}, [title]);
|
}, [title]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
openedModalSettings: false,
|
question.openedModalSettings = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal open={question.openedModalSettings} onClose={handleClose}>
|
<Modal open={question.openedModalSettings} onClose={handleClose}>
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
overflow: "hidden",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
maxWidth: "620px",
|
|
||||||
width: "100%",
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: 24,
|
|
||||||
p: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
boxSizing: "border-box",
|
|
||||||
background: "#F2F3F7",
|
|
||||||
height: "70px",
|
|
||||||
padding: "0 25px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ color: "#9A9AAF" }}>
|
|
||||||
<Typography component="span">(</Typography>
|
|
||||||
<Box sx={{ display: "inline" }}>
|
|
||||||
<Typography
|
|
||||||
ref={titleRef}
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
opacity: 0,
|
|
||||||
zIndex: "-100",
|
|
||||||
whiteSpace: "pre",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={title}
|
|
||||||
placeholder="Заголовок вопроса"
|
|
||||||
onChange={({ target }) => setTitle(target.value)}
|
|
||||||
style={{
|
|
||||||
width: titleInputWidth ? titleInputWidth : 170,
|
|
||||||
outline: "none",
|
|
||||||
background: "transparent",
|
|
||||||
border: "none",
|
|
||||||
fontSize: "18px",
|
|
||||||
minWidth: "50px",
|
|
||||||
maxWidth: "500px",
|
|
||||||
fontFamily: "Rubik",
|
|
||||||
transition: ".2s",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Typography component="span">)</Typography>
|
|
||||||
</Box>
|
|
||||||
<Tooltip
|
|
||||||
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<InfoIcon />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
padding: "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "30px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "20px",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
items={ACTIONS}
|
|
||||||
activeItemIndex={question.content.rule.show ? 0 : 1}
|
|
||||||
sx={{ maxWidth: "140px" }}
|
|
||||||
onChange={(action) => {
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
rule: {
|
|
||||||
...question.content.rule,
|
|
||||||
show: action === ACTIONS[0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography sx={{ color: theme.palette.grey2.main }}>
|
|
||||||
если в ответе на вопрос
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{question.content.rule.reqs.map((request, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
height: "100%",
|
|
||||||
bgcolor: "#F2F3F7",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
position: "absolute",
|
||||||
justifyContent: "space-between",
|
overflow: "hidden",
|
||||||
alignItems: "center",
|
top: "50%",
|
||||||
pb: "5px",
|
left: "50%",
|
||||||
}}
|
transform: "translate(-50%, -50%)",
|
||||||
>
|
maxWidth: "620px",
|
||||||
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
width: "100%",
|
||||||
Условие 1
|
bgcolor: "background.paper",
|
||||||
</Typography>
|
borderRadius: "12px",
|
||||||
<IconButton
|
boxShadow: 24,
|
||||||
sx={{ borderRadius: "6px", padding: "2px" }}
|
p: 0,
|
||||||
onClick={() => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
clonedContent.rule.reqs.splice(index, 1);
|
|
||||||
updateQuestionsList<QuizQuestionBase>(
|
|
||||||
quizId,
|
|
||||||
totalIndex,
|
|
||||||
{
|
|
||||||
content: clonedContent,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<DeleteIcon color={"#4D4D4D"} />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Select
|
|
||||||
empty
|
|
||||||
activeItemIndex={request.id ? Number(request.id) : -1}
|
|
||||||
items={STIPULATIONS}
|
|
||||||
onChange={(stipulation) => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
|
|
||||||
clonedContent.rule.reqs[index] = {
|
|
||||||
id: String(
|
|
||||||
STIPULATIONS.findIndex((item) =>
|
|
||||||
item.includes(stipulation)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
vars: request.vars,
|
|
||||||
};
|
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
sx={{ marginBottom: "15px" }}
|
|
||||||
/>
|
|
||||||
{request.id && (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
pb: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
|
||||||
Дан ответ
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
|
||||||
(Укажите один или несколько вариантов)
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Select
|
|
||||||
empty
|
|
||||||
activeItemIndex={-1}
|
|
||||||
items={ANSWERS}
|
|
||||||
onChange={(answer) => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
const answerItemIndex = ANSWERS.findIndex(
|
|
||||||
(answerItem) => answerItem === answer
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!clonedContent.rule.reqs[index].vars.includes(
|
|
||||||
answerItemIndex
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
question.content.rule.reqs[index].vars.push(
|
|
||||||
answerItemIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionBase>(
|
|
||||||
quizId,
|
|
||||||
totalIndex,
|
|
||||||
{
|
|
||||||
content: clonedContent,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
marginBottom: "10px",
|
|
||||||
".MuiSelect-select.MuiInputBase-input": {
|
|
||||||
color: "transparent",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{question.content.rule.reqs[index].vars.map(
|
|
||||||
(item, varIndex) => (
|
|
||||||
<Chip
|
|
||||||
key={varIndex}
|
|
||||||
label={ANSWERS[item]}
|
|
||||||
variant="outlined"
|
|
||||||
onDelete={() => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
const removedItemIndex = clonedContent.rule.reqs[
|
|
||||||
index
|
|
||||||
].vars.findIndex((varItem) => varItem === item);
|
|
||||||
|
|
||||||
clonedContent.rule.reqs[index].vars.splice(
|
|
||||||
removedItemIndex,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionBase>(
|
|
||||||
quizId,
|
|
||||||
totalIndex,
|
|
||||||
{
|
|
||||||
content: clonedContent,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "baseline",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
marginBottom: "10px",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
clonedContent.rule.reqs.push({ id: "", vars: [] });
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Добавить условие
|
|
||||||
</Link>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
||||||
value={question.content.rule.or ? 1 : 0}
|
|
||||||
onChange={(_, value) => {
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
rule: {
|
|
||||||
...question.content.rule,
|
|
||||||
or: Boolean(Number(value)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{CONDITIONS.map((condition, index) => (
|
<Box
|
||||||
<FormControlLabel
|
sx={{
|
||||||
key={index}
|
boxSizing: "border-box",
|
||||||
sx={{ color: theme.palette.grey2.main }}
|
background: "#F2F3F7",
|
||||||
value={index}
|
height: "70px",
|
||||||
control={
|
padding: "0 25px",
|
||||||
<Radio
|
display: "flex",
|
||||||
checkedIcon={<RadioCheck />}
|
alignItems: "center",
|
||||||
icon={<RadioIcon />}
|
}}
|
||||||
/>
|
>
|
||||||
}
|
<Box sx={{ color: "#9A9AAF" }}>
|
||||||
label={condition}
|
<Typography component="span">(</Typography>
|
||||||
/>
|
<Box sx={{ display: "inline" }}>
|
||||||
))}
|
<Typography
|
||||||
</RadioGroup>
|
ref={titleRef}
|
||||||
</FormControl>
|
sx={{
|
||||||
</Box>
|
position: "absolute",
|
||||||
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
|
opacity: 0,
|
||||||
<Button
|
zIndex: "-100",
|
||||||
variant="outlined"
|
whiteSpace: "pre",
|
||||||
onClick={handleClose}
|
}}
|
||||||
sx={{ width: "100%", maxWidth: "130px" }}
|
>
|
||||||
>
|
{title}
|
||||||
Отмена
|
</Typography>
|
||||||
</Button>
|
<input
|
||||||
<Button
|
type="text"
|
||||||
variant="contained"
|
value={title}
|
||||||
sx={{ width: "100%", maxWidth: "130px" }}
|
placeholder="Заголовок вопроса"
|
||||||
onClick={handleClose}
|
onChange={({ target }) => setTitle(target.value)}
|
||||||
>
|
style={{
|
||||||
Готово
|
width: titleInputWidth ? titleInputWidth : 170,
|
||||||
</Button>
|
outline: "none",
|
||||||
</Box>
|
background: "transparent",
|
||||||
</Box>
|
border: "none",
|
||||||
</Box>
|
fontSize: "18px",
|
||||||
</Modal>
|
minWidth: "50px",
|
||||||
</>
|
maxWidth: "500px",
|
||||||
);
|
fontFamily: "Rubik",
|
||||||
|
transition: ".2s",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography component="span">)</Typography>
|
||||||
|
</Box>
|
||||||
|
<Tooltip
|
||||||
|
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<InfoIcon />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
items={ACTIONS}
|
||||||
|
activeItemIndex={question.content.rule.show ? 0 : 1}
|
||||||
|
sx={{ maxWidth: "140px" }}
|
||||||
|
onChange={(action) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.rule.show = action === ACTIONS[0];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography sx={{ color: theme.palette.grey2.main }}>
|
||||||
|
если в ответе на вопрос
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{question.content.rule.reqs.map((request, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
height: "100%",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Условие 1
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.rule.reqs.splice(index, 1);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
empty
|
||||||
|
activeItemIndex={request.id ? Number(request.id) : -1}
|
||||||
|
items={STIPULATIONS}
|
||||||
|
onChange={(stipulation) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.rule.reqs[index].id = String(
|
||||||
|
STIPULATIONS.findIndex((item) => item.includes(stipulation))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{ marginBottom: "15px" }}
|
||||||
|
/>
|
||||||
|
{request.id && (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
||||||
|
(Укажите один или несколько вариантов)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
empty
|
||||||
|
activeItemIndex={-1}
|
||||||
|
items={ANSWERS}
|
||||||
|
onChange={(answer) => {
|
||||||
|
const answerItemIndex = ANSWERS.findIndex(
|
||||||
|
(answerItem) => answerItem === answer
|
||||||
|
);
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
const vars = question.content.rule.reqs[index].vars;
|
||||||
|
if (vars.includes(answerItemIndex)) {
|
||||||
|
vars.push(answerItemIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
marginBottom: "10px",
|
||||||
|
".MuiSelect-select.MuiInputBase-input": {
|
||||||
|
color: "transparent",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question.content.rule.reqs[index].vars.map(
|
||||||
|
(item, varIndex) => (
|
||||||
|
<Chip
|
||||||
|
key={varIndex}
|
||||||
|
label={ANSWERS[item]}
|
||||||
|
variant="outlined"
|
||||||
|
onDelete={() => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
const vars = question.content.rule.reqs[index].vars;
|
||||||
|
|
||||||
|
const removedItemIndex = vars.findIndex((varItem) => varItem === item);
|
||||||
|
if (removedItemIndex === -1) return;
|
||||||
|
|
||||||
|
vars.splice(removedItemIndex, 1);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "baseline",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
marginBottom: "10px",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.rule.reqs.push({ id: "", vars: [] });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить условие
|
||||||
|
</Link>
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
||||||
|
value={question.content.rule.or ? 1 : 0}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.rule.or = Boolean(Number(value));
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CONDITIONS.map((condition, index) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={index}
|
||||||
|
sx={{ color: theme.palette.grey2.main }}
|
||||||
|
value={index}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
checkedIcon={<RadioCheck />}
|
||||||
|
icon={<RadioIcon />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={condition}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Готово
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,118 +1,103 @@
|
|||||||
import { useState } from "react";
|
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||||
import { Box, ButtonBase, Typography } from "@mui/material";
|
import { Box, ButtonBase, Typography } from "@mui/material";
|
||||||
import { useParams } from "react-router-dom";
|
import { updateQuestionWithFnOptimistic } from "@root/questions/actions";
|
||||||
import SelectableButton from "@ui_kit/SelectableButton";
|
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
|
import SelectableButton from "@ui_kit/SelectableButton";
|
||||||
|
import UploadBox from "@ui_kit/UploadBox";
|
||||||
|
import { useState } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import UploadIcon from "../../assets/icons/UploadIcon";
|
import UploadIcon from "../../assets/icons/UploadIcon";
|
||||||
import UploadBox from "@ui_kit/UploadBox";
|
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
import { UploadVideoModal } from "./UploadVideoModal";
|
import { UploadVideoModal } from "./UploadVideoModal";
|
||||||
|
|
||||||
import type { QuizQuestionBase } from "../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
type BackgroundType = "text" | "video";
|
type BackgroundType = "text" | "video";
|
||||||
|
|
||||||
type HelpQuestionsProps = {
|
type HelpQuestionsProps = {
|
||||||
totalIndex: number;
|
question: QuizQuestionVariant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HelpQuestions({ totalIndex }: HelpQuestionsProps) {
|
export default function HelpQuestions({ question }: HelpQuestionsProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
|
const [backgroundType, setBackgroundType] = useState<BackgroundType>("text");
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
|
|
||||||
const debounced = useDebouncedCallback((value) => {
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
hint: { text: value, video: question.content.hint.video },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const videoHC = (url: string) => {
|
const updateQuestionHint = useDebouncedCallback((value) => {
|
||||||
const clonedContent = { ...question.content };
|
updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
clonedContent.hint.video = url;
|
question.content.hint.text = value;
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
});
|
||||||
content: {
|
}, 1000);
|
||||||
...question.content,
|
|
||||||
hint: { video: url, text: question.content.hint.text },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "20px",
|
gap: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Typography sx={{ fontWeight: "500" }}>Подсказка консультанта</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectableButton
|
|
||||||
isSelected={backgroundType === "text"}
|
|
||||||
onClick={() => setBackgroundType("text")}
|
|
||||||
sx={{ maxWidth: "130px" }}
|
|
||||||
>
|
>
|
||||||
Текст
|
<Typography sx={{ fontWeight: "500" }}>Подсказка консультанта</Typography>
|
||||||
</SelectableButton>
|
<Box
|
||||||
<SelectableButton
|
sx={{
|
||||||
isSelected={backgroundType === "video"}
|
display: "flex",
|
||||||
onClick={() => setBackgroundType("video")}
|
gap: "10px",
|
||||||
sx={{ maxWidth: "130px" }}
|
}}
|
||||||
>
|
>
|
||||||
Видео
|
<SelectableButton
|
||||||
</SelectableButton>
|
isSelected={backgroundType === "text"}
|
||||||
</Box>
|
onClick={() => setBackgroundType("text")}
|
||||||
{backgroundType === "text" ? (
|
sx={{ maxWidth: "130px" }}
|
||||||
<>
|
>
|
||||||
<CustomTextField
|
Текст
|
||||||
placeholder={"Текст консультанта"}
|
</SelectableButton>
|
||||||
text={question.content.hint.text}
|
<SelectableButton
|
||||||
onChange={({ target }) => debounced(target.value)}
|
isSelected={backgroundType === "video"}
|
||||||
/>
|
onClick={() => setBackgroundType("video")}
|
||||||
</>
|
sx={{ maxWidth: "130px" }}
|
||||||
) : (
|
>
|
||||||
<Box>
|
Видео
|
||||||
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
|
</SelectableButton>
|
||||||
Загрузите видео
|
</Box>
|
||||||
</Typography>
|
{backgroundType === "text" ? (
|
||||||
<ButtonBase
|
<>
|
||||||
onClick={() => setOpen(true)}
|
<CustomTextField
|
||||||
sx={{ justifyContent: "flex-start" }}
|
placeholder={"Текст консультанта"}
|
||||||
>
|
text={question.content.hint.text}
|
||||||
{question.content.hint.video ? (
|
onChange={({ target }) => updateQuestionHint(target.value)}
|
||||||
<video src={question.content.hint.video} width="400" controls />
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Box>
|
||||||
<UploadBox
|
<Typography sx={{ paddingBottom: "15px", fontWeight: "500" }}>
|
||||||
icon={<UploadIcon />}
|
Загрузите видео
|
||||||
sx={{
|
</Typography>
|
||||||
height: "48px",
|
<ButtonBase
|
||||||
width: "48px",
|
onClick={() => setOpen(true)}
|
||||||
}}
|
sx={{ justifyContent: "flex-start" }}
|
||||||
/>
|
>
|
||||||
</>
|
{question.content.hint.video ? (
|
||||||
|
<video src={question.content.hint.video} width="400" controls />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UploadBox
|
||||||
|
icon={<UploadIcon />}
|
||||||
|
sx={{
|
||||||
|
height: "48px",
|
||||||
|
width: "48px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ButtonBase>
|
||||||
|
<UploadVideoModal
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
video={question.content.hint.video}
|
||||||
|
onUpload={url => updateQuestionWithFnOptimistic(question.id, question => {
|
||||||
|
question.content.hint.video = url;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</ButtonBase>
|
|
||||||
<UploadVideoModal
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
video={question.content.hint.video}
|
|
||||||
onUpload={videoHC}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
);
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ import { create } from "zustand";
|
|||||||
import { devtools, persist } from "zustand/middleware";
|
import { devtools, persist } from "zustand/middleware";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnyQuizQuestion,
|
AnyQuizQuestion
|
||||||
QuizQuestionType
|
|
||||||
} from "../model/questionTypes/shared";
|
} from "../model/questionTypes/shared";
|
||||||
|
|
||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
import { produce, setAutoFreeze } from "immer";
|
import { produce, setAutoFreeze } from "immer";
|
||||||
import { QUIZ_QUESTION_BASE } from "../constants/base";
|
import { QUIZ_QUESTION_BASE } from "../constants/base";
|
||||||
import { QUIZ_QUESTION_DATE } from "../constants/date";
|
import { QUIZ_QUESTION_DATE } from "../constants/date";
|
||||||
@ -276,7 +276,7 @@ export const reorderVariants = (
|
|||||||
|
|
||||||
export const createQuestion = (
|
export const createQuestion = (
|
||||||
quizId: number,
|
quizId: number,
|
||||||
questionType: QuizQuestionType = "nonselected",
|
questionType: QuestionType = "variant",
|
||||||
placeIndex = -1
|
placeIndex = -1
|
||||||
) => {
|
) => {
|
||||||
const id = getRandom();
|
const id = getRandom();
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
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 { RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
import { RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
||||||
|
import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
import { notReachable } from "utils/notReachable";
|
||||||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||||
import { questionToEditQuestionRequest } from "@model/question/edit";
|
|
||||||
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
|
||||||
|
|
||||||
|
|
||||||
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
||||||
@ -26,6 +27,13 @@ export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state
|
|||||||
question,
|
question,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const removeQuestion = (questionId: number) => setProducedState(state => {
|
||||||
|
delete state.questionsById[questionId];
|
||||||
|
}, {
|
||||||
|
type: "removeQuestion",
|
||||||
|
questionId,
|
||||||
|
});
|
||||||
|
|
||||||
export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
||||||
questionId: number,
|
questionId: number,
|
||||||
field: T,
|
field: T,
|
||||||
@ -34,7 +42,13 @@ export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
|||||||
const question = state.questionsById[questionId];
|
const question = state.questionsById[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,
|
||||||
@ -42,32 +56,159 @@ export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const toggleExpandQuestion = (questionId: number) => setProducedState(state => {
|
||||||
|
const question = state.questionsById[questionId];
|
||||||
|
if (!question) return;
|
||||||
|
|
||||||
|
question.expanded = !question.expanded;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => {
|
||||||
|
const question = state.questionsById[questionId];
|
||||||
|
if (!question) return;
|
||||||
|
|
||||||
|
question.openedModalSettings = !question.openedModalSettings;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addQuestionVariant = (questionId: number) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
switch (question.type) {
|
||||||
|
case "variant":
|
||||||
|
case "emoji":
|
||||||
|
case "select":
|
||||||
|
question.content.variants.push(createQuestionVariant());
|
||||||
|
break;
|
||||||
|
case "images":
|
||||||
|
case "varimg":
|
||||||
|
question.content.variants.push(createQuestionImageVariant());
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
case "date":
|
||||||
|
case "number":
|
||||||
|
case "file":
|
||||||
|
case "page":
|
||||||
|
case "rating":
|
||||||
|
throw new Error(`Cannot add variant to question of type "${question.type}"`);
|
||||||
|
default: notReachable(question);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteQuestionVariant = (questionId: number, variantId: string) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
|
||||||
|
if (variantIndex === -1) return;
|
||||||
|
|
||||||
|
question.content.variants.splice(variantIndex, 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQuestionVariantField = (
|
||||||
|
questionId: number,
|
||||||
|
variantId: string,
|
||||||
|
field: keyof QuestionVariant,
|
||||||
|
value: QuestionVariant[keyof QuestionVariant],
|
||||||
|
) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
|
||||||
|
if (variantIndex === -1) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants[variantIndex];
|
||||||
|
variant[field] = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQuestionImageVariantField = (
|
||||||
|
questionId: number,
|
||||||
|
variantId: string,
|
||||||
|
field: keyof ImageQuestionVariant,
|
||||||
|
value: ImageQuestionVariant[keyof ImageQuestionVariant],
|
||||||
|
) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
|
||||||
|
if (variantIndex === -1) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants[variantIndex];
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
|
variant[field] = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reorderQuestionVariants = (
|
||||||
|
questionId: number,
|
||||||
|
sourceIndex: number,
|
||||||
|
destinationIndex: number,
|
||||||
|
) => {
|
||||||
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
||||||
|
question.content.variants.splice(destinationIndex, 0, removed);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQuestionBackgroundImage = (
|
||||||
|
questionId: number,
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (question.content.back === url) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
question.content.back !== question.content.originalBack
|
||||||
|
) URL.revokeObjectURL(question.content.back);
|
||||||
|
question.content.back = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setQuestionOriginalBackgroundImage = (
|
||||||
|
questionId: number,
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
updateQuestionWithFnOptimistic(questionId, question => {
|
||||||
|
if (question.content.originalBack === url) return;
|
||||||
|
|
||||||
|
URL.revokeObjectURL(question.content.originalBack);
|
||||||
|
question.content.originalBack = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
let savedOriginalQuestion: AnyQuizQuestion | null = null;
|
let savedOriginalQuestion: AnyQuizQuestion | null = null;
|
||||||
let controller: AbortController | null = null;
|
let controller: AbortController | null = null;
|
||||||
|
|
||||||
export const setQuestionFieldOptimistic = async <T extends keyof AnyQuizQuestion>(
|
export const updateQuestionWithFnOptimistic = async (
|
||||||
questionId: number,
|
questionId: number,
|
||||||
field: T,
|
updateFn: (question: AnyQuizQuestion) => void,
|
||||||
value: AnyQuizQuestion[T],
|
|
||||||
) => {
|
) => {
|
||||||
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
const currentUpdatedQuestion = produce(question, draft => {
|
const currentUpdatedQuestion = produce(question, updateFn);
|
||||||
draft[field] = value;
|
|
||||||
});
|
|
||||||
controller?.abort();
|
controller?.abort();
|
||||||
controller = new AbortController();
|
controller = new AbortController();
|
||||||
savedOriginalQuestion ??= question;
|
savedOriginalQuestion ??= question;
|
||||||
|
|
||||||
setQuestion(currentUpdatedQuestion);
|
setQuestion(currentUpdatedQuestion);
|
||||||
try {
|
try {
|
||||||
const { updated } = await questionApi.edit(
|
const { updated: newId } = await questionApi.edit(
|
||||||
questionToEditQuestionRequest(currentUpdatedQuestion),
|
questionToEditQuestionRequest(currentUpdatedQuestion),
|
||||||
controller.signal,
|
controller.signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
setQuestionField(question.id, "id", updated);
|
setQuestionField(question.id, "id", newId);
|
||||||
|
|
||||||
controller = null;
|
controller = null;
|
||||||
savedOriginalQuestion = null;
|
savedOriginalQuestion = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -75,6 +216,7 @@ export const setQuestionFieldOptimistic = async <T extends keyof AnyQuizQuestion
|
|||||||
|
|
||||||
devlog("Error editing question", { error, question, currentUpdatedQuestion });
|
devlog("Error editing question", { error, question, currentUpdatedQuestion });
|
||||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||||
|
|
||||||
if (!savedOriginalQuestion) {
|
if (!savedOriginalQuestion) {
|
||||||
devlog("Cannot rollback question");
|
devlog("Cannot rollback question");
|
||||||
throw new Error("Cannot rollback question");
|
throw new Error("Cannot rollback question");
|
||||||
@ -86,20 +228,6 @@ export const setQuestionFieldOptimistic = async <T extends keyof AnyQuizQuestion
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateQuestionWithFn = (
|
|
||||||
questionId: number,
|
|
||||||
updateFn: (question: AnyQuizQuestion) => void,
|
|
||||||
) => setProducedState(state => {
|
|
||||||
const question = state.questionsById[questionId];
|
|
||||||
if (!question) return;
|
|
||||||
|
|
||||||
updateFn(question);
|
|
||||||
}, {
|
|
||||||
type: "updateQuestion",
|
|
||||||
questionId,
|
|
||||||
updateFn: updateFn.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createQuestion = async (quizId: number) => {
|
export const createQuestion = async (quizId: number) => {
|
||||||
try {
|
try {
|
||||||
const question = await questionApi.create({
|
const question = await questionApi.create({
|
||||||
@ -124,12 +252,25 @@ export const deleteQuestion = async (questionId: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeQuestion = (questionId: number) => setProducedState(state => {
|
export const copyQuestion = async (questionId: number, quizId: number) => {
|
||||||
delete state.questionsById[questionId];
|
try {
|
||||||
}, {
|
const { updated: newQuestionId } = await questionApi.copy(questionId, quizId);
|
||||||
type: "removeQuestion",
|
|
||||||
questionId,
|
setProducedState(state => {
|
||||||
});
|
const question = state.questionsById[questionId];
|
||||||
|
if (!question) return;
|
||||||
|
|
||||||
|
state.questionsById[newQuestionId] = question;
|
||||||
|
}, {
|
||||||
|
type: "copyQuestion",
|
||||||
|
questionId,
|
||||||
|
quizId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
devlog("Error copying question", error);
|
||||||
|
enqueueSnackbar("Не удалось скопировать вопрос");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function setProducedState<A extends string | { type: unknown; }>(
|
function setProducedState<A extends string | { type: unknown; }>(
|
||||||
recipe: (state: QuestionsStore) => void,
|
recipe: (state: QuestionsStore) => void,
|
||||||
|
@ -17,6 +17,8 @@ export const useQuestionsStore = create<QuestionsStore>()(
|
|||||||
{
|
{
|
||||||
name: "QuestionsStore",
|
name: "QuestionsStore",
|
||||||
enabled: process.env.NODE_ENV === "development",
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: process.env.NODE_ENV === "development",
|
||||||
|
actionsBlacklist: "ignored",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -7305,6 +7305,11 @@ nanoid@^3.3.4:
|
|||||||
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz"
|
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz"
|
||||||
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
|
||||||
|
|
||||||
|
nanoid@^5.0.3:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.3.tgz#6c97f53d793a7a1de6a38ebb46f50f95bf9793c7"
|
||||||
|
integrity sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==
|
||||||
|
|
||||||
natural-compare-lite@^1.4.0:
|
natural-compare-lite@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user