505 lines
17 KiB
TypeScript
505 lines
17 KiB
TypeScript
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
|
||
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
|
||
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
|
||
import { OneIcon } from "@icons/questionsPage/OneIcon";
|
||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||
import Answer from "@icons/questionsPage/answer";
|
||
import Date from "@icons/questionsPage/date";
|
||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||
import Download from "@icons/questionsPage/download";
|
||
import DropDown from "@icons/questionsPage/drop_down";
|
||
import Emoji from "@icons/questionsPage/emoji";
|
||
import { HideIcon } from "@icons/questionsPage/hideIcon";
|
||
import Input from "@icons/questionsPage/input";
|
||
import OptionsAndPict from "@icons/questionsPage/options_and_pict";
|
||
import OptionsPict from "@icons/questionsPage/options_pict";
|
||
import Page from "@icons/questionsPage/page";
|
||
import RatingIcon from "@icons/questionsPage/rating";
|
||
import Slider from "@icons/questionsPage/slider";
|
||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||
import {
|
||
Box,
|
||
Button,
|
||
Checkbox,
|
||
FormControl,
|
||
FormControlLabel,
|
||
IconButton,
|
||
InputAdornment,
|
||
Modal,
|
||
Paper,
|
||
TextField,
|
||
Typography,
|
||
useMediaQuery,
|
||
useTheme,
|
||
} from "@mui/material";
|
||
import {
|
||
copyQuestion,
|
||
createUntypedQuestion,
|
||
deleteQuestion,
|
||
clearRuleForAll,
|
||
toggleExpandQuestion,
|
||
updateQuestion,
|
||
updateUntypedQuestion,
|
||
getQuestionByContentId,
|
||
deleteQuestionWithTimeout,
|
||
} from "@root/questions/actions";
|
||
import { updateRootContentId } from "@root/quizes/actions";
|
||
import { useRef, useState } from "react";
|
||
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
|
||
import { useDebouncedCallback } from "use-debounce";
|
||
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
|
||
import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../model/questionTypes/shared";
|
||
import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
||
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
||
import TypeQuestions from "../TypeQuestions";
|
||
import { QuestionType } from "@model/question/question";
|
||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||
import { useQuestionsStore } from "@root/questions/store";
|
||
import { updateSomeWorkBackend } from "@root/uiTools/actions";
|
||
import { DeleteFunction } from "@utils/deleteFunc";
|
||
|
||
interface Props {
|
||
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
|
||
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
|
||
isDragging: boolean;
|
||
index: number;
|
||
}
|
||
|
||
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
|
||
const maxLengthTextField = 225;
|
||
|
||
const { questions } = useQuestionsStore();
|
||
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
||
const [open, setOpen] = useState<boolean>(false);
|
||
|
||
const [isTextFieldtActive, setIsTextFieldtActive] = useState(false);
|
||
|
||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||
const theme = useTheme();
|
||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||
const anchorRef = useRef(null);
|
||
const quiz = useCurrentQuiz();
|
||
|
||
const setTitle = useDebouncedCallback((title) => {
|
||
const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
|
||
|
||
updateQuestionFn(question.id, question => {
|
||
question.title = title;
|
||
});
|
||
}, 200);
|
||
|
||
const handleInputFocus = () => {
|
||
setIsTextFieldtActive(true);
|
||
};
|
||
|
||
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||
setIsTextFieldtActive(false);
|
||
};
|
||
|
||
|
||
return (
|
||
<>
|
||
<Paper
|
||
id={question.id}
|
||
data-cy="quiz-question-card"
|
||
sx={{
|
||
maxWidth: "796px",
|
||
width: "100%",
|
||
borderRadius: "12px",
|
||
backgroundColor: question.expanded ? "white" : "#EEE4FC",
|
||
border: question.expanded ? "none" : "1px solid #9A9AAF",
|
||
boxShadow: "0px 10px 30px #e7e7e7",
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
padding: isMobile ? "10px" : "20px 10px 20px 20px",
|
||
flexDirection: isMobile ? "column" : null,
|
||
}}
|
||
>
|
||
<FormControl
|
||
variant="standard"
|
||
sx={{
|
||
p: 0,
|
||
maxWidth: isTablet ? "549px" : "640px",
|
||
width: "100%",
|
||
marginRight: isMobile ? "0px" : "16.1px",
|
||
}}
|
||
>
|
||
<TextField
|
||
defaultValue={question.title}
|
||
placeholder={"Заголовок вопроса"}
|
||
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value || " ")}
|
||
onFocus={handleInputFocus}
|
||
onBlur={handleInputBlur}
|
||
inputProps={{
|
||
maxLength: maxLengthTextField,
|
||
}}
|
||
InputProps={{
|
||
startAdornment: (
|
||
<Box>
|
||
<InputAdornment
|
||
ref={anchorRef}
|
||
position="start"
|
||
sx={{ cursor: "pointer" }}
|
||
onClick={() => setOpen((isOpened) => !isOpened)}
|
||
>
|
||
{IconAndrom(question.expanded, question.type)}
|
||
</InputAdornment>
|
||
<ChooseAnswerModal
|
||
open={open}
|
||
onClose={() => setOpen(false)}
|
||
anchorRef={anchorRef}
|
||
question={question}
|
||
questionType={question.type}
|
||
/>
|
||
</Box>
|
||
),
|
||
endAdornment: isTextFieldtActive && question.title.length >= maxLengthTextField - 7 && (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
marginTop: "5px",
|
||
marginLeft: "auto",
|
||
position: "absolute",
|
||
bottom: "-28px",
|
||
right: "0",
|
||
}}
|
||
>
|
||
<Typography fontSize="14px">{question.title.length}</Typography>
|
||
<span>/</span>
|
||
<Typography fontSize="14px">{maxLengthTextField}</Typography>
|
||
</Box>
|
||
),
|
||
}}
|
||
sx={{
|
||
margin: isMobile ? "10px 0" : 0,
|
||
"& .MuiInputBase-root": {
|
||
color: "#000000",
|
||
backgroundColor: question.expanded ? theme.palette.background.default : "transparent",
|
||
height: "48px",
|
||
borderRadius: "10px",
|
||
".MuiOutlinedInput-notchedOutline": {
|
||
borderWidth: "1px !important",
|
||
border: !question.expanded ? "none" : null,
|
||
},
|
||
"& .MuiInputBase-input::placeholder": {
|
||
color: "#4D4D4D",
|
||
opacity: 0.8,
|
||
},
|
||
},
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
width: isMobile ? "100%" : "auto",
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<IconButton
|
||
sx={{ padding: "0", margin: "5px" }}
|
||
disableRipple
|
||
data-cy="expand-question"
|
||
onClick={() => toggleExpandQuestion(question.id)}
|
||
>
|
||
{question.expanded ? (
|
||
<ArrowDownIcon
|
||
style={{
|
||
width: "18px",
|
||
color: "#4D4D4D",
|
||
}}
|
||
/>
|
||
) : (
|
||
<ExpandLessIcon
|
||
sx={{
|
||
boxSizing: "border-box",
|
||
fill: theme.palette.brightPurple.main,
|
||
background: "#FFF",
|
||
borderRadius: "6px",
|
||
height: "30px",
|
||
width: "30px",
|
||
}}
|
||
/>
|
||
)}
|
||
</IconButton>
|
||
{question.expanded ? (
|
||
<></>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
height: "30px",
|
||
borderRight: "solid 1px #4D4D4D",
|
||
}}
|
||
>
|
||
{/* <FormControlLabel
|
||
control={
|
||
<Checkbox
|
||
icon={
|
||
<HideIcon
|
||
style={{
|
||
boxSizing: "border-box",
|
||
color: "#7E2AEA",
|
||
background: "#FFF",
|
||
borderRadius: "6px",
|
||
height: "30px",
|
||
width: "30px",
|
||
padding: "3px",
|
||
}}
|
||
/>
|
||
}
|
||
checkedIcon={<CrossedEyeIcon />}
|
||
/>
|
||
}
|
||
label={""}
|
||
sx={{
|
||
color: theme.palette.grey2.main,
|
||
ml: "-9px",
|
||
mr: 0,
|
||
userSelect: "none",
|
||
}}
|
||
/> */}
|
||
<IconButton sx={{ padding: "0" }} onClick={() => copyQuestion(question.id, question.quizId)}>
|
||
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
|
||
</IconButton>
|
||
<IconButton
|
||
sx={{
|
||
cursor: "pointer",
|
||
borderRadius: "6px",
|
||
padding: "0",
|
||
margin: "0 5px 0 10px",
|
||
}}
|
||
onClick={() => {
|
||
if (question.type === null) {
|
||
deleteQuestion(question.id);
|
||
}
|
||
if (question.content.rule.parentId.length !== 0) {
|
||
setOpenDelete(true);
|
||
} else {
|
||
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
|
||
}
|
||
}}
|
||
data-cy="delete-question"
|
||
>
|
||
<DeleteIcon style={{ color: theme.palette.brightPurple.main }} />
|
||
</IconButton>
|
||
<Modal open={openDelete} onClose={() => setOpenDelete(false)}>
|
||
<Box
|
||
sx={{
|
||
position: "absolute",
|
||
top: "50%",
|
||
left: "50%",
|
||
transform: "translate(-50%, -50%)",
|
||
padding: "30px",
|
||
borderRadius: "10px",
|
||
background: "#FFFFFF",
|
||
}}
|
||
>
|
||
<Typography variant="h6" sx={{ textAlign: "center" }}>
|
||
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки потеряют данные ветвления. Вы
|
||
уверены, что хотите удалить вопрос?
|
||
</Typography>
|
||
<Box
|
||
sx={{
|
||
marginTop: "30px",
|
||
display: "flex",
|
||
justifyContent: "center",
|
||
gap: "15px",
|
||
}}
|
||
>
|
||
<Button variant="contained" sx={{ minWidth: "150px" }} onClick={() => setOpenDelete(false)}>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
sx={{ minWidth: "150px" }}
|
||
onClick={() => {
|
||
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
|
||
}}
|
||
>
|
||
Подтвердить
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</Modal>
|
||
</Box>
|
||
)}
|
||
{question.type !== null && (
|
||
<Box
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
height: "30px",
|
||
width: "30px",
|
||
marginLeft: "3px",
|
||
borderRadius: "50%",
|
||
fontSize: "16px",
|
||
color: question.expanded ? theme.palette.brightPurple.main : "#FFF",
|
||
background: question.expanded ? "#EEE4FC" : theme.palette.brightPurple.main,
|
||
}}
|
||
>
|
||
{question.page + 1}
|
||
</Box>
|
||
)}
|
||
<IconButton
|
||
disableRipple
|
||
sx={{
|
||
padding: isMobile ? "0" : "0 5px",
|
||
right: isMobile ? "0" : null,
|
||
bottom: isMobile ? "0" : null,
|
||
}}
|
||
{...draggableProps}
|
||
>
|
||
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
|
||
</IconButton>
|
||
</Box>
|
||
</Box>
|
||
{question.expanded && (
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
padding: 0,
|
||
borderRadius: "12px",
|
||
}}
|
||
>
|
||
{question.type === null ? (
|
||
<TypeQuestions question={question} />
|
||
) : (
|
||
<SwitchQuestionsPage question={question} />
|
||
)}
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
<Box
|
||
onMouseEnter={() => setPlusVisible(true)}
|
||
onMouseLeave={() => setPlusVisible(false)}
|
||
sx={{
|
||
maxWidth: "825px",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
height: "40px",
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
<Box
|
||
onClick={() => createUntypedQuestion(question.quizId, question.id)}
|
||
sx={{
|
||
display: plusVisible && !isDragging ? "flex" : "none",
|
||
width: "100%",
|
||
alignItems: "center",
|
||
columnGap: "10px",
|
||
}}
|
||
data-cy="create-question"
|
||
>
|
||
<Box
|
||
sx={{
|
||
boxSizing: "border-box",
|
||
width: "100%",
|
||
height: "1px",
|
||
backgroundPosition: "bottom",
|
||
backgroundRepeat: "repeat-x",
|
||
backgroundSize: "20px 1px",
|
||
backgroundImage: "radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
|
||
}}
|
||
/>
|
||
<PlusIcon />
|
||
</Box>
|
||
</Box>
|
||
</>
|
||
);
|
||
|
||
}
|
||
|
||
const IconAndrom = (isExpanded: boolean, questionType: QuestionType | null) => {
|
||
switch (questionType) {
|
||
case "variant":
|
||
return (
|
||
<Answer
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "images":
|
||
return (
|
||
<OptionsPict
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "varimg":
|
||
return (
|
||
<OptionsAndPict
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "emoji":
|
||
return (
|
||
<Emoji
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "text":
|
||
return (
|
||
<Input
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "select":
|
||
return (
|
||
<DropDown
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "date":
|
||
return (
|
||
<Date
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "number":
|
||
return (
|
||
<Slider
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "file":
|
||
return (
|
||
<Download
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "page":
|
||
return (
|
||
<Page
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
case "rating":
|
||
return (
|
||
<RatingIcon
|
||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||
sx={{ height: "22px", width: "20px" }}
|
||
/>
|
||
);
|
||
default:
|
||
return <></>;
|
||
}
|
||
};
|