From dd46a3833f5269c251552161373f2577a28005d3 Mon Sep 17 00:00:00 2001 From: nflnkr Date: Tue, 14 Nov 2023 23:15:52 +0300 Subject: [PATCH] WIP --- src/api/question.ts | 47 +- src/api/quiz.ts | 21 - src/constants/base.ts | 57 +- src/constants/default.ts | 28 + src/model/question/edit.ts | 24 +- src/model/question/getList.ts | 4 +- src/model/question/question.ts | 58 +- src/model/questionTypes/shared.ts | 19 +- .../DraggableList/ChooseAnswerModal.tsx | 249 +++--- .../DraggableList/DraggableListItem.tsx | 140 ++- .../DraggableList/QuestionPageCard.tsx | 836 +++++++++--------- src/pages/Questions/DraggableList/index.tsx | 87 +- src/pages/Questions/QuestionsPage.tsx | 206 ++--- src/pages/Questions/TypeQuestions.tsx | 90 +- src/stores/questions/actions.ts | 37 +- src/stores/questions/store.ts | 4 +- 16 files changed, 968 insertions(+), 939 deletions(-) create mode 100644 src/constants/default.ts diff --git a/src/api/question.ts b/src/api/question.ts index 796f0e98..88f6acd7 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -1,6 +1,6 @@ import { makeRequest } from "@frontend/kitui"; import { CreateQuestionRequest } from "model/question/create"; -import { Question } from "model/question/question"; +import { RawQuestion } from "model/question/question"; import { GetQuestionListRequest, GetQuestionListResponse } from "model/question/getList"; import { EditQuestionRequest, EditQuestionResponse } from "model/question/edit"; import { DeleteQuestionRequest, DeleteQuestionResponse } from "model/question/delete"; @@ -9,32 +9,27 @@ import { CopyQuestionRequest, CopyQuestionResponse } from "model/question/copy"; const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz"; -export function createQuestion(body: CreateQuestionRequest = defaultCreateQuestionBody) { - return makeRequest({ +function createQuestion(body?: Partial) { + return makeRequest({ url: `${baseUrl}/question/create`, - body, + body: { ...defaultCreateQuestionBody, ...body }, method: "POST", }); } -export function getQuestionList(body: GetQuestionListRequest = defaultGetQuestionListBody) { - return makeRequest({ +async function getQuestionList(body?: Partial) { + if (!body?.quiz_id) return null; + + const response = await makeRequest({ url: `${baseUrl}/question/getList`, - body, - method: "GET", + body: { ...defaultGetQuestionListBody, ...body }, + method: "POST", }); + + return response.items; } -export function editQuestion(updatedQuestion: Question, signal?: AbortSignal) { - const body: EditQuestionRequest = { - id: updatedQuestion.id, - title: updatedQuestion.title, - desc: updatedQuestion.description, - type: updatedQuestion.type, - required: updatedQuestion.required, - page: updatedQuestion.page, - }; - +function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) { return makeRequest({ url: `${baseUrl}/question/edit`, body, @@ -43,7 +38,7 @@ export function editQuestion(updatedQuestion: Question, signal?: AbortSignal) { }); } -export function copyQuestion(copyQuestionBody: CopyQuestionRequest) { +function copyQuestion(copyQuestionBody: CopyQuestionRequest) { return makeRequest({ url: `${baseUrl}/question/copy`, body: copyQuestionBody, @@ -51,7 +46,7 @@ export function copyQuestion(copyQuestionBody: CopyQuestionRequest) { }); } -export function deleteQuestion(id: number) { +function deleteQuestion(id: number) { return makeRequest({ url: `${baseUrl}/question/delete`, body: { id }, @@ -72,7 +67,7 @@ const defaultCreateQuestionBody: CreateQuestionRequest = { "quiz_id": 0, "title": "string", "description": "string", - "type": "string", + "type": "variant", "required": true, "page": 0, "content": "string", @@ -87,14 +82,4 @@ const defaultGetQuestionListBody: GetQuestionListRequest = { "type": "string", "deleted": true, "required": true, - "quiz_id": 0 -}; - -const defaultEditQuestionBody: EditQuestionRequest = { - "id": 0, - "title": "string", - "desc": "string", - "type": "", - "required": true, - "page": 0 }; diff --git a/src/api/quiz.ts b/src/api/quiz.ts index 2d0f4fdd..24fa211a 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -38,7 +38,6 @@ function getQuiz(body?: Partial) { } async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) { - // await new Promise((resolve) => setTimeout(resolve, 1000)); return makeRequest({ url: `${baseUrl}/quiz/edit`, body, @@ -106,26 +105,6 @@ const defaultCreateQuizBody: CreateQuizRequest = { "group_id": 0, }; -const defaultEditQuizBody: EditQuizRequest = { - "id": 0, - "fp": true, - "rep": true, - "note_prevented": true, - "mailing": true, - "uniq": true, - "name": "string", - "desc": "string", - "conf": "string", - "status": "string", - "limit": 0, - "due_to": 0, - "time_of_passing": 0, - "pausable": true, - "question_cnt": 0, - "super": true, - "group_id": 0, -}; - const defaultGetQuizBody: GetQuizRequest = { "quiz_id": "string", "limit": 0, diff --git a/src/constants/base.ts b/src/constants/base.ts index 2660041c..e0655fa4 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -1,31 +1,34 @@ -import type { QuizQuestionInitial } from "../model/questionTypes/shared"; +import type { QuizQuestionBase } from "../model/questionTypes/shared"; -export const QUIZ_QUESTION_BASE: Omit = { - title: "", - type: "nonselected", - expanded: true, - openedModalSettings: false, - required: false, - deleted: false, - deleteTimeoutId: 0, - content: { - hint: { - text: "", - video: "", - }, - rule: { - or: true, - show: true, - title: "", - reqs: [ - { - id: "", - vars: [], + +export const QUIZ_QUESTION_BASE: Omit = { + quizId: 0, + description: "", + page: 0, + title: "", + expanded: true, + openedModalSettings: false, + required: false, + deleted: false, + deleteTimeoutId: 0, + content: { + hint: { + text: "", + video: "", }, - ], + rule: { + or: true, + show: true, + title: "", + reqs: [ + { + id: "", + vars: [], + }, + ], + }, + back: "", + originalBack: "", + autofill: false, }, - back: "", - originalBack: "", - autofill: false, - }, }; diff --git a/src/constants/default.ts b/src/constants/default.ts new file mode 100644 index 00000000..9ef97e28 --- /dev/null +++ b/src/constants/default.ts @@ -0,0 +1,28 @@ +import { QuestionType } from "@model/question/question"; +import { QUIZ_QUESTION_DATE } from "./date"; +import { QUIZ_QUESTION_EMOJI } from "./emoji"; +import { QUIZ_QUESTION_FILE } from "./file"; +import { QUIZ_QUESTION_IMAGES } from "./images"; +import { QUIZ_QUESTION_NUMBER } from "./number"; +import { QUIZ_QUESTION_PAGE } from "./page"; +import { QUIZ_QUESTION_RATING } from "./rating"; +import { QUIZ_QUESTION_SELECT } from "./select"; +import { QUIZ_QUESTION_TEXT } from "./text"; +import { QUIZ_QUESTION_VARIANT } from "./variant"; +import { QUIZ_QUESTION_VARIMG } from "./varimg"; +import { AnyQuestionContent } from "@model/questionTypes/shared"; + + +export const defaultQuestionContentByType: Record = { + "date": QUIZ_QUESTION_DATE.content, + "emoji": QUIZ_QUESTION_EMOJI.content, + "file": QUIZ_QUESTION_FILE.content, + "images": QUIZ_QUESTION_IMAGES.content, + "number": QUIZ_QUESTION_NUMBER.content, + "page": QUIZ_QUESTION_PAGE.content, + "rating": QUIZ_QUESTION_RATING.content, + "select": QUIZ_QUESTION_SELECT.content, + "text": QUIZ_QUESTION_TEXT.content, + "variant": QUIZ_QUESTION_VARIANT.content, + "varimg": QUIZ_QUESTION_VARIMG.content, +} as const; diff --git a/src/model/question/edit.ts b/src/model/question/edit.ts index 82978999..2d9c6446 100644 --- a/src/model/question/edit.ts +++ b/src/model/question/edit.ts @@ -1,12 +1,26 @@ +import { AnyQuizQuestion, DefiniteQuestionType } from "@model/questionTypes/shared"; + + export interface EditQuestionRequest { id: number; - title: string; - desc: string; - type: "test" | "button" | "file" | "checkbox" | "select" | "none" | ""; - required: boolean; - page: number; + title?: string; + desc?: string; + type?: DefiniteQuestionType; + required?: boolean; + page?: number; } export interface EditQuestionResponse { updated: number; } + +export function questionToEditQuestionRequest(question: AnyQuizQuestion): EditQuestionRequest { + return { + id: question.id, + title: question.title, + desc: question.description, + type: question.type, + required: question.required, + page: question.page, + }; +} diff --git a/src/model/question/getList.ts b/src/model/question/getList.ts index 3e346106..235cc626 100644 --- a/src/model/question/getList.ts +++ b/src/model/question/getList.ts @@ -1,4 +1,4 @@ -import { Question } from "./question"; +import { RawQuestion } from "./question"; export interface GetQuestionListRequest { @@ -24,5 +24,5 @@ export interface GetQuestionListRequest { export interface GetQuestionListResponse { count: number; - items: Question[]; + items: RawQuestion[]; } diff --git a/src/model/question/question.ts b/src/model/question/question.ts index 18834716..2f1e5faa 100644 --- a/src/model/question/question.ts +++ b/src/model/question/question.ts @@ -1,15 +1,69 @@ -export interface Question { +import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { defaultQuestionContentByType } from "../../constants/default"; + + +export type QuestionType = + | "variant" + | "images" + | "varimg" + | "emoji" + | "text" + | "select" + | "date" + | "number" + | "file" + | "page" + | "rating"; + +/** Type that comes from server */ +export interface RawQuestion { + /** Id of created question */ id: number; + /** relation to quiz */ quiz_id: number; + /** title of question. max 512 length */ title: string; + /** description of question */ description: string; - type: "test" | "button" | "file" | "checkbox" | "select" | "none" | ""; + /** status of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */ + type: QuestionType; + /** user must pass this question */ required: boolean; + /** true if question is deleted */ deleted: boolean; + /** page if question */ page: number; + /** serialized json of created question */ content: string; + /** version of quiz */ version: number; + /** array of previous versions of quiz */ parent_ids: number[]; created_at: string; updated_at: string; } + +export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion { + let content = defaultQuestionContentByType[rawQuestion.type]; + + try { + content = JSON.parse(rawQuestion.content); + } catch (error) { + console.warn("Cannot parse question content from string, using default content", error); + } + + return { + id: rawQuestion.id, + description: rawQuestion.description, + page: rawQuestion.page, + quizId: rawQuestion.quiz_id, + required: rawQuestion.required, + title: rawQuestion.title, + type: rawQuestion.type, + expanded: true, + openedModalSettings: false, + deleted: false, + deleteTimeoutId: 0, + content, + } as AnyQuizQuestion; +} diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index b3761dbb..e5176371 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -1,3 +1,4 @@ +import { QuestionType } from "@model/question/question"; import type { QuizQuestionDate } from "./date"; import type { QuizQuestionEmoji } from "./emoji"; import type { QuizQuestionFile } from "./file"; @@ -10,6 +11,7 @@ import type { QuizQuestionText } from "./text"; import type { QuizQuestionVariant } from "./variant"; import type { QuizQuestionVarImg } from "./varimg"; + export interface QuestionBranchingRule { /** Радиокнопка "Все условия обязательны" */ or: boolean; @@ -45,8 +47,11 @@ export interface ImageQuestionVariant extends QuestionVariant { export interface QuizQuestionBase { id: number; + quizId: number; title: string; - type: string; + description: string; + page: number; + type?: QuestionType; expanded: boolean; openedModalSettings: boolean; required: boolean; @@ -61,9 +66,9 @@ export interface QuizQuestionBase { }; } -export interface QuizQuestionInitial extends QuizQuestionBase { - type: "nonselected"; -} +// export interface QuizQuestionInitial extends QuizQuestionBase { +// type: "nonselected"; +// } export type AnyQuizQuestion = | QuizQuestionVariant @@ -76,9 +81,11 @@ export type AnyQuizQuestion = | QuizQuestionNumber | QuizQuestionFile | QuizQuestionPage - | QuizQuestionRating - | QuizQuestionInitial; + | QuizQuestionRating; +// | QuizQuestionInitial; export type QuizQuestionType = AnyQuizQuestion["type"]; +export type AnyQuestionContent = AnyQuizQuestion["content"]; + export type DefiniteQuestionType = Exclude; diff --git a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx index e5c75c1d..478e72da 100644 --- a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx +++ b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx @@ -1,149 +1,146 @@ import { useState } from "react"; -import { useParams } from "react-router-dom"; import { - Box, - Typography, - Popper, - Grow, - Paper, - MenuList, - MenuItem, - ClickAwayListener, - Modal, - Button, - useTheme, + Box, + Typography, + Popper, + Grow, + Paper, + MenuList, + MenuItem, + ClickAwayListener, + Modal, + Button, + useTheme, } from "@mui/material"; import { - questionStore, - updateQuestionsList, - removeQuestionForce, - createQuestion, + updateQuestionsList, + removeQuestionForce, + createQuestion, } from "@root/questions"; import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; import type { RefObject } from "react"; import type { - QuizQuestionType, - QuizQuestionBase, + QuizQuestionType, + QuizQuestionBase, + AnyQuizQuestion, } from "../../../model/questionTypes/shared"; type ChooseAnswerModalProps = { - open: boolean; - onClose: () => void; - anchorRef: RefObject; - totalIndex: number; - switchState: string; + open: boolean; + onClose: () => void; + anchorRef: RefObject; + question: AnyQuizQuestion; + switchState: string; }; export const ChooseAnswerModal = ({ - open, - onClose, - anchorRef, - totalIndex, - switchState, + open, + onClose, + anchorRef, + question, + switchState, }: ChooseAnswerModalProps) => { - const [openModal, setOpenModal] = useState(false); - const [selectedValue, setSelectedValue] = useState("text"); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const theme = useTheme(); + const [openModal, setOpenModal] = useState(false); + const [selectedValue, setSelectedValue] = useState("text"); + const theme = useTheme(); - return ( - <> - - {({ TransitionProps }) => ( - - - - - {BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => ( - { - onClose(); - setOpenModal(true); - setSelectedValue(value); - }, - })} - > - {icon} - + + {({ TransitionProps }) => ( + + + + + {BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => ( + { + onClose(); + setOpenModal(true); + setSelectedValue(value); + }, + })} + > + {icon} + + {title} + + + ))} + + + + + )} + + setOpenModal(false)}> + + + Все настройки, кроме заголовка вопроса будут сброшены + + - {title} - - - ))} - - - - - )} - - setOpenModal(false)}> - - - Все настройки, кроме заголовка вопроса будут сброшены - - - - + - - - - - ); + // removeQuestionForce(quizId, question.id); + // createQuestion(quizId, selectedValue, totalIndex); + // updateQuestionsList(quizId, totalIndex, { + // title: question.title, + // expanded: question.expanded, + // }); + }} + > + Подтвердить + + + + + + ); }; diff --git a/src/pages/Questions/DraggableList/DraggableListItem.tsx b/src/pages/Questions/DraggableList/DraggableListItem.tsx index 462a3db3..08d88c7d 100644 --- a/src/pages/Questions/DraggableList/DraggableListItem.tsx +++ b/src/pages/Questions/DraggableList/DraggableListItem.tsx @@ -1,84 +1,80 @@ -import { memo } from "react"; -import { useParams } from "react-router-dom"; -import { Draggable } from "react-beautiful-dnd"; import { Box, ListItem, Typography, useTheme } from "@mui/material"; - +import { memo } from "react"; +import { Draggable } from "react-beautiful-dnd"; import QuestionsPageCard from "./QuestionPageCard"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; -import { updateQuestionsList } from "@root/questions"; -import { QuizQuestionBase } from "../../../model/questionTypes/shared"; - -type DraggableListItemProps = { - index: number; - isDragging: boolean; - questionData: QuizQuestionBase; +type Props = { + question: AnyQuizQuestion; + isDragging: boolean; + index: number; }; -export default memo( - ({ index, isDragging, questionData }: DraggableListItemProps) => { - const quizId = Number(useParams().quizId); +function DraggableListItem({ question, isDragging, index }: Props) { const theme = useTheme(); return ( - - {(provided) => ( - - {questionData.deleted ? ( - - + {(provided) => ( + - Вопрос удалён. - - { - updateQuestionsList(quizId, index, { - ...questionData, - deleted: false, - }); - }} - sx={{ - cursor: "pointer", - fontSize: "16px", - textDecoration: "underline", - color: theme.palette.brightPurple.main, - textDecorationColor: theme.palette.brightPurple.main, - }} - > - Восстановить? - - - ) : ( - - - + {/* questionData.deleted TODO */ true ? ( + + + Вопрос удалён. + + { // TODO + // updateQuestionsList(quizId, index, { + // ...questionData, + // deleted: false, + // }); + }} + sx={{ + cursor: "pointer", + fontSize: "16px", + textDecoration: "underline", + color: theme.palette.brightPurple.main, + textDecorationColor: theme.palette.brightPurple.main, + }} + > + Восстановить? + + + ) : ( + + + + )} + )} - - )} - + ); - } -); +} + +const DraggableListItemMemo = memo(DraggableListItem); + +export default DraggableListItemMemo; diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index 86b04f93..1e89d0f1 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -1,449 +1,433 @@ -import { useState, useRef, useEffect } from "react"; -import { useParams } from "react-router-dom"; import { - Box, - Checkbox, - FormControl, - FormControlLabel, - IconButton, - InputAdornment, - Paper, - TextField, - useMediaQuery, - useTheme, + Box, + Checkbox, + FormControl, + FormControlLabel, + IconButton, + InputAdornment, + Paper, + TextField, + useMediaQuery, + useTheme, } from "@mui/material"; +import { useRef, useState } from "react"; +import { useParams } from "react-router-dom"; import { useDebouncedCallback } from "use-debounce"; -import { ChooseAnswerModal } from "./ChooseAnswerModal"; -import TypeQuestions from "../TypeQuestions"; import SwitchQuestionsPage from "../SwitchQuestionsPage"; +import TypeQuestions from "../TypeQuestions"; +import { ChooseAnswerModal } from "./ChooseAnswerModal"; import { - questionStore, - updateQuestionsList, - createQuestion, - copyQuestion, - removeQuestion, - removeQuestionForce, + copyQuestion, + createQuestion, + removeQuestion, + removeQuestionForce, + updateQuestionsList } from "@root/questions"; -import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; +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 { CopyIcon } from "@icons/questionsPage/CopyIcon"; -import { CrossedEyeIcon } from "@icons/CrossedEyeIcon"; -import { HideIcon } from "@icons/questionsPage/hideIcon"; import Answer from "@icons/questionsPage/answer"; -import OptionsPict from "@icons/questionsPage/options_pict"; -import OptionsAndPict from "@icons/questionsPage/options_and_pict"; -import Emoji from "@icons/questionsPage/emoji"; -import Input from "@icons/questionsPage/input"; -import DropDown from "@icons/questionsPage/drop_down"; import Date from "@icons/questionsPage/date"; -import Slider from "@icons/questionsPage/slider"; +import { 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 { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon"; -import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; - +import Slider from "@icons/questionsPage/slider"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; -import type { - AnyQuizQuestion, - QuizQuestionInitial, -} from "../../../model/questionTypes/shared"; +import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; +import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; + interface Props { - totalIndex: number; - draggableProps: DraggableProvidedDragHandleProps | null | undefined; - isDragging: boolean; + question: AnyQuizQuestion; + draggableProps: DraggableProvidedDragHandleProps | null | undefined; + isDragging: boolean; +} + +export default function QuestionsPageCard({ question, draggableProps, isDragging }: Props) { + const [plusVisible, setPlusVisible] = useState(false); + const [open, setOpen] = useState(false); + const quizId = Number(useParams().quizId); + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down(1000)); + const isMobile = useMediaQuery(theme.breakpoints.down(790)); + const anchorRef = useRef(null); + const debounced = useDebouncedCallback((title) => { // TODO update title + // updateQuestionsList(quizId, totalIndex, { title }); + }, 200); + + return ( + <> + + + + debounced(target.value)} + InputProps={{ + startAdornment: ( + + setOpen((isOpened) => !isOpened)} + > + {IconAndrom(question.expanded, question.type)} + + setOpen(false)} + anchorRef={anchorRef} + question={question} + switchState={question.type} + /> + + ), + }} + 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, + }, + }, + }} + inputProps={{ + sx: { + fontSize: "18px", + lineHeight: "21px", + py: 0, + paddingLeft: question.type.length === 0 ? 0 : "18px", + }, + "data-cy": "quiz-question-title", + }} + /> + + + + updateQuestionsList(quizId, totalIndex, { + expanded: !question.expanded, + }) + } + > + {question.expanded ? ( + + ) : ( + + )} + + {question.expanded ? ( + <> + ) : ( + + + } + checkedIcon={} + /> + } + label={""} + sx={{ + color: theme.palette.grey2.main, + ml: "-9px", + mr: 0, + userSelect: "none", + }} + /> + copyQuestion(quizId, totalIndex)} + > + + + { + const removedId = question.id; + if (question.deleteTimeoutId) { + clearTimeout(question.deleteTimeoutId); + } + + removeQuestion(quizId, totalIndex); + + const newTimeoutId = window.setTimeout(() => { + removeQuestionForce(quizId, removedId); + }, 5000); + + updateQuestionsList(quizId, totalIndex, { + ...question, + deleteTimeoutId: newTimeoutId, + }); + }} + > + + + + )} + + + + + + + + {question.expanded && ( + + {question.type === "nonselected" ? ( + + ) : ( + + )} + + )} + + setPlusVisible(true)} + onMouseLeave={() => setPlusVisible(false)} + sx={{ + maxWidth: "825px", + display: "flex", + alignItems: "center", + height: "40px", + cursor: "pointer", + }} + > + createQuestion(quizId, "nonselected", totalIndex + 1)} + sx={{ + display: plusVisible && !isDragging ? "flex" : "none", + width: "100%", + alignItems: "center", + columnGap: "10px", + }} + > + + + + + + ); } const IconAndrom = (isExpanded: boolean, switchState: string) => { - switch (switchState) { - case "variant": - return ( - - ); - case "images": - return ( - - ); - case "varimg": - return ( - - ); - case "emoji": - return ( - - ); - case "text": - return ( - - ); - case "select": - return ( - - ); - case "date": - return ( - - ); - case "number": - return ( - - ); - case "file": - return ( - - ); - case "page": - return ( - - ); - case "rating": - return ( - - ); - default: - return <>; - } -}; -export default function QuestionsPageCard({ - totalIndex, - draggableProps, - isDragging, -}: Props) { - const [plusVisible, setPlusVisible] = useState(false); - const [open, setOpen] = useState(false); - const quizId = Number(useParams().quizId); - const theme = useTheme(); - const isTablet = useMediaQuery(theme.breakpoints.down(1000)); - const isMobile = useMediaQuery(theme.breakpoints.down(790)); - const { listQuestions } = questionStore(); - const question = listQuestions[quizId][totalIndex]; - const anchorRef = useRef(null); - const debounced = useDebouncedCallback((title) => { - updateQuestionsList(quizId, totalIndex, { title }); - }, 200); - - useEffect(() => { - if (question.deleteTimeoutId) { - clearTimeout(question.deleteTimeoutId); + switch (switchState) { + case "variant": + return ( + + ); + case "images": + return ( + + ); + case "varimg": + return ( + + ); + case "emoji": + return ( + + ); + case "text": + return ( + + ); + case "select": + return ( + + ); + case "date": + return ( + + ); + case "number": + return ( + + ); + case "file": + return ( + + ); + case "page": + return ( + + ); + case "rating": + return ( + + ); + default: + return <>; } - }, [question]); - - return ( - <> - - - - debounced(target.value)} - InputProps={{ - startAdornment: ( - - setOpen((isOpened) => !isOpened)} - > - {IconAndrom(question.expanded, question.type)} - - setOpen(false)} - anchorRef={anchorRef} - totalIndex={totalIndex} - switchState={question.type} - /> - - ), - }} - 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, - }, - }, - }} - inputProps={{ - sx: { - fontSize: "18px", - lineHeight: "21px", - py: 0, - paddingLeft: question.type.length === 0 ? 0 : "18px", - }, - "data-cy": "quiz-question-title", - }} - /> - - - - updateQuestionsList(quizId, totalIndex, { - expanded: !question.expanded, - }) - } - > - {question.expanded ? ( - - ) : ( - - )} - - {question.expanded ? ( - <> - ) : ( - - - } - checkedIcon={} - /> - } - label={""} - sx={{ - color: theme.palette.grey2.main, - ml: "-9px", - mr: 0, - userSelect: "none", - }} - /> - copyQuestion(quizId, totalIndex)} - > - - - { - const removedId = question.id; - if (question.deleteTimeoutId) { - clearTimeout(question.deleteTimeoutId); - } - - removeQuestion(quizId, totalIndex); - - const newTimeoutId = window.setTimeout(() => { - removeQuestionForce(quizId, removedId); - }, 5000); - - updateQuestionsList(quizId, totalIndex, { - ...question, - deleteTimeoutId: newTimeoutId, - }); - }} - > - - - - )} - - - - - - - - {question.expanded && ( - - {question.type === "nonselected" ? ( - - ) : ( - - )} - - )} - - setPlusVisible(true)} - onMouseLeave={() => setPlusVisible(false)} - sx={{ - maxWidth: "825px", - display: "flex", - alignItems: "center", - height: "40px", - cursor: "pointer", - }} - > - createQuestion(quizId, "nonselected", totalIndex + 1)} - sx={{ - display: plusVisible && !isDragging ? "flex" : "none", - width: "100%", - alignItems: "center", - columnGap: "10px", - }} - > - - - - - - ); -} +}; diff --git a/src/pages/Questions/DraggableList/index.tsx b/src/pages/Questions/DraggableList/index.tsx index 7ff2474c..abafd4bc 100644 --- a/src/pages/Questions/DraggableList/index.tsx +++ b/src/pages/Questions/DraggableList/index.tsx @@ -1,48 +1,59 @@ -import { useParams } from "react-router-dom"; import { Box } from "@mui/material"; import { DragDropContext, Droppable } from "react-beautiful-dnd"; - import DraggableListItem from "./DraggableListItem"; - -import { questionStore, updateQuestionsListDragAndDrop } from "@root/questions"; - -import { reorder } from "./helper"; - import type { DropResult } from "react-beautiful-dnd"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { useQuestionArray } from "@root/questions/hooks"; +import useSWR from "swr"; +import { questionApi } from "@api/question"; +import { setQuestions } from "@root/questions/actions"; +import { isAxiosError } from "axios"; +import { devlog } from "@frontend/kitui"; +import { enqueueSnackbar } from "notistack"; + export const DraggableList = () => { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); + const { quiz } = useCurrentQuiz(); + useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), { + onSuccess: setQuestions, + onError: error => { + const message = isAxiosError(error) ? (error.response?.data ?? "") : ""; - const onDragEnd = ({ destination, source }: DropResult) => { - if (destination) { - const newItems = reorder( - listQuestions[quizId], - source.index, - destination.index - ); + devlog("Error getting question list", error); + enqueueSnackbar(`Не удалось получить вопросы. ${message}`); + } + }); + const questions = useQuestionArray(); - updateQuestionsListDragAndDrop(quizId, newItems); - } - }; + const onDragEnd = ({ destination, source }: DropResult) => { // TODO + // if (destination) { + // const newItems = reorder( + // listQuestions[quizId], + // source.index, + // destination.index + // ); - return ( - - - {(provided, snapshot) => ( - - {listQuestions[quizId]?.map((_, index) => ( - - ))} - {provided.placeholder} - - )} - - - ); + // updateQuestionsListDragAndDrop(quizId, newItems); + // } + }; + + return ( + + + {(provided, snapshot) => ( + + {questions.map((question, index) => ( + + ))} + {provided.placeholder} + + )} + + + ); }; diff --git a/src/pages/Questions/QuestionsPage.tsx b/src/pages/Questions/QuestionsPage.tsx index 7586087e..adb96775 100755 --- a/src/pages/Questions/QuestionsPage.tsx +++ b/src/pages/Questions/QuestionsPage.tsx @@ -1,120 +1,106 @@ import { - Box, - Button, - IconButton, - Typography, - useMediaQuery, - useTheme, + Box, + Button, + IconButton, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; -import AddPlus from "../../assets/icons/questionsPage/addPlus"; -import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; -import { quizStore } from "@root/quizes"; -import { useParams } from "react-router-dom"; -import { - questionStore, - createQuestion, - updateQuestionsList, -} from "@root/questions"; -import { DraggableList } from "./DraggableList"; - -import type { AnyQuizQuestion } from "../../model/questionTypes/shared"; +import { createQuestion } from "@root/questions/actions"; +import { incrementCurrentStep } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import { createPortal } from "react-dom"; +import AddPlus from "../../assets/icons/questionsPage/addPlus"; +import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; +import { DraggableList } from "./DraggableList"; + export default function QuestionsPage() { - const { listQuizes, updateQuizesList } = quizStore(); - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - const handleNext = () => { - updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 }); - }; + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(660)); + const { quiz } = useCurrentQuiz(); - const handleBack = () => { - let result = listQuizes[quizId].step - 1; - updateQuizesList(quizId, { step: result ? result : 1 }); - }; + const collapseEverything = () => { // TODO + // listQuestions[quizId].forEach((item, index) => { + // updateQuestionsList(quizId, index, { + // ...item, + // expanded: false, + // }); + // }); + }; - const collapseEverything = () => { - listQuestions[quizId].forEach((item, index) => { - updateQuestionsList(quizId, index, { - ...item, - expanded: false, - }); - }); - }; + if (!quiz) return null; - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down(660)); - - return ( - <> - - Заголовок квиза - - - - - { - createQuestion(quizId); - }} - sx={{ - position: "fixed", - left: isMobile ? "20px" : "250px", - bottom: "20px", - }} - > - - - - - - - - {createPortal(, document.body)} - - ); + return ( + <> + + Заголовок квиза + + + + + { + createQuestion(quiz.id); + }} + sx={{ + position: "fixed", + left: isMobile ? "20px" : "250px", + bottom: "20px", + }} + > + + + + + + + + {createPortal(, document.body)} + + ); } diff --git a/src/pages/Questions/TypeQuestions.tsx b/src/pages/Questions/TypeQuestions.tsx index 9d91c317..9b79d94f 100755 --- a/src/pages/Questions/TypeQuestions.tsx +++ b/src/pages/Questions/TypeQuestions.tsx @@ -1,33 +1,24 @@ +import { Box } from "@mui/material"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; import Answer from "../../assets/icons/questionsPage/answer"; -import OptionsPict from "../../assets/icons/questionsPage/options_pict"; -import OptionsAndPict from "../../assets/icons/questionsPage/options_and_pict"; +import Date from "../../assets/icons/questionsPage/date"; +import Download from "../../assets/icons/questionsPage/download"; +import DropDown from "../../assets/icons/questionsPage/drop_down"; import Emoji from "../../assets/icons/questionsPage/emoji"; import Input from "../../assets/icons/questionsPage/input"; -import DropDown from "../../assets/icons/questionsPage/drop_down"; -import Date from "../../assets/icons/questionsPage/date"; -import Slider from "../../assets/icons/questionsPage/slider"; -import Download from "../../assets/icons/questionsPage/download"; +import OptionsAndPict from "../../assets/icons/questionsPage/options_and_pict"; +import OptionsPict from "../../assets/icons/questionsPage/options_pict"; import Page from "../../assets/icons/questionsPage/page"; import RatingIcon from "../../assets/icons/questionsPage/rating"; -import { Box } from "@mui/material"; -import React from "react"; -import { useParams } from "react-router-dom"; - -import { - questionStore, - updateQuestionsList, - createQuestion, - removeQuestionForce, -} from "@root/questions"; - +import Slider from "../../assets/icons/questionsPage/slider"; +import { setQuestionFieldOptimistic } from "@root/questions/actions"; import type { - QuizQuestionType, - QuizQuestionBase, + AnyQuizQuestion, + QuizQuestionType } from "../../model/questionTypes/shared"; interface Props { - totalIndex: number; + question: AnyQuizQuestion; } type ButtonTypeQuestion = { @@ -36,6 +27,30 @@ type ButtonTypeQuestion = { value: QuizQuestionType; }; +export default function TypeQuestions({ question }: Props) { + + return ( + + {BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => ( + setQuestionFieldOptimistic(question.id, "type", value)} + icon={icon} + text={title} + /> + ))} + + ); +} + export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [ { icon: , @@ -93,38 +108,3 @@ export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [ value: "rating", }, ]; - -export default function TypeQuestions({ totalIndex }: Props) { - const quizId = Number(useParams().quizId); - const { listQuestions } = questionStore(); - - return ( - - {BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => ( - { - const question = { ...listQuestions[quizId][totalIndex] }; - - removeQuestionForce(quizId, question.id); - createQuestion(quizId, value, totalIndex); - updateQuestionsList(quizId, totalIndex, { - expanded: question.expanded, - type: value, - }); - }} - icon={icon} - text={title} - /> - ))} - - ); -} diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 7f06090a..ab27af40 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -1,33 +1,35 @@ import { questionApi } from "@api/question"; import { devlog } from "@frontend/kitui"; -import { Question } from "@model/question/question"; +import { RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { QuestionsStore, useQuestionsStore } from "./store"; +import { questionToEditQuestionRequest } from "@model/question/edit"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; -export const setQuestions = (quizes: Question[] | null) => setProducedState(state => { +export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { state.questionsById = {}; - if (quizes === null) return; + if (questions === null) return; - quizes.forEach(question => state.questionsById[question.id] = question); + questions.forEach(question => state.questionsById[question.id] = rawQuestionToQuestion(question)); }, { - type: "setQuizes", - quizes, + type: "setQuestions", + questions, }); -export const setQuestion = (question: Question) => setProducedState(state => { +export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => { state.questionsById[question.id] = question; }, { type: "setQuestion", question, }); -export const setQuestionField = ( +export const setQuestionField = ( questionId: number, field: T, - value: Question[T], + value: AnyQuizQuestion[T], ) => setProducedState(state => { const question = state.questionsById[questionId]; if (!question) return; @@ -40,13 +42,13 @@ export const setQuestionField = ( value, }); -let savedOriginalQuestion: Question | null = null; +let savedOriginalQuestion: AnyQuizQuestion | null = null; let controller: AbortController | null = null; -export const setQuestionFieldOptimistic = async ( +export const setQuestionFieldOptimistic = async ( questionId: number, field: T, - value: Question[T], + value: AnyQuizQuestion[T], ) => { const question = useQuestionsStore.getState().questionsById[questionId] ?? null; if (!question) return; @@ -60,9 +62,12 @@ export const setQuestionFieldOptimistic = async ( setQuestion(currentUpdatedQuestion); try { - const { updated } = await questionApi.edit(currentUpdatedQuestion, controller.signal); + const { updated } = await questionApi.edit( + questionToEditQuestionRequest(currentUpdatedQuestion), + controller.signal, + ); - setQuestionField(question.id, "version", updated); + setQuestionField(question.id, "id", updated); controller = null; savedOriginalQuestion = null; } catch (error) { @@ -83,7 +88,7 @@ export const setQuestionFieldOptimistic = async ( export const updateQuestionWithFn = ( questionId: number, - updateFn: (question: Question) => void, + updateFn: (question: AnyQuizQuestion) => void, ) => setProducedState(state => { const question = state.questionsById[questionId]; if (!question) return; @@ -101,7 +106,7 @@ export const createQuestion = async (quizId: number) => { quiz_id: quizId, }); - setQuestion(question); + setQuestion(rawQuestionToQuestion(question)); } catch (error) { devlog("Error creating question", error); enqueueSnackbar("Не удалось создать вопрос"); diff --git a/src/stores/questions/store.ts b/src/stores/questions/store.ts index f5f00207..b1e7c8a2 100644 --- a/src/stores/questions/store.ts +++ b/src/stores/questions/store.ts @@ -1,10 +1,10 @@ -import { Question } from "@model/question/question"; +import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; export type QuestionsStore = { - questionsById: Record; + questionsById: Record; }; const initialState: QuestionsStore = {