diff --git a/package.json b/package.json index 46011cf8..be9d1334 100755 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-cytoscapejs": "^2.0.0", + "react-datepicker": "^4.24.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", @@ -79,6 +80,7 @@ "@emoji-mart/react": "^1.1.1", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-cytoscapejs": "^1.2.4", + "@types/react-datepicker": "^4.19.3", "craco-alias": "^3.0.1", "cypress": "^13.4.0" } diff --git a/src/App.tsx b/src/App.tsx index 56d797b3..a6f4ded1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import dayjs from "dayjs"; import "dayjs/locale/ru"; import SigninDialog from "./pages/auth/Signin"; import SignupDialog from "./pages/auth/Signup"; +import { ViewPage } from "./pages/ViewPublicationPage"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import "./index.css"; import ContactFormPage from "./pages/ContactFormPage/ContactFormPage"; @@ -61,6 +62,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/src/assets/icons/questionsPage/StarIconMini.tsx b/src/assets/icons/questionsPage/StarIconMini.tsx index 561c7413..96a7a2b1 100644 --- a/src/assets/icons/questionsPage/StarIconMini.tsx +++ b/src/assets/icons/questionsPage/StarIconMini.tsx @@ -1,19 +1,23 @@ import { Box } from "@mui/material"; +import type { SxProps } from "@mui/material"; + interface Props { color: string; - width?: string; + width?: number; + sx?: SxProps; } -export default function StarIconMini({ color, width = "30px" }: Props) { +export default function StarIconMini({ color, width = 30, sx }: Props) { return ( diff --git a/src/constants/base.ts b/src/constants/base.ts index 0fa0b59c..d74a7d73 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -17,16 +17,9 @@ export const QUIZ_QUESTION_BASE: Omit = { video: "", }, rule: { - or: true, - show: true, - title: "", - reqs: [ - { - id: "", - vars: [], - }, - ], - }, + default: "", + main: [], + }, back: "", originalBack: "", autofill: false, diff --git a/src/model/questionTypes/date.ts b/src/model/questionTypes/date.ts index 2b905e85..3f9bfaa0 100644 --- a/src/model/questionTypes/date.ts +++ b/src/model/questionTypes/date.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionDate extends QuizQuestionBase { @@ -16,7 +16,7 @@ export interface QuizQuestionDate extends QuizQuestionBase { dateRange: boolean; time: boolean; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/emoji.ts b/src/model/questionTypes/emoji.ts index 4a9f7efc..017580ce 100644 --- a/src/model/questionTypes/emoji.ts +++ b/src/model/questionTypes/emoji.ts @@ -2,7 +2,7 @@ import type { QuizQuestionBase, QuestionVariant, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionEmoji extends QuizQuestionBase { @@ -20,7 +20,7 @@ export interface QuizQuestionEmoji extends QuizQuestionBase { required: boolean; variants: QuestionVariant[]; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/file.ts b/src/model/questionTypes/file.ts index d1a6981d..83ccfa95 100644 --- a/src/model/questionTypes/file.ts +++ b/src/model/questionTypes/file.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export const UPLOAD_FILE_TYPES_MAP = { @@ -27,7 +27,7 @@ export interface QuizQuestionFile extends QuizQuestionBase { autofill: boolean; type: UploadFileType; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; }; diff --git a/src/model/questionTypes/images.ts b/src/model/questionTypes/images.ts index 0317fff7..65ff1937 100644 --- a/src/model/questionTypes/images.ts +++ b/src/model/questionTypes/images.ts @@ -1,36 +1,36 @@ import type { - QuestionBranchingRule, - QuestionHint, - QuestionVariant, - QuizQuestionBase + QuestionHint, + QuestionVariant, + QuizQuestionBase, + PreviewRule, } from "./shared"; export interface QuizQuestionImages extends QuizQuestionBase { - type: "images"; - content: { - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Можно несколько" */ - multi: boolean; - /** Пропорции */ - xy: "1:1" | "1:2" | "2:1"; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Большие картинки" */ - large: boolean; - /** Форма */ - format: "carousel" | "masonry"; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - /** Варианты (картинки) */ - variants: QuestionVariant[]; - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string; - originalBack: string; - autofill: boolean; - largeCheck: boolean; - }; + type: "images"; + content: { + /** Чекбокс "Вариант "свой ответ"" */ + own: boolean; + /** Чекбокс "Можно несколько" */ + multi: boolean; + /** Пропорции */ + xy: "1:1" | "1:2" | "2:1"; + /** Чекбокс "Внутреннее название вопроса" */ + innerNameCheck: boolean; + /** Поле "Внутреннее название вопроса" */ + innerName: string; + /** Чекбокс "Большие картинки" */ + large: boolean; + /** Форма */ + format: "carousel" | "masonry"; + /** Чекбокс "Необязательный вопрос" */ + required: boolean; + /** Варианты (картинки) */ + variants: QuestionVariant[]; + hint: QuestionHint; + rule: PreviewRule; + back: string; + originalBack: string; + autofill: boolean; + largeCheck: boolean; + }; } diff --git a/src/model/questionTypes/number.ts b/src/model/questionTypes/number.ts index 68f1370e..33a03f93 100644 --- a/src/model/questionTypes/number.ts +++ b/src/model/questionTypes/number.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionNumber extends QuizQuestionBase { @@ -25,7 +25,7 @@ export interface QuizQuestionNumber extends QuizQuestionBase { /** Чекбокс "Выбор диапазона (два ползунка)" */ chooseRange: boolean; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/page.ts b/src/model/questionTypes/page.ts index 9f829c86..d7183a07 100644 --- a/src/model/questionTypes/page.ts +++ b/src/model/questionTypes/page.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionPage extends QuizQuestionBase { @@ -16,7 +16,7 @@ export interface QuizQuestionPage extends QuizQuestionBase { originalPicture: string; video: string; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/rating.ts b/src/model/questionTypes/rating.ts index 2f73ef17..9acc6b8d 100644 --- a/src/model/questionTypes/rating.ts +++ b/src/model/questionTypes/rating.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionRating extends QuizQuestionBase { @@ -18,7 +18,7 @@ export interface QuizQuestionRating extends QuizQuestionBase { /** Форма иконки */ form: string; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/select.ts b/src/model/questionTypes/select.ts index f74cfdb5..00d34ee1 100644 --- a/src/model/questionTypes/select.ts +++ b/src/model/questionTypes/select.ts @@ -2,7 +2,7 @@ import type { QuizQuestionBase, QuestionVariant, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionSelect extends QuizQuestionBase { @@ -19,7 +19,7 @@ export interface QuizQuestionSelect extends QuizQuestionBase { /** Поле "Текст в выпадающем списке" */ default: string; variants: QuestionVariant[]; - rule: QuestionBranchingRule; + rule: PreviewRule; hint: QuestionHint; back: string; originalBack: string; diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index 14e7d847..36200c73 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -12,95 +12,104 @@ import type { QuizQuestionVariant } from "./variant"; import type { QuizQuestionVarImg } from "./varimg"; import { nanoid } from "nanoid"; +export type Rule = { + /* question id */ + question: string; + /* Ответы на вопросы. Для вариантов выбора - конкретные айдишники ответов, для полей ввода текста - текст по полному совпадению, для ввода файла - просто факт того что файл ввели, т.е. boolean */ + answers: (number | string | boolean)[]; +}; -export interface QuestionBranchingRule { - /** Радиокнопка "Все условия обязательны" */ - or: boolean; - show: boolean; - title: string; - reqs: { - id: string; - /** Список выбранных вариантов */ - vars: number[]; - }[]; +export type PreviewRuleInfo = { + /* Id следующего вопроса */ + next: string; + /* Радиокнопка "Все условия обязательны" */ + or: boolean; + rules: Rule[]; +}; + +export interface PreviewRule { + default: string; + main: PreviewRuleInfo[]; } export interface QuestionHint { - /** Текст подсказки */ - text: string; - /** URL видео подсказки */ - video: string; + /** Текст подсказки */ + text: string; + /** URL видео подсказки */ + video: string; } export type QuestionVariant = { - id: string; - /** Текст */ - answer: string; - /** Текст подсказки */ - hints: string; - /** Дополнительное поле для текста, emoji, ссылки на картинку */ - extendedText: string; - /** Оригинал изображения (до кропа) */ - originalImageUrl: string; + id: string; + /** Текст */ + answer: string; + /** Текст подсказки */ + hints: string; + /** Дополнительное поле для текста, emoji, ссылки на картинку */ + extendedText: string; + /** Оригинал изображения (до кропа) */ + originalImageUrl: string; }; export interface QuizQuestionBase { - backendId: number; - /** Stable id, generated on client */ - id: string; - quizId: number; - title: string; - description: string; - page: number; - type?: QuestionType | null; - expanded: boolean; - openedModalSettings: boolean; - required: boolean; - deleted: boolean; - deleteTimeoutId: number; - content: { - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string; - originalBack: string; - autofill: boolean; - }; + backendId: number; + /** Stable id, generated on client */ + id: string; + quizId: number; + title: string; + description: string; + page: number; + type?: QuestionType | null; + expanded: boolean; + openedModalSettings: boolean; + required: boolean; + deleted: boolean; + deleteTimeoutId: number; + content: { + hint: QuestionHint; + rule: PreviewRule; + back: string; + originalBack: string; + autofill: boolean; + }; } export interface UntypedQuizQuestion { - type: null; - id: string; - quizId: number; - title: string; - description: string; - expanded: boolean; - deleted: boolean; + type: null; + id: string; + quizId: number; + title: string; + description: string; + expanded: boolean; + deleted: boolean; } export type AnyTypedQuizQuestion = - | QuizQuestionVariant - | QuizQuestionImages - | QuizQuestionVarImg - | QuizQuestionEmoji - | QuizQuestionText - | QuizQuestionSelect - | QuizQuestionDate - | QuizQuestionNumber - | QuizQuestionFile - | QuizQuestionPage - | QuizQuestionRating; + | QuizQuestionVariant + | QuizQuestionImages + | QuizQuestionVarImg + | QuizQuestionEmoji + | QuizQuestionText + | QuizQuestionSelect + | QuizQuestionDate + | QuizQuestionNumber + | QuizQuestionFile + | QuizQuestionPage + | QuizQuestionRating; type FilterQuestionsWithVariants = T extends { - content: { variants: QuestionVariant[]; }; -} ? T : never; - -export type QuizQuestionsWithVariants = FilterQuestionsWithVariants; + content: { variants: QuestionVariant[] }; +} + ? T + : never; +export type QuizQuestionsWithVariants = + FilterQuestionsWithVariants; export const createQuestionVariant: () => QuestionVariant = () => ({ - id: nanoid(), - answer: "", - extendedText: "", - hints: "", - originalImageUrl: "", -}); + id: nanoid(), + answer: "", + extendedText: "", + hints: "", + originalImageUrl: "", +}); diff --git a/src/model/questionTypes/text.ts b/src/model/questionTypes/text.ts index 8b636013..587ae648 100644 --- a/src/model/questionTypes/text.ts +++ b/src/model/questionTypes/text.ts @@ -1,7 +1,7 @@ import type { QuizQuestionBase, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionText extends QuizQuestionBase { @@ -18,7 +18,7 @@ export interface QuizQuestionText extends QuizQuestionBase { autofill: boolean; answerType: "single" | "multi"; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; onlyNumbers: boolean; diff --git a/src/model/questionTypes/variant.ts b/src/model/questionTypes/variant.ts index 32c0c4bc..2188cbe7 100644 --- a/src/model/questionTypes/variant.ts +++ b/src/model/questionTypes/variant.ts @@ -2,7 +2,7 @@ import type { QuizQuestionBase, QuestionVariant, QuestionHint, - QuestionBranchingRule, + PreviewRule, } from "./shared"; export interface QuizQuestionVariant extends QuizQuestionBase { @@ -23,7 +23,7 @@ export interface QuizQuestionVariant extends QuizQuestionBase { /** Варианты ответов */ variants: QuestionVariant[]; hint: QuestionHint; - rule: QuestionBranchingRule; + rule: PreviewRule; back: string; originalBack: string; autofill: boolean; diff --git a/src/model/questionTypes/varimg.ts b/src/model/questionTypes/varimg.ts index 741b91a6..1ac78525 100644 --- a/src/model/questionTypes/varimg.ts +++ b/src/model/questionTypes/varimg.ts @@ -1,28 +1,28 @@ import type { - QuestionBranchingRule, - QuestionHint, - QuestionVariant, - QuizQuestionBase + QuestionHint, + QuestionVariant, + QuizQuestionBase, + PreviewRule, } from "./shared"; export interface QuizQuestionVarImg extends QuizQuestionBase { - type: "varimg"; - content: { - /** Чекбокс "Вариант "свой ответ"" */ - own: boolean; - /** Чекбокс "Внутреннее название вопроса" */ - innerNameCheck: boolean; - /** Поле "Внутреннее название вопроса" */ - innerName: string; - /** Чекбокс "Необязательный вопрос" */ - required: boolean; - variants: QuestionVariant[]; - hint: QuestionHint; - rule: QuestionBranchingRule; - back: string; - originalBack: string; - autofill: boolean; - largeCheck: boolean; - replText: string; - }; + type: "varimg"; + content: { + /** Чекбокс "Вариант "свой ответ"" */ + own: boolean; + /** Чекбокс "Внутреннее название вопроса" */ + innerNameCheck: boolean; + /** Поле "Внутреннее название вопроса" */ + innerName: string; + /** Чекбокс "Необязательный вопрос" */ + required: boolean; + variants: QuestionVariant[]; + hint: QuestionHint; + rule: PreviewRule; + back: string; + originalBack: string; + autofill: boolean; + largeCheck: boolean; + replText: string; + }; } diff --git a/src/pages/Questions/RatingOptions/RatingOptions.tsx b/src/pages/Questions/RatingOptions/RatingOptions.tsx index 9fd1dd82..31ee01f0 100644 --- a/src/pages/Questions/RatingOptions/RatingOptions.tsx +++ b/src/pages/Questions/RatingOptions/RatingOptions.tsx @@ -73,7 +73,7 @@ export default function RatingOptions({ question }: Props) { const buttonRatingForm: ButtonRatingFrom[] = [ { name: "star", - icon: , + icon: , }, { name: "trophie", icon: }, { name: "flag", icon: }, diff --git a/src/pages/Questions/branchingQuestions.tsx b/src/pages/Questions/branchingQuestions.tsx index 4a5836a5..fbb7ba83 100644 --- a/src/pages/Questions/branchingQuestions.tsx +++ b/src/pages/Questions/branchingQuestions.tsx @@ -71,7 +71,7 @@ export default function BranchingQuestions({ p: 0, }} > - - + */} diff --git a/src/pages/ViewPublicationPage/Footer.tsx b/src/pages/ViewPublicationPage/Footer.tsx new file mode 100644 index 00000000..975def78 --- /dev/null +++ b/src/pages/ViewPublicationPage/Footer.tsx @@ -0,0 +1,127 @@ +import { useState, useEffect } from "react"; +import { Box, Typography, Button, useTheme } from "@mui/material"; + +import { useQuizViewStore } from "@root/quizView"; + +import type { QuizQuestionBase } from "../../model/questionTypes/shared"; + +type FooterProps = { + stepNumber: number; + setStepNumber: (step: number) => void; + questions: QuizQuestionBase[]; +}; + +export const Footer = ({ + stepNumber, + setStepNumber, + questions, +}: FooterProps) => { + const [disabledQuestionsId, setDisabledQuestionsId] = useState>( + new Set() + ); + const { answers } = useQuizViewStore(); + const theme = useTheme(); + + useEffect(() => { + clearDisabledQuestions(); + + const nextStepId = questions[stepNumber + 1].id; + + const disabledIds = [] as string[]; + + const newDisabledIds = new Set([...disabledQuestionsId, ...disabledIds]); + setDisabledQuestionsId(newDisabledIds); + }, [answers]); + + const clearDisabledQuestions = () => { + const cleanDisabledQuestions = new Set(); + + answers.forEach(({ step, answer }) => { + questions[step].content.rule.main.forEach(({ next, rules }) => { + rules.forEach(({ answers }) => { + if (answer !== answers[0]) { + cleanDisabledQuestions.add(next); + } + }); + }); + }); + + setDisabledQuestionsId(cleanDisabledQuestions); + }; + + const followPreviousStep = () => { + setStepNumber(stepNumber - 1); + }; + + const followNextStep = () => { + setStepNumber(stepNumber + 1); + }; + + return ( + + + + Шаг + + {stepNumber} + + Из + + {questions.length} + + + + + + + ); +}; diff --git a/src/pages/ViewPublicationPage/Question.tsx b/src/pages/ViewPublicationPage/Question.tsx new file mode 100644 index 00000000..cef98135 --- /dev/null +++ b/src/pages/ViewPublicationPage/Question.tsx @@ -0,0 +1,72 @@ +import { Box } from "@mui/material"; + +import { Variant } from "./questions/Variant"; +import { Images } from "./questions/Images"; +import { Varimg } from "./questions/Varimg"; +import { Emoji } from "./questions/Emoji"; +import { Text } from "./questions/Text"; +import { Select } from "./questions/Select"; +import { Date } from "./questions/Date"; +import { Number } from "./questions/Number"; +import { File } from "./questions/File"; +import { Page } from "./questions/Page"; +import { Rating } from "./questions/Rating"; +import { Footer } from "./Footer"; + +import type { FC } from "react"; +import type { QuestionType } from "../../model/question/question"; +import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; + +type QuestionProps = { + stepNumber: number; + setStepNumber: (step: number) => void; + questions: AnyTypedQuizQuestion[]; +}; + +const QUESTIONS_MAP: Record< + Exclude, + FC<{ stepNumber: number; question: any }> +> = { + variant: Variant, + images: Images, + varimg: Varimg, + emoji: Emoji, + text: Text, + select: Select, + date: Date, + number: Number, + file: File, + page: Page, + rating: Rating, +}; + +export const Question = ({ + stepNumber, + setStepNumber, + questions, +}: QuestionProps) => { + const question = questions[stepNumber - 1] as AnyTypedQuizQuestion; + const QuestionComponent = + QUESTIONS_MAP[question.type as Exclude]; + + return ( + + + + +