Merge branch 'branching' into view-integration
This commit is contained in:
commit
b930dd8260
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
18910
package-lock.json
generated
Normal file
18910
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -13,6 +13,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
|
"@types/cytoscape": "^3.19.16",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.5.1",
|
||||||
"cytoscape": "^3.26.0",
|
"cytoscape": "^3.26.0",
|
||||||
|
"cytoscape-popper": "^2.0.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@ -44,7 +46,7 @@
|
|||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"swr": "^2.2.4",
|
"swr": "^2.2.4",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^5.2.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"yup": "^1.3.2",
|
"yup": "^1.3.2",
|
||||||
@ -57,12 +59,6 @@
|
|||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"cypress:open": "cypress open"
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@ -78,6 +74,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@types/cytoscape-popper": "^2.0.4",
|
||||||
"@types/react-beautiful-dnd": "^13.1.4",
|
"@types/react-beautiful-dnd": "^13.1.4",
|
||||||
"@types/react-cytoscapejs": "^1.2.4",
|
"@types/react-cytoscapejs": "^1.2.4",
|
||||||
"@types/react-datepicker": "^4.19.3",
|
"@types/react-datepicker": "^4.19.3",
|
||||||
|
|||||||
@ -30,6 +30,7 @@ async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
||||||
|
console.log("`${baseUrl}/question/edit` start")
|
||||||
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
||||||
url: `${baseUrl}/question/edit`,
|
url: `${baseUrl}/question/edit`,
|
||||||
body,
|
body,
|
||||||
|
|||||||
@ -69,7 +69,7 @@ function addQuizImages(quizId: number, image: Blob) {
|
|||||||
formData.append("quiz", quizId.toString());
|
formData.append("quiz", quizId.toString());
|
||||||
formData.append("image", image);
|
formData.append("image", image);
|
||||||
|
|
||||||
return makeRequest<FormData, never>({
|
return makeRequest<FormData, { [key: string]: string; }>({
|
||||||
url: `${imagesUrl}/quiz/putImages`,
|
url: `${imagesUrl}/quiz/putImages`,
|
||||||
body: formData,
|
body: formData,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@ -93,7 +93,7 @@ const defaultCreateQuizBody: CreateQuizRequest = {
|
|||||||
"note_prevented": true,
|
"note_prevented": true,
|
||||||
"mail_notifications": false,
|
"mail_notifications": false,
|
||||||
"unique_answers": true,
|
"unique_answers": true,
|
||||||
"name": "Название квиза",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"config": JSON.stringify(defaultQuizConfig),
|
"config": JSON.stringify(defaultQuizConfig),
|
||||||
"status": "stop",
|
"status": "stop",
|
||||||
|
|||||||
6
src/assets/icons/ArrowGear.svg
Normal file
6
src/assets/icons/ArrowGear.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.7 KiB |
4
src/assets/icons/checked.svg
Normal file
4
src/assets/icons/checked.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 -2.62268e-07C21.3137 -1.17422e-07 24 2.68629 24 6L24 18C24 21.3137 21.3137 24 18 24L6 24C2.68629 24 -9.31652e-07 21.3137 -7.86805e-07 18L-5.24537e-07 12L-2.62268e-07 6C-1.17422e-07 2.68629 2.68629 -9.31652e-07 6 -7.86805e-07L18 -2.62268e-07Z" fill="#9A9AAF" fill-opacity="0.7"/>
|
||||||
|
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 495 B |
@ -1,4 +1,4 @@
|
|||||||
import type { QuizQuestionBase } from "../model/questionTypes/shared";
|
import type { QuizQuestionBase, QuestionBranchingRuleMain } from "../model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
||||||
@ -12,13 +12,15 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
|||||||
deleted: false,
|
deleted: false,
|
||||||
deleteTimeoutId: 0,
|
deleteTimeoutId: 0,
|
||||||
content: {
|
content: {
|
||||||
|
id: "",
|
||||||
hint: {
|
hint: {
|
||||||
text: "",
|
text: "",
|
||||||
video: "",
|
video: "",
|
||||||
},
|
},
|
||||||
rule: {
|
rule: {
|
||||||
default: "",
|
main: [] as QuestionBranchingRuleMain[],
|
||||||
main: [],
|
parentId: "",
|
||||||
|
default: ""
|
||||||
},
|
},
|
||||||
back: "",
|
back: "",
|
||||||
originalBack: "",
|
originalBack: "",
|
||||||
|
|||||||
@ -46,16 +46,18 @@ export interface RawQuestion {
|
|||||||
|
|
||||||
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion {
|
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion {
|
||||||
let content = defaultQuestionByType[rawQuestion.type].content;
|
let content = defaultQuestionByType[rawQuestion.type].content;
|
||||||
|
const frontId = nanoid()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(rawQuestion.content);
|
content = JSON.parse(rawQuestion.content);
|
||||||
|
if (content.id.length === 0 || content.id.length === undefined) content.id = frontId
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Cannot parse question content from string, using default content", error);
|
console.warn("Cannot parse question content from string, using default content", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backendId: rawQuestion.id,
|
backendId: rawQuestion.id,
|
||||||
id: nanoid(),
|
id: frontId,
|
||||||
description: rawQuestion.description,
|
description: rawQuestion.description,
|
||||||
page: rawQuestion.page,
|
page: rawQuestion.page,
|
||||||
quizId: rawQuestion.quiz_id,
|
quizId: rawQuestion.quiz_id,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
export interface QuizQuestionDate extends QuizQuestionBase {
|
export interface QuizQuestionDate extends QuizQuestionBase {
|
||||||
type: "date";
|
type: "date";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
export interface QuizQuestionEmoji extends QuizQuestionBase {
|
export interface QuizQuestionEmoji extends QuizQuestionBase {
|
||||||
type: "emoji";
|
type: "emoji";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Можно несколько" */
|
/** Чекбокс "Можно несколько" */
|
||||||
multi: boolean;
|
multi: boolean;
|
||||||
/** Чекбокс "Вариант "свой ответ"" */
|
/** Чекбокс "Вариант "свой ответ"" */
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP;
|
|||||||
export interface QuizQuestionFile extends QuizQuestionBase {
|
export interface QuizQuestionFile extends QuizQuestionBase {
|
||||||
type: "file";
|
type: "file";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
export interface QuizQuestionImages extends QuizQuestionBase {
|
export interface QuizQuestionImages extends QuizQuestionBase {
|
||||||
type: "images";
|
type: "images";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Вариант "свой ответ"" */
|
/** Чекбокс "Вариант "свой ответ"" */
|
||||||
own: boolean;
|
own: boolean;
|
||||||
/** Чекбокс "Можно несколько" */
|
/** Чекбокс "Можно несколько" */
|
||||||
@ -27,7 +28,7 @@ export interface QuizQuestionImages extends QuizQuestionBase {
|
|||||||
/** Варианты (картинки) */
|
/** Варианты (картинки) */
|
||||||
variants: QuestionVariant[];
|
variants: QuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: PreviewRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
originalBack: string;
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
export interface QuizQuestionNumber extends QuizQuestionBase {
|
export interface QuizQuestionNumber extends QuizQuestionBase {
|
||||||
type: "number";
|
type: "number";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
export interface QuizQuestionPage extends QuizQuestionBase {
|
export interface QuizQuestionPage extends QuizQuestionBase {
|
||||||
type: "page";
|
type: "page";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
innerNameCheck: boolean;
|
innerNameCheck: boolean;
|
||||||
/** Поле "Внутреннее название вопроса" */
|
/** Поле "Внутреннее название вопроса" */
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
export interface QuizQuestionRating extends QuizQuestionBase {
|
export interface QuizQuestionRating extends QuizQuestionBase {
|
||||||
type: "rating";
|
type: "rating";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
export interface QuizQuestionSelect extends QuizQuestionBase {
|
export interface QuizQuestionSelect extends QuizQuestionBase {
|
||||||
type: "select";
|
type: "select";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Можно несколько" */
|
/** Чекбокс "Можно несколько" */
|
||||||
multi: boolean;
|
multi: boolean;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
|
|||||||
@ -12,24 +12,20 @@ import type { QuizQuestionVariant } from "./variant";
|
|||||||
import type { QuizQuestionVarImg } from "./varimg";
|
import type { QuizQuestionVarImg } from "./varimg";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export type Rule = {
|
export interface QuestionBranchingRuleMain {
|
||||||
/* question id */
|
|
||||||
question: string;
|
|
||||||
/* Ответы на вопросы. Для вариантов выбора - конкретные айдишники ответов, для полей ввода текста - текст по полному совпадению, для ввода файла - просто факт того что файл ввели, т.е. boolean */
|
|
||||||
answers: (number | string | boolean)[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PreviewRuleInfo = {
|
|
||||||
/* Id следующего вопроса */
|
|
||||||
next: string;
|
next: string;
|
||||||
/* Радиокнопка "Все условия обязательны" */
|
|
||||||
or: boolean;
|
or: boolean;
|
||||||
rules: Rule[];
|
rules: {
|
||||||
};
|
question: string; //id родителя (пока что)
|
||||||
|
answers: string[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
export interface QuestionBranchingRule {
|
||||||
|
|
||||||
export interface PreviewRule {
|
//список условий
|
||||||
|
main: QuestionBranchingRuleMain[];
|
||||||
|
parentId: string | null | "root";
|
||||||
default: string;
|
default: string;
|
||||||
main: PreviewRuleInfo[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionHint {
|
export interface QuestionHint {
|
||||||
@ -48,7 +44,7 @@ export type QuestionVariant = {
|
|||||||
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
||||||
extendedText: string;
|
extendedText: string;
|
||||||
/** Оригинал изображения (до кропа) */
|
/** Оригинал изображения (до кропа) */
|
||||||
originalImageUrl?: string;
|
originalImageUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface QuizQuestionBase {
|
export interface QuizQuestionBase {
|
||||||
@ -66,8 +62,9 @@ export interface QuizQuestionBase {
|
|||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
deleteTimeoutId: number;
|
deleteTimeoutId: number;
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: PreviewRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
originalBack: string;
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
@ -98,14 +95,20 @@ export type AnyTypedQuizQuestion =
|
|||||||
| QuizQuestionRating;
|
| QuizQuestionRating;
|
||||||
|
|
||||||
type FilterQuestionsWithVariants<T> = T extends {
|
type FilterQuestionsWithVariants<T> = T extends {
|
||||||
content: { variants: QuestionVariant[] };
|
content: { variants: QuestionVariant[]; };
|
||||||
}
|
} ? T : never;
|
||||||
? T
|
|
||||||
: never;
|
|
||||||
|
|
||||||
export type QuizQuestionsWithVariants =
|
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
|
||||||
FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
|
|
||||||
|
|
||||||
|
|
||||||
|
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
|
||||||
|
next: targetId,
|
||||||
|
or: false,
|
||||||
|
rules: [{
|
||||||
|
question: parentId,
|
||||||
|
answers: [] as string[],
|
||||||
|
}]
|
||||||
|
})
|
||||||
export const createQuestionVariant: () => QuestionVariant = () => ({
|
export const createQuestionVariant: () => QuestionVariant = () => ({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
answer: "",
|
answer: "",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type {
|
|||||||
export interface QuizQuestionText extends QuizQuestionBase {
|
export interface QuizQuestionText extends QuizQuestionBase {
|
||||||
type: "text";
|
type: "text";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
innerNameCheck: boolean;
|
innerNameCheck: boolean;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
export interface QuizQuestionVariant extends QuizQuestionBase {
|
export interface QuizQuestionVariant extends QuizQuestionBase {
|
||||||
type: "variant";
|
type: "variant";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Длинный текстовый ответ" */
|
/** Чекбокс "Длинный текстовый ответ" */
|
||||||
largeCheck: boolean;
|
largeCheck: boolean;
|
||||||
/** Чекбокс "Можно несколько" */
|
/** Чекбокс "Можно несколько" */
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||||
type: "varimg";
|
type: "varimg";
|
||||||
content: {
|
content: {
|
||||||
|
id: string;
|
||||||
/** Чекбокс "Вариант "свой ответ"" */
|
/** Чекбокс "Вариант "свой ответ"" */
|
||||||
own: boolean;
|
own: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
@ -18,7 +19,7 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
|
|||||||
required: boolean;
|
required: boolean;
|
||||||
variants: QuestionVariant[];
|
variants: QuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: PreviewRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
originalBack: string;
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const quizSetupSteps = [
|
|||||||
{ stepperText: "Настройка стартовой страницы", sidebarText: "Стартовая страница", sidebarIcon: LayoutIcon },
|
{ stepperText: "Настройка стартовой страницы", sidebarText: "Стартовая страница", sidebarIcon: LayoutIcon },
|
||||||
{ stepperText: "Задайте вопросы", sidebarText: "Вопросы", sidebarIcon: QuestionIcon },
|
{ stepperText: "Задайте вопросы", sidebarText: "Вопросы", sidebarIcon: QuestionIcon },
|
||||||
{ stepperText: "Настройте авторезультаты", sidebarText: "Результаты", sidebarIcon: ChartPieIcon },
|
{ stepperText: "Настройте авторезультаты", sidebarText: "Результаты", sidebarIcon: ChartPieIcon },
|
||||||
{ stepperText: "Оценка графа карты вопросов", sidebarText: "Карта вопросов", sidebarIcon: QuestionsMapIcon },
|
// { stepperText: "Оценка графа карты вопросов", sidebarText: "Карта вопросов", sidebarIcon: QuestionsMapIcon },
|
||||||
{ stepperText: "Настройте форму контактов", sidebarText: "Форма контактов", sidebarIcon: ContactBookIcon },
|
{ stepperText: "Настройте форму контактов", sidebarText: "Форма контактов", sidebarIcon: ContactBookIcon },
|
||||||
{ stepperText: "Установите квиз", sidebarText: "Установка квиза", sidebarIcon: FlowArrowIcon },
|
{ stepperText: "Установите квиз", sidebarText: "Установка квиза", sidebarIcon: FlowArrowIcon },
|
||||||
{ stepperText: "Запустите рекламу", sidebarText: "Запуск рекламы", sidebarIcon: MegaphoneIcon },
|
{ stepperText: "Запустите рекламу", sidebarText: "Запуск рекламы", sidebarIcon: MegaphoneIcon },
|
||||||
@ -29,19 +29,20 @@ export type QuizResultsType = true | null;
|
|||||||
|
|
||||||
export interface QuizConfig {
|
export interface QuizConfig {
|
||||||
type: QuizType;
|
type: QuizType;
|
||||||
logo: string;
|
logo: string | null;
|
||||||
noStartPage: boolean;
|
noStartPage: boolean;
|
||||||
startpageType: QuizStartpageType;
|
startpageType: QuizStartpageType;
|
||||||
results: QuizResultsType;
|
results: QuizResultsType;
|
||||||
|
haveRoot: boolean;
|
||||||
startpage: {
|
startpage: {
|
||||||
description: string;
|
description: string;
|
||||||
button: string;
|
button: string;
|
||||||
position: QuizStartpageAlignType;
|
position: QuizStartpageAlignType;
|
||||||
background: {
|
background: {
|
||||||
type: null | "image" | "video";
|
type: null | "image" | "video";
|
||||||
desktop: string;
|
desktop: string | null;
|
||||||
mobile: string;
|
mobile: string | null;
|
||||||
video: string;
|
video: string | null;
|
||||||
cycle: boolean;
|
cycle: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -57,19 +58,20 @@ export interface QuizConfig {
|
|||||||
|
|
||||||
export const defaultQuizConfig: QuizConfig = {
|
export const defaultQuizConfig: QuizConfig = {
|
||||||
type: null,
|
type: null,
|
||||||
logo: "",
|
logo: null,
|
||||||
noStartPage: false,
|
noStartPage: false,
|
||||||
startpageType: null,
|
startpageType: null,
|
||||||
results: null,
|
results: null,
|
||||||
|
haveRoot: false,
|
||||||
startpage: {
|
startpage: {
|
||||||
description: "",
|
description: "",
|
||||||
button: "",
|
button: "",
|
||||||
position: "left",
|
position: "left",
|
||||||
background: {
|
background: {
|
||||||
type: null,
|
type: null,
|
||||||
desktop: "https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg",
|
desktop: null,
|
||||||
mobile: "https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png",
|
mobile: null,
|
||||||
video: "https://youtu.be/dbaPkCiLPKQ",
|
video: null,
|
||||||
cycle: false,
|
cycle: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export const AnswerItem = ({
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
sx={{
|
sx={{
|
||||||
margin: isTablet ? " 15px 0 20px 0" : "0 0 20px 0",
|
margin: isTablet ? " 15px 0 20px 0" : "0 0 15px 0",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
border: "1px solid rgba(0, 0, 0, 0.23)",
|
border: "1px solid rgba(0, 0, 0, 0.23)",
|
||||||
background: "white",
|
background: "white",
|
||||||
|
|||||||
611
src/pages/Questions/BranchingMap/CsComponent.tsx
Normal file
611
src/pages/Questions/BranchingMap/CsComponent.tsx
Normal file
@ -0,0 +1,611 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Cytoscape from "cytoscape";
|
||||||
|
import CytoscapeComponent from "react-cytoscapejs";
|
||||||
|
import popper from "cytoscape-popper";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { updateRootInfo } from "@root/quizes/actions"
|
||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared"
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { cleardragQuestionContentId, getQuestionById, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId } from "@root/questions/actions";
|
||||||
|
|
||||||
|
import { storeToNodes } from "./helper";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Stylesheet,
|
||||||
|
Core,
|
||||||
|
NodeSingular,
|
||||||
|
AbstractEventObject,
|
||||||
|
ElementDefinition,
|
||||||
|
} from "cytoscape";
|
||||||
|
import { QuestionsList } from "../BranchingPanel/QuestionsList";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
type PopperItem = {
|
||||||
|
id: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Modifier = {
|
||||||
|
name: string;
|
||||||
|
options: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PopperConfig = {
|
||||||
|
popper: {
|
||||||
|
placement: string;
|
||||||
|
modifiers?: Modifier[];
|
||||||
|
};
|
||||||
|
content: (items: PopperItem[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Popper = {
|
||||||
|
update: () => Promise<void>;
|
||||||
|
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeSingularWithPopper = NodeSingular & {
|
||||||
|
popper: (config: PopperConfig) => Popper;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stylesheet: Stylesheet[] = [
|
||||||
|
{
|
||||||
|
selector: "node",
|
||||||
|
style: {
|
||||||
|
shape: "round-rectangle",
|
||||||
|
width: 130,
|
||||||
|
height: 130,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
label: "data(label)",
|
||||||
|
"font-size": "16",
|
||||||
|
color: "#4D4D4D",
|
||||||
|
"text-halign": "center",
|
||||||
|
"text-valign": "center",
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".multiline-auto",
|
||||||
|
style: {
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "edge",
|
||||||
|
style: {
|
||||||
|
width: 30,
|
||||||
|
"line-color": "#DEDFE7",
|
||||||
|
"curve-style": "taxi",
|
||||||
|
"taxi-direction": "horizontal",
|
||||||
|
"taxi-turn": 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ":selected",
|
||||||
|
style: {
|
||||||
|
"border-style": "solid",
|
||||||
|
"border-width": 1.5,
|
||||||
|
"border-color": "#9A9AAF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Cytoscape.use(popper);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modalQuestionParentContentId: string;
|
||||||
|
modalQuestionTargetContentId: string;
|
||||||
|
setOpenedModalQuestions: (open: boolean) => void;
|
||||||
|
setModalQuestionParentContentId: (id: string) => void;
|
||||||
|
setModalQuestionTargetContentId: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const CsComponent = ({
|
||||||
|
modalQuestionParentContentId,
|
||||||
|
modalQuestionTargetContentId,
|
||||||
|
setOpenedModalQuestions,
|
||||||
|
setModalQuestionParentContentId,
|
||||||
|
setModalQuestionTargetContentId
|
||||||
|
}: Props) => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
|
const { dragQuestionContentId, questions } = useQuestionsStore()
|
||||||
|
const [startCreate, setStartCreate] = useState("");
|
||||||
|
const [startRemove, setStartRemove] = useState("");
|
||||||
|
|
||||||
|
const cyRef = useRef<Core | null>(null);
|
||||||
|
const layoutsContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
const plusesContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
const crossesContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
const gearsContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
|
||||||
|
console.log("был выбран вопрос " + modalQuestionTargetContentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [modalQuestionTargetContentId])
|
||||||
|
|
||||||
|
const addNode = ({ parentNodeContentId }: { parentNodeContentId: string }) => {
|
||||||
|
console.log("dragQuestionContentId " + dragQuestionContentId)
|
||||||
|
const cy = cyRef?.current
|
||||||
|
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
|
||||||
|
const targetQuestion = { ...getQuestionByContentId(dragQuestionContentId) } as AnyQuizQuestion
|
||||||
|
|
||||||
|
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
|
||||||
|
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
|
||||||
|
cy?.add([
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
id: targetQuestion.content.id,
|
||||||
|
label: targetQuestion.title || "noname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
source: parentNodeContentId,
|
||||||
|
target: targetQuestion.content.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]).layout(lyopts).run()
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Добавляемый вопрос не найден")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyQuizQuestion, parentNodeChildren: number }) => {
|
||||||
|
console.log("записываю на бек ид родителя")
|
||||||
|
console.log({ parentNodeContentId, targetQuestion, parentNodeChildren })
|
||||||
|
//предупреждаем добавленный вопрос о том, кто его родитель
|
||||||
|
updateQuestion(targetQuestion.content.id, question => {
|
||||||
|
question.content.rule.parentId = parentNodeContentId
|
||||||
|
question.content.rule.main = []
|
||||||
|
})
|
||||||
|
|
||||||
|
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
|
||||||
|
if (parentNodeChildren > 1) {
|
||||||
|
updateOpenedModalSettingsId(parentNodeContentId)
|
||||||
|
} else {
|
||||||
|
//Если ребёнок первый - добавляем его родителю как дефолтный
|
||||||
|
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
|
||||||
|
console.log("remove")
|
||||||
|
const cy = cyRef?.current
|
||||||
|
|
||||||
|
const a = getQuestionByContentId(targetNodeContentId)
|
||||||
|
console.log(a)
|
||||||
|
console.log(a.content)
|
||||||
|
console.log(a.content.rule.parentId)
|
||||||
|
console.log(a.content.rule.parentId === "root")
|
||||||
|
console.log(targetNodeContentId)
|
||||||
|
|
||||||
|
if (a.content.rule.parentId === "root" && quiz) {
|
||||||
|
console.log("click ROOT")
|
||||||
|
updateQuestion(targetNodeContentId, question => {
|
||||||
|
question.content.rule.parentId = ""
|
||||||
|
question.content.rule.main = []
|
||||||
|
question.content.rule.default = ""
|
||||||
|
})
|
||||||
|
updateRootInfo(quiz?.id, false)
|
||||||
|
} else {
|
||||||
|
console.log("click not ROOT")
|
||||||
|
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
|
||||||
|
if (targetNodeContentId && parentQuestionContentId) {
|
||||||
|
|
||||||
|
clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId })
|
||||||
|
cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log({ targetQuestionContentId, parentQuestionContentId })
|
||||||
|
|
||||||
|
updateQuestion(targetQuestionContentId, question => {
|
||||||
|
question.content.rule.parentId = ""
|
||||||
|
question.content.rule.main = []
|
||||||
|
question.content.rule.default = ""
|
||||||
|
})
|
||||||
|
updateQuestion(parentQuestionContentId, question => {
|
||||||
|
if (question.content.rule.parentId === parentQuestionContentId) question.content.rule.parentId = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startCreate) {
|
||||||
|
addNode({ parentNodeContentId: startCreate });
|
||||||
|
cleardragQuestionContentId()
|
||||||
|
setStartCreate("");
|
||||||
|
}
|
||||||
|
}, [startCreate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startRemove) {
|
||||||
|
removeNode({ targetNodeContentId: startRemove });
|
||||||
|
setStartRemove("");
|
||||||
|
}
|
||||||
|
}, [startRemove]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const readyLO = (e) => {
|
||||||
|
//удаляем иконки
|
||||||
|
e.cy.nodes().forEach((ele: any) => {
|
||||||
|
const data = ele.data()
|
||||||
|
data.id && removeButtons(data.id);
|
||||||
|
})
|
||||||
|
initialPopperIcons(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lyopts = {
|
||||||
|
name: 'preset',
|
||||||
|
|
||||||
|
positions: (e) => {
|
||||||
|
console.log('POSITIIIIIIIONS')
|
||||||
|
const id = e.id()
|
||||||
|
const incomming = e.cy().edges(`[target="${id}"]`)
|
||||||
|
const layer = 0
|
||||||
|
e.removeData('lastChild')
|
||||||
|
|
||||||
|
if (incomming.length === 0) {
|
||||||
|
const children = e.cy().edges(`[source="${id}"]`)
|
||||||
|
e.data('layer', layer)
|
||||||
|
e.data('children', children.targets().length)
|
||||||
|
const queue = []
|
||||||
|
children.forEach(n => {
|
||||||
|
queue.push({ task: n.target(), layer: layer + 1 })
|
||||||
|
})
|
||||||
|
while (queue.length) {
|
||||||
|
const task = queue.pop()
|
||||||
|
task.task.data('layer', task.layer)
|
||||||
|
const children = e.cy().edges(`[source="${task.task.id()}"]`)
|
||||||
|
task.task.data('children', children.targets().length)
|
||||||
|
if (children.length !== 0) {
|
||||||
|
children.forEach(n => queue.push({ task: n.target(), layer: task.layer + 1 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queue.push({ parent: e, children: children.targets() })
|
||||||
|
while (queue.length) {
|
||||||
|
const task = queue.pop()
|
||||||
|
if (task.children.length === 0) {
|
||||||
|
task.parent.data('subtreeWidth', 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const unprocessed = task?.children.filter(e => {
|
||||||
|
return (e.data('subtreeWidth') === undefined)
|
||||||
|
})
|
||||||
|
if (unprocessed.length !== 0) {
|
||||||
|
queue.push(task)
|
||||||
|
unprocessed.forEach(t => {
|
||||||
|
queue.push({ parent: t, children: t.cy().edges(`[source="${t.id()}"]`).targets() })
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
task?.parent.data('subtreeWidth', task.children.reduce((p, n) => p + n.data('subtreeWidth'), 0))
|
||||||
|
}
|
||||||
|
return { x: 200 * e.data('layer'), y: 0 }
|
||||||
|
} else {
|
||||||
|
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
|
||||||
|
const wing = parent.data('subtreeWidth') / 2
|
||||||
|
const lastOffset = parent.data('lastChild')
|
||||||
|
const step = wing * 2 / (parent.data('children') - 1)
|
||||||
|
//e.removeData('subtreeWidth')
|
||||||
|
if (lastOffset !== undefined) {
|
||||||
|
parent.data('lastChild', lastOffset + step)
|
||||||
|
return { x: 200 * e.data('layer'), y: (lastOffset + step) }
|
||||||
|
} else {
|
||||||
|
parent.data('lastChild', parent.position().y - wing)
|
||||||
|
return { x: 200 * e.data('layer'), y: (parent.position().y - wing) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, // map of (node id) => (position obj); or function(node){ return somPos; }
|
||||||
|
zoom: undefined, // the zoom level to set (prob want fit = false if set)
|
||||||
|
pan: true, // the pan level to set (prob want fit = false if set)
|
||||||
|
fit: false, // whether to fit to viewport
|
||||||
|
padding: 30, // padding on fit
|
||||||
|
animate: false, // whether to transition the node positions
|
||||||
|
animationDuration: 500, // duration of animation in ms if enabled
|
||||||
|
animationEasing: undefined, // easing of animation if enabled
|
||||||
|
animateFilter: function (node, i) { return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||||
|
ready: readyLO, // callback on layoutready
|
||||||
|
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
|
||||||
|
const cy = cyRef.current;
|
||||||
|
const eles = cy?.add(storeToNodes(questions))
|
||||||
|
console.log('PETTY', storeToNodes(questions), eles.length)
|
||||||
|
const elecs = eles.layout(lyopts).run()
|
||||||
|
|
||||||
|
cy?.fit()
|
||||||
|
//cy?.layout().run()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId);
|
||||||
|
layoutsContainer.current?.remove();
|
||||||
|
plusesContainer.current?.remove();
|
||||||
|
crossesContainer.current?.remove();
|
||||||
|
gearsContainer.current?.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const removeButtons = (id: string) => {
|
||||||
|
layoutsContainer.current
|
||||||
|
?.querySelector(`.popper-layout[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
plusesContainer.current
|
||||||
|
?.querySelector(`.popper-plus[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
crossesContainer.current
|
||||||
|
?.querySelector(`.popper-cross[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
gearsContainer.current
|
||||||
|
?.querySelector(`.popper-gear[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const initialPopperIcons = (e) => {
|
||||||
|
const cy = e.cy
|
||||||
|
|
||||||
|
const container =
|
||||||
|
(document.body.querySelector(
|
||||||
|
".__________cytoscape_container"
|
||||||
|
) as HTMLDivElement) || null;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.overflow = "hidden";
|
||||||
|
|
||||||
|
if (!plusesContainer.current) {
|
||||||
|
plusesContainer.current = document.createElement("div");
|
||||||
|
plusesContainer.current.setAttribute("id", "popper-pluses");
|
||||||
|
container.append(plusesContainer.current);
|
||||||
|
}
|
||||||
|
if (!crossesContainer.current) {
|
||||||
|
crossesContainer.current = document.createElement("div");
|
||||||
|
crossesContainer.current.setAttribute("id", "popper-crosses");
|
||||||
|
container.append(crossesContainer.current);
|
||||||
|
}
|
||||||
|
if (!gearsContainer.current) {
|
||||||
|
gearsContainer.current = document.createElement("div");
|
||||||
|
gearsContainer.current.setAttribute("id", "popper-gears");
|
||||||
|
container.append(gearsContainer.current);
|
||||||
|
}
|
||||||
|
if (!layoutsContainer.current) {
|
||||||
|
layoutsContainer.current = document.createElement("div");
|
||||||
|
layoutsContainer.current.setAttribute("id", "popper-layouts");
|
||||||
|
container.append(layoutsContainer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = cy.extent()
|
||||||
|
const nodesInView = cy.nodes().filter(n => {
|
||||||
|
const bb = n.boundingBox()
|
||||||
|
return bb.x1 > ext.x1 && bb.x2 < ext.x2 && bb.y1 > ext.y1 && bb.y2 < ext.y2
|
||||||
|
})
|
||||||
|
|
||||||
|
nodesInView
|
||||||
|
.toArray()
|
||||||
|
?.forEach((item) => {
|
||||||
|
const node = item as NodeSingularWithPopper;
|
||||||
|
|
||||||
|
const layoutsPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "left",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = layoutsContainer.current?.querySelector(
|
||||||
|
`.popper-layout[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutElement = document.createElement("div");
|
||||||
|
layoutElement.style.zIndex = "0"
|
||||||
|
layoutElement.classList.add("popper-layout");
|
||||||
|
layoutElement.setAttribute("data-id", item.id());
|
||||||
|
layoutElement.addEventListener("mouseup", () => {
|
||||||
|
alert("layout")
|
||||||
|
}
|
||||||
|
// setStartCreate(node.id())
|
||||||
|
);
|
||||||
|
layoutsContainer.current?.appendChild(layoutElement);
|
||||||
|
|
||||||
|
return layoutElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const plusesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "right",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = plusesContainer.current?.querySelector(
|
||||||
|
`.popper-plus[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plusElement = document.createElement("div");
|
||||||
|
plusElement.classList.add("popper-plus");
|
||||||
|
plusElement.setAttribute("data-id", item.id());
|
||||||
|
plusElement.style.zIndex = "1"
|
||||||
|
plusElement.addEventListener("mouseup", () => {
|
||||||
|
setStartCreate(node.id());
|
||||||
|
});
|
||||||
|
plusesContainer.current?.appendChild(plusElement);
|
||||||
|
|
||||||
|
return plusElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const crossesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "top-end",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = crossesContainer.current?.querySelector(
|
||||||
|
`.popper-cross[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossElement = document.createElement("div");
|
||||||
|
crossElement.classList.add("popper-cross");
|
||||||
|
crossElement.setAttribute("data-id", item.id());
|
||||||
|
crossElement.style.zIndex = "2"
|
||||||
|
crossesContainer.current?.appendChild(crossElement);
|
||||||
|
crossElement.addEventListener("mouseup", () => {
|
||||||
|
setStartRemove(node.id())
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return crossElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gearsPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "left",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
if (item.data().lastChild === NaN || item.data().lastChild === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemElement = gearsContainer.current?.querySelector(
|
||||||
|
`.popper-gear[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gearElement = document.createElement("div");
|
||||||
|
gearElement.classList.add("popper-gear");
|
||||||
|
gearElement.setAttribute("data-id", item.id());
|
||||||
|
gearElement.style.zIndex = "1"
|
||||||
|
gearsContainer.current?.appendChild(gearElement);
|
||||||
|
gearElement.addEventListener("mouseup", (e) => {
|
||||||
|
console.log("шестерня")
|
||||||
|
// setOpenedModalSettings(
|
||||||
|
// findQuestionById(quizId, node.id().split(" ").pop() || "").index
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
|
||||||
|
return gearElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
await plusesPopper.update();
|
||||||
|
await crossesPopper.update();
|
||||||
|
await gearsPopper.update();
|
||||||
|
await layoutsPopper.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onZoom = (event: AbstractEventObject) => {
|
||||||
|
const zoom = event.cy.zoom();
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
crossesPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutsPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [0, -130 * zoom] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
layoutsContainer.current
|
||||||
|
?.querySelectorAll("#popper-layouts > .popper-layout")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${130 * zoom}px`;
|
||||||
|
element.style.height = `${130 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
plusesContainer.current
|
||||||
|
?.querySelectorAll("#popper-pluses > .popper-plus")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${40 * zoom}px`;
|
||||||
|
element.style.height = `${40 * zoom}px`;
|
||||||
|
element.style.fontSize = `${40 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
crossesContainer.current
|
||||||
|
?.querySelectorAll("#popper-crosses > .popper-cross")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${24 * zoom}px`;
|
||||||
|
element.style.height = `${24 * zoom}px`;
|
||||||
|
element.style.fontSize = `${24 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
gearsContainer.current
|
||||||
|
?.querySelectorAll("#popper-gears > .popper-gear")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${60 * zoom}px`;
|
||||||
|
element.style.height = `${40 * zoom}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
node?.on("position", update);
|
||||||
|
cy?.on("pan zoom resize render", onZoom);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CytoscapeComponent
|
||||||
|
wheelSensitivity={0.1}
|
||||||
|
elements={[]}
|
||||||
|
// elements={createGraphElements(tree, quiz)}
|
||||||
|
style={{ height: "480px", background: "#F2F3F7" }}
|
||||||
|
stylesheet={stylesheet}
|
||||||
|
layout={(lyopts)}
|
||||||
|
cy={(cy) => {
|
||||||
|
cyRef.current = cy;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
src/pages/Questions/BranchingMap/FirstNodeField.tsx
Normal file
73
src/pages/Questions/BranchingMap/FirstNodeField.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Box } from "@mui/material"
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
|
||||||
|
import { updateRootInfo } from "@root/quizes/actions"
|
||||||
|
import { useQuestionsStore } from "@root/questions/store"
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
setOpenedModalQuestions: (open: boolean) => void;
|
||||||
|
modalQuestionTargetContentId: string;
|
||||||
|
}
|
||||||
|
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
|
||||||
|
const { dragQuestionContentId } = useQuestionsStore()
|
||||||
|
const Container = useRef<HTMLDivElement | null>(null);
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
console.log(dragQuestionContentId)
|
||||||
|
|
||||||
|
const modalOpen = () => setOpenedModalQuestions(true)
|
||||||
|
|
||||||
|
const newRootNode = () => {
|
||||||
|
if (quiz) {
|
||||||
|
console.log(dragQuestionContentId)
|
||||||
|
|
||||||
|
if (dragQuestionContentId) {
|
||||||
|
updateRootInfo(quiz?.id, true)
|
||||||
|
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Нет информации о взятом опроснике")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Container.current?.addEventListener("mouseup", newRootNode)
|
||||||
|
Container.current?.addEventListener("click", modalOpen)
|
||||||
|
return () => {
|
||||||
|
Container.current?.removeEventListener("mouseup", newRootNode)
|
||||||
|
Container.current?.removeEventListener("click", modalOpen)
|
||||||
|
}
|
||||||
|
}, [dragQuestionContentId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (quiz) {
|
||||||
|
|
||||||
|
if (modalQuestionTargetContentId) {
|
||||||
|
updateRootInfo(quiz?.id, true)
|
||||||
|
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Нет информации о взятом опроснике")
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [modalQuestionTargetContentId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={Container}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#f2f3f7",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#4d4d4d",
|
||||||
|
fontSize: "50px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/pages/Questions/BranchingMap/helper.ts
Normal file
43
src/pages/Questions/BranchingMap/helper.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared"
|
||||||
|
|
||||||
|
interface Nodes {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
parent?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Edges {
|
||||||
|
data: {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storeToNodes = (questions: AnyQuizQuestion[]) => {
|
||||||
|
console.log(questions)
|
||||||
|
const nodes: Nodes[] = []
|
||||||
|
const edges: Edges[] = []
|
||||||
|
questions.forEach((question) => {
|
||||||
|
console.log(question)
|
||||||
|
if (question.content.rule.parentId) {
|
||||||
|
nodes.push({data: {
|
||||||
|
id: question.content.id,
|
||||||
|
label: question.title ? question.title : "noname"
|
||||||
|
}})
|
||||||
|
// nodes.push({
|
||||||
|
// data: {
|
||||||
|
// id: "delete" + question.content.id,
|
||||||
|
// label: "X",
|
||||||
|
// parent: question.content.id,
|
||||||
|
// }
|
||||||
|
// },)
|
||||||
|
if (question.content.rule.parentId !== "root") edges.push({data: {
|
||||||
|
source: question.content.rule.parentId,
|
||||||
|
target: question.content.id
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log([...nodes, ...edges])
|
||||||
|
return [...nodes, ...edges];
|
||||||
|
}
|
||||||
41
src/pages/Questions/BranchingMap/index.tsx
Normal file
41
src/pages/Questions/BranchingMap/index.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { FirstNodeField } from "./FirstNodeField";
|
||||||
|
import { CsComponent } from "./CsComponent";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store"
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {BranchingQuestionsModal} from "../BranchingQuestionsModal"
|
||||||
|
|
||||||
|
|
||||||
|
export const BranchingMap = () => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const { dragQuestionContentId } = useQuestionsStore()
|
||||||
|
const [modalQuestionParentContentId, setModalQuestionParentContentId] = useState<string>("")
|
||||||
|
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
|
||||||
|
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
id="cytoscape-container"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: "20px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||||
|
marginBottom: "40px",
|
||||||
|
height: "521px",
|
||||||
|
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
quiz?.config.haveRoot ?
|
||||||
|
<CsComponent modalQuestionParentContentId={modalQuestionParentContentId} modalQuestionTargetContentId={modalQuestionTargetContentId} setOpenedModalQuestions={setOpenedModalQuestions} setModalQuestionParentContentId={setModalQuestionParentContentId} setModalQuestionTargetContentId={setModalQuestionTargetContentId}/>
|
||||||
|
:
|
||||||
|
<FirstNodeField setOpenedModalQuestions={setOpenedModalQuestions} modalQuestionTargetContentId={modalQuestionTargetContentId}/>
|
||||||
|
}
|
||||||
|
<BranchingQuestionsModal openedModalQuestions={openedModalQuestions} setOpenedModalQuestions={setOpenedModalQuestions} setModalQuestionTargetContentId={setModalQuestionTargetContentId} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
src/pages/Questions/BranchingMap/styles.css
Normal file
45
src/pages/Questions/BranchingMap/styles.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#popper-pluses > .popper-plus {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eeeff4;
|
||||||
|
border: 1.5px dashed rgba(154, 154, 175, 0.5);
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-pluses > .popper-plus::before {
|
||||||
|
content: "+";
|
||||||
|
color: rgba(154, 154, 175, 0.5);
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-crosses > .popper-cross {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(154, 154, 175, 0.7);
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-crosses > .popper-cross::before {
|
||||||
|
content: "+";
|
||||||
|
transform: rotate(45deg);
|
||||||
|
color: #fff;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-gears > .popper-gear {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-image: url("../../../assets/icons/ArrowGear.svg");
|
||||||
|
font-size: 0px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
178
src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx
Normal file
178
src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
Checkbox,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { AnyQuizQuestion, createBranchingRuleMain } from "../../../model/questionTypes/shared"
|
||||||
|
import { Select } from "../Select";
|
||||||
|
|
||||||
|
import RadioCheck from "@ui_kit/RadioCheck";
|
||||||
|
import RadioIcon from "@ui_kit/RadioIcon";
|
||||||
|
import InfoIcon from "@icons/Info";
|
||||||
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
|
||||||
|
import { TypeSwitch, BlockRule } from "./Settings";
|
||||||
|
import { getQuestionById, updateOpenedModalSettingsId, updateQuestion } from "@root/questions/actions";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
|
||||||
|
export default function BranchingQuestions() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
|
||||||
|
const { openedModalSettingsId } = useQuestionsStore();
|
||||||
|
const [targetQuestion, setTargetQuestion] = useState<AnyQuizQuestion | null>(getQuestionById(openedModalSettingsId))
|
||||||
|
const [parentQuestion, setParentQuestion] = useState<AnyQuizQuestion | null>(getQuestionById(openedModalSettingsId))
|
||||||
|
|
||||||
|
if (targetQuestion === null || parentQuestion === null) {
|
||||||
|
enqueueSnackbar("Невозможно найти данные ветвления для этого вопроса")
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveData = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
updateOpenedModalSettingsId()
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal open={openedModalSettingsId !== null} 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: "#4d4d4d" }}>
|
||||||
|
<Typography component="span">{targetQuestion.title}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Tooltip
|
||||||
|
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<InfoIcon />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "400px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
parentQuestion.content.rule.main.length ?
|
||||||
|
parentQuestion.content.rule.main.map((e: any, i: number) => {
|
||||||
|
if (e.next === targetQuestion.id) {
|
||||||
|
return <TypeSwitch key={i} targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={i} />
|
||||||
|
} else {
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
:
|
||||||
|
<TypeSwitch targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={0} />
|
||||||
|
}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
margin: "20px 0 0 20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
marginBottom: "10px",
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const mutate = parentQuestion
|
||||||
|
mutate.content.rule.main.push(createBranchingRuleMain(targetQuestion.id, parentQuestion.id))
|
||||||
|
setParentQuestion(mutate)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить условие
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
<FormControlLabel control={<Checkbox
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
checked={parentQuestion.content.rule.default === targetQuestion.id}
|
||||||
|
|
||||||
|
onClick={() => {
|
||||||
|
const mutate = parentQuestion
|
||||||
|
mutate.content.rule.default = targetQuestion.id
|
||||||
|
setParentQuestion(mutate)
|
||||||
|
}}
|
||||||
|
/>} label="Следующий вопрос по-умолчанию" />
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px", margin: "20px" }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Готово
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
516
src/pages/Questions/BranchingModal/Settings.tsx
Normal file
516
src/pages/Questions/BranchingModal/Settings.tsx
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
import { Box, MenuItem, FormControl, Checkbox, FormControlLabel, Radio, RadioGroup, Typography, useTheme, Select, Chip, IconButton, TextField } from "@mui/material"
|
||||||
|
import RadioCheck from "@ui_kit/RadioCheck"
|
||||||
|
import RadioIcon from "@ui_kit/RadioIcon"
|
||||||
|
import { QuizQuestionBase } from "model/questionTypes/shared"
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { updateQuestion, getQuestionById } from "@root/questions/actions";
|
||||||
|
import { AnyQuizQuestion } from "../../../model/questionTypes/shared"
|
||||||
|
import { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
|
||||||
|
import InfoIcon from "@icons/Info";
|
||||||
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
|
||||||
|
const CONDITIONS = [
|
||||||
|
"Все условия обязательны",
|
||||||
|
"Обязательно хотя бы одно условие",
|
||||||
|
];
|
||||||
|
interface Props {
|
||||||
|
parentQuestion: AnyQuizQuestion;
|
||||||
|
targetQuestion: AnyQuizQuestion;
|
||||||
|
ruleIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Этот компонент вызывается 1 раз на каждое условие родителя для перехода к этому вопросу. Поэтому для изменения стора мы знаем индекс
|
||||||
|
export const TypeSwitch = ({ parentQuestion, targetQuestion, ruleIndex }: Props) => {
|
||||||
|
|
||||||
|
switch (parentQuestion.type) {
|
||||||
|
// case 'nonselected':
|
||||||
|
// return <BlockRule text={"Не выбран тип родительского вопроса"} />
|
||||||
|
// break;
|
||||||
|
|
||||||
|
case "variant":
|
||||||
|
case "images":
|
||||||
|
case "varimg":
|
||||||
|
case "emoji":
|
||||||
|
case "select":
|
||||||
|
|
||||||
|
return (parentQuestion.content.variants === undefined ? <BlockRule text={"У родителя нет вариантов"} /> :
|
||||||
|
<SelectorType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
// return <DateInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
return <></>
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return <NumberInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "page":
|
||||||
|
case "date":
|
||||||
|
return <BlockRule text={"У такого родителя может быть только один потомок"} />
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "text":
|
||||||
|
return <TextInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
return <FileInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "rating":
|
||||||
|
return <RatingInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <BlockRule text={"Не распознан тип родительского вопроса"} />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockRule = ({ text }: { text: string }) => {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
margin: "100px 0",
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>{text}</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const SelectorType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
||||||
|
(Укажите один или несколько вариантов)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers}
|
||||||
|
onChange={(event: SelectChangeEvent) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = (event.target as HTMLSelectElement).value
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: `1px solid ${theme.palette.brightPurple.main} !important`,
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parentQuestion.content.variants.map((e: any) => {
|
||||||
|
return <MenuItem value={e.id}>
|
||||||
|
{e.answer}
|
||||||
|
</MenuItem>
|
||||||
|
})}
|
||||||
|
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].or}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].or = value
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CONDITIONS.map((condition, totalIndex) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={totalIndex}
|
||||||
|
sx={{ color: theme.palette.grey2.main }}
|
||||||
|
value={Boolean(Number(totalIndex))}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
checkedIcon={<RadioCheck />}
|
||||||
|
icon={<RadioIcon />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={condition}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
||||||
|
(Укажите один или несколько вариантов)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, "")]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const TextInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px", fontSize: "12px" }}>
|
||||||
|
(Укажите текст, при совпадении с которым пользователь попадёт на этот вопрос)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const FileInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Перевести на этот вопрос если пользователь загрузил файл
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<FormControlLabel control={<Checkbox
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
checked={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).checked]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>} label="да" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const RatingInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px", fontSize: "12px" }}>
|
||||||
|
(Ожидаемое количество ячеек)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, "")]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// content: {
|
||||||
|
// rule: {
|
||||||
|
// main: [
|
||||||
|
// {
|
||||||
|
// next: "id of next question", // 2 string
|
||||||
|
// or: true // 3 boolean
|
||||||
|
// rules: [
|
||||||
|
// {
|
||||||
|
// question: "question id", string
|
||||||
|
// answers: [] // ответы на вопросы. для вариантов выбора - конкретные айдишники, для полей ввода текста - текст по полному совпадению, для ввода файла ии ещё какой подобной дичи - просто факт того что файл ввели, т.е. boolean
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// default: ID string
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
89
src/pages/Questions/BranchingPanel/QuestionsList.tsx
Normal file
89
src/pages/Questions/BranchingPanel/QuestionsList.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
|
import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { updateDragQuestionContentId } from "@root/questions/actions";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
|
const getItemStyle = (isDragging:any, draggableStyle:any) => ({
|
||||||
|
// some basic styles to make the items look a bit nicer
|
||||||
|
userSelect: "none",
|
||||||
|
padding: 5 * 2,
|
||||||
|
margin: `0 0 5px 0`,
|
||||||
|
|
||||||
|
// change background colour if dragging
|
||||||
|
background: isDragging ? "lightgreen" : "grey",
|
||||||
|
|
||||||
|
// styles we need to apply on draggables
|
||||||
|
...draggableStyle
|
||||||
|
});
|
||||||
|
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
|
||||||
|
|
||||||
|
export const QuestionsList = () => {
|
||||||
|
const { questions } = useQuestionsStore()
|
||||||
|
console.log(questions)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ padding: "15px" }}>
|
||||||
|
<Typography
|
||||||
|
sx={{ fontSize: "12px", color: "#9A9AAF", marginBottom: "5px" }}
|
||||||
|
>
|
||||||
|
Перетащите вопросы в карту ветвления
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
paddingRight: "12px",
|
||||||
|
"&::-webkit-scrollbar": { width: "8px" },
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
background: "#D4D4DF",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
background: "#9A9AAF",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1.5px solid transparent",
|
||||||
|
backgroundClip: "padding-box",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/}
|
||||||
|
{questions.filter((q:AnyQuestion) => q.type).map(({ title, id, content }, index) => (
|
||||||
|
<Button
|
||||||
|
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
|
||||||
|
if (!content.rule.parentId) updateDragQuestionContentId(content.id)
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||||
|
border-radius: 8px;`,
|
||||||
|
"&:last-child": { marginBottom: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: "100%", color: content.rule.parentId ? "#9A9AAF" : "#000" }}>
|
||||||
|
{title || "нет заголовка"}
|
||||||
|
</Typography>
|
||||||
|
{content.rule.parentId && <CheckedIcon />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
85
src/pages/Questions/BranchingPanel/index.tsx
Normal file
85
src/pages/Questions/BranchingPanel/index.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Box, Typography, Switch, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
import { QuestionsList } from "./QuestionsList";
|
||||||
|
|
||||||
|
type BranchingPanelProps = {
|
||||||
|
active: boolean;
|
||||||
|
setActive: (active: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BranchingPanel = ({ active, setActive }: BranchingPanelProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
padding: "18px",
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={active}
|
||||||
|
onChange={(_, value) => setTimeout(() => setActive(value), 10)}
|
||||||
|
sx={{
|
||||||
|
width: 50,
|
||||||
|
height: 30,
|
||||||
|
padding: 0,
|
||||||
|
"& .MuiSwitch-switchBase": {
|
||||||
|
padding: 0,
|
||||||
|
margin: "2px",
|
||||||
|
transitionDuration: "300ms",
|
||||||
|
"&.Mui-checked": {
|
||||||
|
transform: "translateX(20px)",
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
"& + .MuiSwitch-track": {
|
||||||
|
backgroundColor: "#E8DCF9",
|
||||||
|
opacity: 1,
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
|
||||||
|
},
|
||||||
|
"&.Mui-disabled .MuiSwitch-thumb": {
|
||||||
|
color:
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
"&.Mui-disabled + .MuiSwitch-track": {
|
||||||
|
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiSwitch-thumb": {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
},
|
||||||
|
"& .MuiSwitch-track": {
|
||||||
|
borderRadius: 13,
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
|
||||||
|
opacity: 1,
|
||||||
|
transition: theme.transitions.create(["background-color"], {
|
||||||
|
duration: 500,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
|
||||||
|
Логика ветвления
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
|
||||||
|
Настройте связи между вопросами
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{ active && <QuestionsList /> }
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
77
src/pages/Questions/BranchingQuestionsModal/index.tsx
Normal file
77
src/pages/Questions/BranchingQuestionsModal/index.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Box, Modal, Button, Typography } from "@mui/material";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
|
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
openedModalQuestions: boolean;
|
||||||
|
setModalQuestionTargetContentId: (contentId:string) => void;
|
||||||
|
setOpenedModalQuestions: (open:boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BranchingQuestionsModal = ({ openedModalQuestions, setOpenedModalQuestions, setModalQuestionTargetContentId}:Props) => {
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
const { questions } = useQuestionsStore();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpenedModalQuestions(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={openedModalQuestions} 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,
|
||||||
|
padding: "30px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ margin: "0 auto", maxWidth: "350px" }}>
|
||||||
|
{questions.filter((q:AnyQuestion) => (q.type && !q.content.rule.parentId)).map((question: AnyTypedQuizQuestion, index:number) => (
|
||||||
|
<Button
|
||||||
|
key={question.content.id}
|
||||||
|
onClick={() => {
|
||||||
|
setModalQuestionTargetContentId(question.content.id)
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||||
|
border-radius: 8px;`,
|
||||||
|
"&:last-child": { marginBottom: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
color: "#000",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{question.title || "нет заголовка"}
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -247,6 +247,7 @@ export default function ButtonsOptions({
|
|||||||
sx={{
|
sx={{
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
gap: "6px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
|
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionDate } from "@model/questionTypes/date";
|
import { QuizQuestionDate } from "@model/questionTypes/date";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingData from "./settingData";
|
import SettingData from "./settingData";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchData({
|
|||||||
return <SettingData question={question} />;
|
return <SettingData question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,6 +66,7 @@ function DraggableListItem({ question, isDragging, index }: Props) {
|
|||||||
question={question}
|
question={question}
|
||||||
draggableProps={provided.dragHandleProps}
|
draggableProps={provided.dragHandleProps}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
|
index={index}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -45,9 +45,10 @@ interface Props {
|
|||||||
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
|
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
|
||||||
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
|
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuestionsPageCard({ question, draggableProps, isDragging }: Props) {
|
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: 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 theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -96,7 +97,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
<TextField
|
<TextField
|
||||||
defaultValue={question.title}
|
defaultValue={question.title}
|
||||||
placeholder={"Заголовок вопроса"}
|
placeholder={"Заголовок вопроса"}
|
||||||
onChange={({ target }) => setTitle(target.value)}
|
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<Box>
|
<Box>
|
||||||
@ -262,17 +263,26 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
<Box
|
||||||
<OneIcon
|
|
||||||
style={{
|
style={{
|
||||||
fontSize: "30px",
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
height: "30px",
|
||||||
|
width: "30px",
|
||||||
marginLeft: "3px",
|
marginLeft: "3px",
|
||||||
color: !question.expanded ? "#FFF" : "",
|
borderRadius: "50%",
|
||||||
fill: question.expanded
|
fontSize: "16px",
|
||||||
|
color: question.expanded
|
||||||
|
? theme.palette.brightPurple.main
|
||||||
|
: "#FFF",
|
||||||
|
background: question.expanded
|
||||||
? "#EEE4FC"
|
? "#EEE4FC"
|
||||||
: theme.palette.brightPurple.main,
|
: theme.palette.brightPurple.main,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
disableRipple
|
disableRipple
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionSelect } from "@model/questionTypes/select";
|
import { QuizQuestionSelect } from "@model/questionTypes/select";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingDropDown from "./settingDropDown";
|
import SettingDropDown from "./settingDropDown";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchDropDown({
|
|||||||
return <SettingDropDown question={question} />;
|
return <SettingDropDown question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,6 @@ export default function Emoji({ question }: Props) {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
background: "#EEE4FC",
|
background: "#EEE4FC",
|
||||||
borderRadius: "3px",
|
borderRadius: "3px",
|
||||||
marginRight: "15px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
|
import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingEmoji from "./settingEmoji";
|
import SettingEmoji from "./settingEmoji";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchEmoji({
|
|||||||
return <SettingEmoji question={question} />;
|
return <SettingEmoji question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export default function FormQuestionsPage() {
|
|||||||
maxWidth: "796px",
|
maxWidth: "796px",
|
||||||
boxShadow: "0px 10px 30px #e7e7e7",
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
marginBottom: "40px",
|
marginBottom: "30px",
|
||||||
borderTop: "1px solid transparent",
|
borderTop: "1px solid transparent",
|
||||||
borderBottom: "1px solid transparent",
|
borderBottom: "1px solid transparent",
|
||||||
background: "#FFFFFF",
|
background: "#FFFFFF",
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export default function FormTypeQuestions({ question }: Props) {
|
|||||||
margin: "20px",
|
margin: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(true /* TODO какое-то непонятное условие */
|
{(true /* TODO только первый вопрос */
|
||||||
? BUTTON_TYPE_QUESTIONS
|
? BUTTON_TYPE_QUESTIONS
|
||||||
: BUTTON_TYPE_SHORT_QUESTIONS
|
: BUTTON_TYPE_SHORT_QUESTIONS
|
||||||
).map(({ icon, title, value: questionType }) => (
|
).map(({ icon, title, value: questionType }) => (
|
||||||
|
|||||||
@ -14,12 +14,13 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { openCropModal } from "@root/cropModal";
|
import { setCropModal } from "@root/cropModal";
|
||||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
|
||||||
import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useDisclosure } from "../../../utils/useDisclosure";
|
||||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||||
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
|
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
@ -38,27 +39,44 @@ export default function OptionsAndPicture({ question }: Props) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
|
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
|
||||||
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageUpload = (files: FileList | null) => {
|
const handleImageUpload = async (files: FileList | null) => {
|
||||||
if (!files?.length || !selectedVariantId) return;
|
if (!files?.length || !selectedVariantId) return;
|
||||||
|
|
||||||
const [file] = Array.from(files);
|
const [file] = Array.from(files);
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
||||||
setVariantOriginalImageUrl(question.id, selectedVariantId, url);
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
variant.originalImageUrl = url;
|
||||||
|
});
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(url, url);
|
setCropModal(file, url);
|
||||||
|
openCropModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||||
if (!selectedVariantId) return;
|
if (!selectedVariantId) return;
|
||||||
|
|
||||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -72,14 +90,17 @@ export default function OptionsAndPicture({ question }: Props) {
|
|||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
setSelectedVariantId(variant.id);
|
setSelectedVariantId(variant.id);
|
||||||
if (variant.extendedText) return openCropModal(
|
if (variant.extendedText) {
|
||||||
|
openCropModal();
|
||||||
|
setCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl || ""
|
variant.originalImageUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openImageUploadModal();
|
openImageUploadModal();
|
||||||
}}
|
}}
|
||||||
onPlusClick={() => {
|
onPlusClick={() => {
|
||||||
@ -97,14 +118,17 @@ export default function OptionsAndPicture({ question }: Props) {
|
|||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
setSelectedVariantId(variant.id);
|
setSelectedVariantId(variant.id);
|
||||||
if (variant.extendedText) return openCropModal(
|
if (variant.extendedText) {
|
||||||
|
openCropModal();
|
||||||
|
setCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl || ""
|
variant.originalImageUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openImageUploadModal();
|
openImageUploadModal();
|
||||||
}}
|
}}
|
||||||
onPlusClick={() => {
|
onPlusClick={() => {
|
||||||
@ -117,8 +141,8 @@ export default function OptionsAndPicture({ question }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<UploadImageModal imgHC={handleImageUpload} />
|
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} />
|
||||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} />
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -312,7 +336,7 @@ export default function OptionsAndPicture({ question }: Props) {
|
|||||||
height: "19px",
|
height: "19px",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addQuestionVariant(question.id)
|
addQuestionVariant(question.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
Добавьте ответ
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
||||||
import UploadImage from "../UploadImage";
|
import UploadImage from "../UploadImage";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingOptionsAndPict from "./SettingOptionsAndPict";
|
import SettingOptionsAndPict from "./SettingOptionsAndPict";
|
||||||
|
|
||||||
@ -19,8 +18,6 @@ export default function SwitchOptionsAndPict({
|
|||||||
return <SettingOptionsAndPict question={question} />;
|
return <SettingOptionsAndPict question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
case "image":
|
case "image":
|
||||||
return <UploadImage question={question} />;
|
return <UploadImage question={question} />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -5,9 +5,8 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme
|
useTheme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { openCropModal } from "@root/cropModal";
|
import { setCropModal } from "@root/cropModal";
|
||||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
import { addQuestionVariant, uploadQuestionImage } from "@root/questions/actions";
|
||||||
import { addQuestionVariant, setVariantImageUrl, setVariantOriginalImageUrl } from "@root/questions/actions";
|
|
||||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -17,6 +16,8 @@ import { AnswerDraggableList } from "../AnswerDraggableList";
|
|||||||
import ButtonsOptions from "../ButtonsOptions";
|
import ButtonsOptions from "../ButtonsOptions";
|
||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { useDisclosure } from "../../../utils/useDisclosure";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -25,30 +26,47 @@ interface Props {
|
|||||||
|
|
||||||
export default function OptionsPicture({ question }: Props) {
|
export default function OptionsPicture({ question }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
|
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
|
||||||
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageUpload = (files: FileList | null) => {
|
const handleImageUpload = async (files: FileList | null) => {
|
||||||
if (!files?.length || !selectedVariantId) return;
|
if (!files?.length || !selectedVariantId) return;
|
||||||
|
|
||||||
const [file] = Array.from(files);
|
const [file] = Array.from(files);
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
|
||||||
setVariantOriginalImageUrl(question.id, selectedVariantId, url);
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
variant.originalImageUrl = url;
|
||||||
|
});
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(url, url);
|
setCropModal(file, url);
|
||||||
|
openCropModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||||
if (!selectedVariantId) return;
|
if (!selectedVariantId) return;
|
||||||
|
|
||||||
setVariantImageUrl(question.id, selectedVariantId, url);
|
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants.find(variant => variant.id === selectedVariantId);
|
||||||
|
if (!variant) return;
|
||||||
|
|
||||||
|
variant.extendedText = url;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,14 +80,15 @@ export default function OptionsPicture({ question }: Props) {
|
|||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
setSelectedVariantId(variant.id);
|
setSelectedVariantId(variant.id);
|
||||||
if (variant.extendedText) {
|
if (variant.extendedText) {
|
||||||
return openCropModal(
|
openCropModal();
|
||||||
|
setCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl || ""
|
variant.originalImageUrl || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openImageUploadModal();
|
openImageUploadModal();
|
||||||
@ -89,14 +108,15 @@ export default function OptionsPicture({ question }: Props) {
|
|||||||
<AddOrEditImageButton
|
<AddOrEditImageButton
|
||||||
imageSrc={variant.extendedText}
|
imageSrc={variant.extendedText}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
setSelectedVariantId(variant.id);
|
setSelectedVariantId(variant.id);
|
||||||
if (variant.extendedText) {
|
if (variant.extendedText) {
|
||||||
return openCropModal(
|
openCropModal();
|
||||||
|
setCropModal(
|
||||||
variant.extendedText,
|
variant.extendedText,
|
||||||
variant.originalImageUrl || ""
|
variant.originalImageUrl || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openImageUploadModal();
|
openImageUploadModal();
|
||||||
@ -111,8 +131,8 @@ export default function OptionsPicture({ question }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<UploadImageModal imgHC={handleImageUpload} />
|
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} />
|
||||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} />
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import SettingOpytionsPict from "./settingOpytionsPict";
|
import SettingOpytionsPict from "./settingOpytionsPict";
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import { QuizQuestionImages } from "@model/questionTypes/images";
|
import { QuizQuestionImages } from "@model/questionTypes/images";
|
||||||
@ -18,8 +17,6 @@ export default function SwitchAnswerOptionsPict({
|
|||||||
return <SettingOpytionsPict question={question} />;
|
return <SettingOpytionsPict question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionText } from "@model/questionTypes/text";
|
import { QuizQuestionText } from "@model/questionTypes/text";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingTextField from "./settingTextField";
|
import SettingTextField from "./settingTextField";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchTextField({
|
|||||||
return <SettingTextField question={question} />;
|
return <SettingTextField question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
import { openCropModal } from "@root/cropModal";
|
import { setCropModal } from "@root/cropModal";
|
||||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
|
||||||
import { setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestion } from "@root/questions/actions";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
@ -13,6 +13,7 @@ import ButtonsOptions from "../ButtonsOptions";
|
|||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
import { UploadVideoModal } from "../UploadVideoModal";
|
import { UploadVideoModal } from "../UploadVideoModal";
|
||||||
import SwitchPageOptions from "./switchPageOptions";
|
import SwitchPageOptions from "./switchPageOptions";
|
||||||
|
import { useDisclosure } from "../../../utils/useDisclosure";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -27,6 +28,9 @@ export default function PageOptions({ disableInput, question }: Props) {
|
|||||||
const isTablet = useMediaQuery(theme.breakpoints.down(980));
|
const isTablet = useMediaQuery(theme.breakpoints.down(980));
|
||||||
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
|
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(780));
|
const isMobile = useMediaQuery(theme.breakpoints.down(780));
|
||||||
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
|
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
|
||||||
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
||||||
|
|
||||||
const setText = useDebouncedCallback((value) => {
|
const setText = useDebouncedCallback((value) => {
|
||||||
updateQuestion(question.id, question => {
|
updateQuestion(question.id, question => {
|
||||||
@ -40,19 +44,26 @@ export default function PageOptions({ disableInput, question }: Props) {
|
|||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleImageUpload(fileList: FileList | null) {
|
async function handleImageUpload(fileList: FileList | null) {
|
||||||
if (!fileList?.length) return;
|
if (!fileList?.length) return;
|
||||||
|
|
||||||
const url = URL.createObjectURL(fileList[0]);
|
const url = await uploadQuestionImage(question.id, quizQid, fileList[0], (question, url) => {
|
||||||
|
if (question.type !== "page") return;
|
||||||
|
|
||||||
setPageQuestionPicture(question.id, url);
|
question.content.picture = url;
|
||||||
setPageQuestionOriginalPicture(question.id, url);
|
question.content.originalPicture = url;
|
||||||
|
});
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(url, url);
|
setCropModal(fileList[0], url);
|
||||||
|
openCropModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||||
setPageQuestionPicture(question.id, url);
|
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||||
|
if (question.type !== "page") return;
|
||||||
|
|
||||||
|
question.content.picture = url;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,7 +108,7 @@ export default function PageOptions({ disableInput, question }: Props) {
|
|||||||
imageSrc={question.content.picture}
|
imageSrc={question.content.picture}
|
||||||
onImageClick={() => {
|
onImageClick={() => {
|
||||||
if (question.content.picture) {
|
if (question.content.picture) {
|
||||||
return openCropModal(
|
return setCropModal(
|
||||||
question.content.picture,
|
question.content.picture,
|
||||||
question.content.originalPicture
|
question.content.originalPicture
|
||||||
);
|
);
|
||||||
@ -122,8 +133,8 @@ export default function PageOptions({ disableInput, question }: Props) {
|
|||||||
Изображение
|
Изображение
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<UploadImageModal imgHC={handleImageUpload} />
|
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} />
|
||||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} />
|
||||||
<Typography> или</Typography>
|
<Typography> или</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionPage } from "@model/questionTypes/page";
|
import { QuizQuestionPage } from "@model/questionTypes/page";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingPageOptions from "./SettingPageOptions";
|
import SettingPageOptions from "./SettingPageOptions";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchPageOptions({
|
|||||||
return <SettingPageOptions question={question} />;
|
return <SettingPageOptions question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/pages/Questions/QuestionSwitchWindowTool.tsx
Normal file
27
src/pages/Questions/QuestionSwitchWindowTool.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DraggableList } from "./DraggableList";
|
||||||
|
import { BranchingPanel } from "./BranchingPanel";
|
||||||
|
import { BranchingMap } from "./BranchingMap";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settingBranching: boolean;
|
||||||
|
setSettingBranching: (active: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionSwitchWindowTool = ({settingBranching, setSettingBranching}:Props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||||
|
<Box sx={{ flexBasis: "796px" }}>
|
||||||
|
{settingBranching ? <BranchingMap /> : <DraggableList />}
|
||||||
|
</Box>
|
||||||
|
<BranchingPanel
|
||||||
|
active={settingBranching}
|
||||||
|
setActive={setSettingBranching}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useState, useEffect } from "react"
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -7,41 +7,25 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions";
|
||||||
collapseAllQuestions,
|
import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions";
|
||||||
createUntypedQuestion,
|
|
||||||
} from "@root/questions/actions";
|
|
||||||
import {
|
|
||||||
decrementCurrentStep,
|
|
||||||
incrementCurrentStep,
|
|
||||||
} from "@root/quizes/actions";
|
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
|
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import AddPlus from "../../assets/icons/questionsPage/addPlus";
|
import AddPlus from "../../assets/icons/questionsPage/addPlus";
|
||||||
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
||||||
import { DraggableList } from "./DraggableList";
|
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal"
|
||||||
import { setDefaultState } from "@root/questions/actions";
|
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
|
||||||
|
|
||||||
export default function QuestionsPage() {
|
export default function QuestionsPage() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { openedModalSettingsId } = useQuestionsStore();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
useEffect(() => {
|
const [settingBranching, setSettingBranching] = useState<boolean>(false);
|
||||||
const setDefault = ({ code }: KeyboardEvent) => {
|
|
||||||
if (code === "Backslash") {
|
|
||||||
setDefaultState(Number(quiz?.id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", setDefault);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", setDefault);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!quiz) return null;
|
if (!quiz) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,6 +42,7 @@ export default function QuestionsPage() {
|
|||||||
<Typography variant={"h5"}>Заголовок квиза</Typography>
|
<Typography variant={"h5"}>Заголовок квиза</Typography>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
display: settingBranching ? "none" : "flex",
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
lineHeight: "19px",
|
lineHeight: "19px",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@ -70,7 +55,7 @@ export default function QuestionsPage() {
|
|||||||
Свернуть всё
|
Свернуть всё
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<DraggableList />
|
<QuestionSwitchWindowTool settingBranching={settingBranching} setSettingBranching={setSettingBranching} />
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -116,6 +101,7 @@ export default function QuestionsPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{createPortal(<QuizPreview />, document.body)}
|
{createPortal(<QuizPreview />, document.body)}
|
||||||
|
{/* {openedModalSettingsId !== null && <BranchingQuestions/>} */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
const buttonRatingForm: ButtonRatingFrom[] = [
|
const buttonRatingForm: ButtonRatingFrom[] = [
|
||||||
{
|
{
|
||||||
name: "star",
|
name: "star",
|
||||||
icon: <StarIconMini width={50} color={theme.palette.grey2.main} />,
|
icon: <StarIconMini width={35} color={theme.palette.grey2.main} />,
|
||||||
},
|
},
|
||||||
{ name: "trophie", icon: <TropfyIcon color={theme.palette.grey2.main} /> },
|
{ name: "trophie", icon: <TropfyIcon color={theme.palette.grey2.main} /> },
|
||||||
{ name: "flag", icon: <FlagIcon color={theme.palette.grey2.main} /> },
|
{ name: "flag", icon: <FlagIcon color={theme.palette.grey2.main} /> },
|
||||||
@ -107,8 +107,8 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
maxWidth: "440px",
|
maxWidth: "440px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
padding: "0 10px",
|
padding: "10px 10px 0 10px",
|
||||||
gap: isMobile ? "10px" : "15px",
|
gap: isMobile ? "10px" : "30px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Array.from(
|
{Array.from(
|
||||||
@ -172,13 +172,13 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
defaultValue={question.content.ratingNegativeDescription}
|
defaultValue={question.content.ratingNegativeDescription}
|
||||||
value={negativeText}
|
value={negativeText}
|
||||||
placeholder="Негативно"
|
placeholder="Негативно"
|
||||||
onChange={({ target }) => {
|
onChange={({ target }: {target: HTMLInputElement}) => {
|
||||||
if (target.value.length <= 15) {
|
if (target.value.length <= 15) {
|
||||||
setNegativeText(target.value);
|
setNegativeText(target.value);
|
||||||
debounceNegativeDescription(target.value);
|
debounceNegativeDescription(target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={({ target }) => debounceNegativeDescription(target.value)}
|
onBlur={({ target }: {target: HTMLInputElement}) => debounceNegativeDescription(target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
width: negativeTextWidth + 10 + "px",
|
width: negativeTextWidth + 10 + "px",
|
||||||
maxWidth: isMobile ? "140px" : "230px",
|
maxWidth: isMobile ? "140px" : "230px",
|
||||||
@ -206,7 +206,7 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ minWidth: isMobile ? "140px" : "205px" }}>
|
<Box sx={{ minWidth: isMobile ? "140px" : "205px", textAlign: isMobile ? "right" : undefined, }}>
|
||||||
<Typography
|
<Typography
|
||||||
ref={positiveRef}
|
ref={positiveRef}
|
||||||
sx={{
|
sx={{
|
||||||
@ -222,13 +222,13 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
<TextField
|
<TextField
|
||||||
value={positiveText}
|
value={positiveText}
|
||||||
placeholder="Позитивно"
|
placeholder="Позитивно"
|
||||||
onChange={({ target }) => {
|
onChange={({ target }: {target: HTMLInputElement}) => {
|
||||||
if (target.value.length <= 15) {
|
if (target.value.length <= 15) {
|
||||||
setPositiveText(target.value);
|
setPositiveText(target.value);
|
||||||
debouncePositiveDescription(target.value);
|
debouncePositiveDescription(target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={({ target }) => debouncePositiveDescription(target.value)}
|
onBlur={({ target }: {target: HTMLInputElement}) => debouncePositiveDescription(target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
width: positiveTextWidth + 10 + "px",
|
width: positiveTextWidth + 10 + "px",
|
||||||
maxWidth: isMobile ? "140px" : "230px",
|
maxWidth: isMobile ? "140px" : "230px",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionRating } from "@model/questionTypes/rating";
|
import { QuizQuestionRating } from "@model/questionTypes/rating";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingRating from "./settingRating";
|
import SettingRating from "./settingRating";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchRating({
|
|||||||
return <SettingRating question={question} />;
|
return <SettingRating question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export default function SliderOptions({ question }: Props) {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: isTablet ? "auto" : "100%",
|
width: isTablet ? "auto" : "100%",
|
||||||
maxWidth: "673.8px",
|
maxWidth: "720.8px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
pl: "20px",
|
pl: "20px",
|
||||||
pr: isMobile ? "13px" : "20px",
|
pr: isMobile ? "13px" : "20px",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionNumber } from "@model/questionTypes/number";
|
import { QuizQuestionNumber } from "@model/questionTypes/number";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingSlider from "./settingSlider";
|
import SettingSlider from "./settingSlider";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchSlider({
|
|||||||
return <SettingSlider question={question} />;
|
return <SettingSlider question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function UploadFile({ question }: Props) {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: isTablet ? "auto" : "100%",
|
width: isTablet ? "auto" : "100%",
|
||||||
maxWidth: "640px",
|
maxWidth: "680px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
px: "20px",
|
px: "20px",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@ -163,9 +163,10 @@ export default function UploadFile({ question }: Props) {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "end",
|
||||||
marginBottom: "20px",
|
marginBottom: "20px",
|
||||||
marginTop: "15px",
|
marginTop: "15px",
|
||||||
|
gap: "9px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionFile } from "@model/questionTypes/file";
|
import { QuizQuestionFile } from "@model/questionTypes/file";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingsUpload from "./settingUpload";
|
import SettingsUpload from "./settingUpload";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchUpload({
|
|||||||
return <SettingsUpload question={question} />;
|
return <SettingsUpload question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,17 +14,19 @@ import * as React from "react";
|
|||||||
import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
|
import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
|
||||||
|
|
||||||
import type { DragEvent } from "react";
|
import type { DragEvent } from "react";
|
||||||
import { closeImageUploadModal, useImageUploadModalStore } from "@root/imageUploadModal";
|
|
||||||
|
|
||||||
interface ModalkaProps {
|
interface ModalkaProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
imgHC: (imgInp: FileList | null) => void;
|
imgHC: (imgInp: FileList | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadImageModal: React.FC<ModalkaProps> = ({
|
export const UploadImageModal: React.FC<ModalkaProps> = ({
|
||||||
imgHC,
|
imgHC,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isOpen = useImageUploadModalStore(state => state.isOpen)
|
|
||||||
|
|
||||||
const dropZone = React.useRef<HTMLDivElement>(null);
|
const dropZone = React.useRef<HTMLDivElement>(null);
|
||||||
const [ready, setReady] = React.useState(false);
|
const [ready, setReady] = React.useState(false);
|
||||||
@ -44,7 +46,7 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onClose={closeImageUploadModal}
|
onClose={onClose}
|
||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||||
import { openCropModal } from "@root/cropModal";
|
import { uploadQuestionImage } from "@root/questions/actions";
|
||||||
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
|
|
||||||
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 { type DragEvent } from "react";
|
import { type DragEvent } from "react";
|
||||||
import UploadIcon from "../../../assets/icons/UploadIcon";
|
import UploadIcon from "../../../assets/icons/UploadIcon";
|
||||||
import { UploadImageModal } from "./UploadImageModal";
|
import { UploadImageModal } from "./UploadImageModal";
|
||||||
|
import { setCropModal } from "@root/cropModal";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { useDisclosure } from "../../../utils/useDisclosure";
|
||||||
|
|
||||||
|
|
||||||
type UploadImageProps = {
|
type UploadImageProps = {
|
||||||
@ -16,18 +17,20 @@ type UploadImageProps = {
|
|||||||
|
|
||||||
export default function UploadImage({ question }: UploadImageProps) {
|
export default function UploadImage({ question }: UploadImageProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const quizQid = useCurrentQuiz()?.qid;
|
||||||
|
const [isCropModalOpen, openCropModal, closeCropModal] = useDisclosure();
|
||||||
|
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
||||||
|
|
||||||
const handleImageUpload = (files: FileList | null) => {
|
const handleImageUpload = async (files: FileList | null) => {
|
||||||
if (!files?.length) return;
|
if (!files?.length) return;
|
||||||
|
|
||||||
const [file] = Array.from(files);
|
const url = await uploadQuestionImage(question.id, quizQid, files[0], (question, url) => {
|
||||||
|
question.content.back = url;
|
||||||
const url = URL.createObjectURL(file);
|
question.content.originalBack = url;
|
||||||
|
});
|
||||||
setQuestionBackgroundImage(question.id, url);
|
|
||||||
setQuestionOriginalBackgroundImage(question.id, url);
|
|
||||||
closeImageUploadModal();
|
closeImageUploadModal();
|
||||||
openCropModal(url, url);
|
setCropModal(files[0], url);
|
||||||
|
openCropModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
@ -37,8 +40,10 @@ export default function UploadImage({ question }: UploadImageProps) {
|
|||||||
handleImageUpload(event.dataTransfer.files);
|
handleImageUpload(event.dataTransfer.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleCropModalSaveClick(url: string) {
|
function handleCropModalSaveClick(imageBlob: Blob) {
|
||||||
setQuestionBackgroundImage(question.id, url);
|
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
|
||||||
|
question.content.back = url;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -81,8 +86,8 @@ export default function UploadImage({ question }: UploadImageProps) {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<UploadImageModal imgHC={handleImageUpload} />
|
<UploadImageModal isOpen={isImageUploadOpen} onClose={closeImageUploadModal} imgHC={handleImageUpload} />
|
||||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
<CropModal isOpen={isCropModalOpen} onClose={closeCropModal} onSaveImageClick={handleCropModalSaveClick} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
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";
|
||||||
|
|
||||||
@ -19,8 +18,6 @@ export default function SwitchAnswerOptions({
|
|||||||
return <ResponseSettings question={question} />;
|
return <ResponseSettings question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
case "image":
|
case "image":
|
||||||
return <UploadImage question={question} />;
|
return <UploadImage question={question} />;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
|
||||||
import AddImage from "@icons/questionsPage/addImage";
|
import AddImage from "@icons/questionsPage/addImage";
|
||||||
import AddVideofile from "@icons/questionsPage/addVideofile";
|
import AddVideofile from "@icons/questionsPage/addVideofile";
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
|
||||||
import { openCropModal } from "@root/cropModal";
|
|
||||||
|
|
||||||
export default function ImageAndVideoButtons() {
|
export default function ImageAndVideoButtons() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -11,8 +9,7 @@ export default function ImageAndVideoButtons() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
||||||
<AddImage onClick={() => openCropModal("", "")} />
|
<AddImage onClick={undefined/* TODO () => openCropModal("", "") */} />
|
||||||
<CropModal />
|
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import Info from "@icons/Info";
|
import Info from "@icons/Info";
|
||||||
import { Box, TextField } from "@mui/material";
|
import { Box, TextField } from "@mui/material";
|
||||||
import BranchingQuestions from "../../Questions/branchingQuestions";
|
|
||||||
import PointsQuestions from "./PointsQuestions";
|
import PointsQuestions from "./PointsQuestions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -45,10 +44,6 @@ export default function SwitchResult({
|
|||||||
case "setting":
|
case "setting":
|
||||||
return <ResponseSettings />;
|
return <ResponseSettings />;
|
||||||
break;
|
break;
|
||||||
case "branching":
|
|
||||||
// return <BranchingQuestions question={question} />;
|
|
||||||
return null
|
|
||||||
break;
|
|
||||||
case "points":
|
case "points":
|
||||||
return <PointsQuestions />;
|
return <PointsQuestions />;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useRef } from "react";
|
||||||
import ChartIcon from "@icons/ChartIcon";
|
import ChartIcon from "@icons/ChartIcon";
|
||||||
import LinkIcon from "@icons/LinkIcon";
|
import LinkIcon from "@icons/LinkIcon";
|
||||||
import PencilIcon from "@icons/PencilIcon";
|
import PencilIcon from "@icons/PencilIcon";
|
||||||
@ -10,6 +11,8 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
Popover,
|
||||||
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -31,6 +34,8 @@ export default function QuizCard({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [subMenuOpen, setSubMenuOpen] = useState<boolean>(false);
|
||||||
|
const subMenuRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
function handleEditClick() {
|
function handleEditClick() {
|
||||||
setEditQuizId(quiz.backendId);
|
setEditQuizId(quiz.backendId);
|
||||||
@ -134,15 +139,45 @@ export default function QuizCard({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
ref={subMenuRef}
|
||||||
sx={{
|
sx={{
|
||||||
color: theme.palette.brightPurple.main,
|
color: theme.palette.brightPurple.main,
|
||||||
ml: "auto",
|
ml: "auto",
|
||||||
}}
|
}}
|
||||||
onClick={() => deleteQuiz(quiz.id)}
|
onClick={() => setSubMenuOpen(true)}
|
||||||
data-cy="delete-quiz"
|
data-cy="delete-quiz"
|
||||||
>
|
>
|
||||||
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
|
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
open={subMenuOpen}
|
||||||
|
anchorEl={subMenuRef.current}
|
||||||
|
onClose={() => setSubMenuOpen(false)}
|
||||||
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
|
>
|
||||||
|
<Box onClick={() => setSubMenuOpen(false)}>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
padding: "15px 30px",
|
||||||
|
}}
|
||||||
|
onClick={()=>{}}
|
||||||
|
>
|
||||||
|
Копировать
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
padding: "15px 30px",
|
||||||
|
}}
|
||||||
|
onClick={() => deleteQuiz(quiz.id)}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -171,21 +171,6 @@ export default function StartPage() {
|
|||||||
gap: "15px",
|
gap: "15px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<EyeIcon />}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
fontSize: "14px",
|
|
||||||
lineHeight: "18px",
|
|
||||||
height: "34px",
|
|
||||||
"& .MuiButton-startIcon": {
|
|
||||||
mr: "3px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Предпросмотр
|
|
||||||
</Button>
|
|
||||||
<a href={`/view/${quiz?.backendId}`} target="_blank" rel="noreferrer" style={{ textDecoration: "none" }}>
|
<a href={`/view/${quiz?.backendId}`} target="_blank" rel="noreferrer" style={{ textDecoration: "none" }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@ -253,21 +238,6 @@ export default function StartPage() {
|
|||||||
background: "#FFF",
|
background: "#FFF",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<EyeIcon />}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
fontSize: "14px",
|
|
||||||
lineHeight: "18px",
|
|
||||||
height: "34px",
|
|
||||||
"& .MuiButton-startIcon": {
|
|
||||||
mr: "3px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Предпросмотр
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -153,7 +153,7 @@ export default function StartPageSettings() {
|
|||||||
}`,
|
}`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
flex: `1 1 342px`,
|
flex: `1 1 361px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@ -306,6 +306,10 @@ export default function StartPageSettings() {
|
|||||||
text={"5 MB максимум"}
|
text={"5 MB максимум"}
|
||||||
heightImg={"110px"}
|
heightImg={"110px"}
|
||||||
sx={{ maxWidth: "300px" }}
|
sx={{ maxWidth: "300px" }}
|
||||||
|
imageUrl={quiz.config.startpage.background.desktop}
|
||||||
|
onImageUpload={(quiz, url) => {
|
||||||
|
quiz.config.startpage.background.desktop = url;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -313,7 +317,7 @@ export default function StartPageSettings() {
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
mt: "20px",
|
mt: "10px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "10px",
|
gap: "10px",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@ -333,6 +337,9 @@ export default function StartPageSettings() {
|
|||||||
textDecorationColor: theme.palette.brightPurple.main,
|
textDecorationColor: theme.palette.brightPurple.main,
|
||||||
ml: "-9px",
|
ml: "-9px",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
|
"& .css-14o5ia4-MuiTypography-root": {
|
||||||
|
fontSize: "16px"
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
MobileVersionHC(!mobileVersion);
|
MobileVersionHC(!mobileVersion);
|
||||||
@ -350,7 +357,14 @@ export default function StartPageSettings() {
|
|||||||
>
|
>
|
||||||
Изображение для мобильной версии
|
Изображение для мобильной версии
|
||||||
</Typography>
|
</Typography>
|
||||||
<DropZone text={"5 MB максимум"} heightImg={"110px"} />
|
<DropZone
|
||||||
|
text={"5 MB максимум"}
|
||||||
|
heightImg={"110px"}
|
||||||
|
imageUrl={quiz.config.startpage.background.mobile}
|
||||||
|
onImageUpload={(quiz, url) => {
|
||||||
|
quiz.config.startpage.background.mobile = url;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
@ -432,7 +446,14 @@ export default function StartPageSettings() {
|
|||||||
>
|
>
|
||||||
Изображение для мобильной версии
|
Изображение для мобильной версии
|
||||||
</Typography>
|
</Typography>
|
||||||
<DropZone text={"5 MB максимум"} heightImg={"110px"} />
|
<DropZone
|
||||||
|
text={"5 MB максимум"}
|
||||||
|
heightImg={"110px"}
|
||||||
|
imageUrl={quiz.config.startpage.background.mobile}
|
||||||
|
onImageUpload={(quiz, url) => {
|
||||||
|
quiz.config.startpage.background.mobile = url;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
@ -496,6 +517,10 @@ export default function StartPageSettings() {
|
|||||||
text={"5 MB максимум"}
|
text={"5 MB максимум"}
|
||||||
heightImg={"110px"}
|
heightImg={"110px"}
|
||||||
sx={{ maxWidth: "300px" }}
|
sx={{ maxWidth: "300px" }}
|
||||||
|
imageUrl={quiz.config.logo}
|
||||||
|
onImageUpload={(quiz, url) => {
|
||||||
|
quiz.config.logo = url;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -566,6 +591,10 @@ export default function StartPageSettings() {
|
|||||||
text={"5 MB максимум"}
|
text={"5 MB максимум"}
|
||||||
heightImg={"110px"}
|
heightImg={"110px"}
|
||||||
sx={{ maxWidth: "300px" }}
|
sx={{ maxWidth: "300px" }}
|
||||||
|
imageUrl={quiz.config.logo}
|
||||||
|
onImageUpload={(quiz, url) => {
|
||||||
|
quiz.config.logo = url;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -617,7 +646,7 @@ export default function StartPageSettings() {
|
|||||||
Заголовок
|
Заголовок
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который"
|
placeholder="Имя заголовка об опроснике для подбора табуретки"
|
||||||
text={quiz.name}
|
text={quiz.name}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.name = e.target.value;
|
quiz.name = e.target.value;
|
||||||
@ -634,7 +663,7 @@ export default function StartPageSettings() {
|
|||||||
Текст
|
Текст
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который "
|
placeholder="Внимательно заполняйте поля ответов"
|
||||||
text={quiz.config.startpage.description}
|
text={quiz.config.startpage.description}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.startpage.description = e.target.value;
|
quiz.config.startpage.description = e.target.value;
|
||||||
@ -651,7 +680,7 @@ export default function StartPageSettings() {
|
|||||||
Текст кнопки
|
Текст кнопки
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Начать"
|
placeholder="Начать опрос"
|
||||||
text={quiz.config.startpage.button}
|
text={quiz.config.startpage.button}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.startpage.button = e.target.value;
|
quiz.config.startpage.button = e.target.value;
|
||||||
@ -668,13 +697,14 @@ export default function StartPageSettings() {
|
|||||||
Телефон
|
Телефон
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="+7 900 000 00 00"
|
placeholder="8-800-000-00-00"
|
||||||
text={quiz.config.info.phonenumber}
|
text={quiz.config.info.phonenumber}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.info.phonenumber = e.target.value;
|
quiz.config.info.phonenumber = e.target.value;
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<CustomCheckbox
|
<CustomCheckbox
|
||||||
|
sx={{margin: "10px 0"}}
|
||||||
label="Кликабельный"
|
label="Кликабельный"
|
||||||
checked={quiz.config.info.clickable}
|
checked={quiz.config.info.clickable}
|
||||||
handleChange={e => updateQuiz(quiz.id, quiz => {
|
handleChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
@ -692,7 +722,7 @@ export default function StartPageSettings() {
|
|||||||
Название или слоган компании
|
Название или слоган компании
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который "
|
placeholder="Только лучшее"
|
||||||
text={quiz.config.info.orgname}
|
text={quiz.config.info.orgname}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.info.orgname = e.target.value;
|
quiz.config.info.orgname = e.target.value;
|
||||||
@ -709,7 +739,7 @@ export default function StartPageSettings() {
|
|||||||
Сайт
|
Сайт
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который "
|
placeholder="https://mysite.com"
|
||||||
text={quiz.config.info.site}
|
text={quiz.config.info.site}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.info.site = e.target.value;
|
quiz.config.info.site = e.target.value;
|
||||||
@ -726,7 +756,7 @@ export default function StartPageSettings() {
|
|||||||
Юридическая информация
|
Юридическая информация
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который "
|
placeholder="Данные наших документов"
|
||||||
text={quiz.config.info.law}
|
text={quiz.config.info.law}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.config.info.law = e.target.value;
|
quiz.config.info.law = e.target.value;
|
||||||
@ -784,6 +814,7 @@ export default function StartPageSettings() {
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
flexDirection: isTablet ? "column" : "row",
|
flexDirection: isTablet ? "column" : "row",
|
||||||
marginTop: "30px",
|
marginTop: "30px",
|
||||||
|
marginBottom: isTablet ? "90px" : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import UploadIcon from "@icons/UploadIcon";
|
import UploadIcon from "@icons/UploadIcon";
|
||||||
|
import { Quiz } from "@model/quiz/quiz";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
@ -7,7 +8,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { updateQuiz } from "@root/quizes/actions";
|
import { uploadQuizImage } from "@root/quizes/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -18,27 +19,27 @@ interface Props {
|
|||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
heightImg: string;
|
heightImg: string;
|
||||||
widthImg?: string;
|
widthImg?: string;
|
||||||
|
onImageUpload: (quiz: Quiz, url: string) => void;
|
||||||
|
imageUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Научи функцию принимать данные для валидации
|
//Научи функцию принимать данные для валидации
|
||||||
export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
export const DropZone = ({ text, sx, heightImg, widthImg, onImageUpload, imageUrl }: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
if (!quiz) return null; // TODO throw and catch with error boundary
|
if (!quiz) return null; // TODO throw and catch with error boundary
|
||||||
|
|
||||||
const imgHC = (imgInp: HTMLInputElement) => {
|
const imgHC = async (imgInp: HTMLInputElement) => {
|
||||||
|
if (!quiz) return;
|
||||||
|
|
||||||
const file = imgInp.files?.[0];
|
const file = imgInp.files?.[0];
|
||||||
if (file) {
|
|
||||||
if (file.size < 5242880) {
|
if (!file) return;
|
||||||
updateQuiz(quiz.id, quiz => {
|
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||||
quiz.config.startpage.background.desktop = URL.createObjectURL(file);
|
|
||||||
});
|
uploadQuizImage(quiz.id, file, onImageUpload);
|
||||||
} else {
|
|
||||||
enqueueSnackbar("Размер картинки слишком велик");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragenterHC = () => {
|
const dragenterHC = () => {
|
||||||
@ -54,13 +55,9 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
|||||||
setReady(false);
|
setReady(false);
|
||||||
|
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
if (file.size < 5242880) {
|
if (file.size < 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||||
updateQuiz(quiz.id, quiz => {
|
|
||||||
quiz.config.startpage.background.desktop = URL.createObjectURL(file);
|
uploadQuizImage(quiz.id, file, onImageUpload);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar("Размер картинки слишком велик");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragOverHC = (event: React.DragEvent<HTMLDivElement>) => {
|
const dragOverHC = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
@ -91,7 +88,7 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
|||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
|
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
opacity: quiz.config.startpage.background.desktop ? "0.5" : 1,
|
opacity: imageUrl ? "0.5" : 1,
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -109,11 +106,11 @@ export const DropZone = ({ text, sx, heightImg, widthImg }: Props) => {
|
|||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Typography>
|
</Typography>
|
||||||
{quiz.config.startpage.background.desktop && (
|
{imageUrl && (
|
||||||
<img
|
<img
|
||||||
height={heightImg}
|
height={heightImg}
|
||||||
width={widthImg}
|
width={widthImg}
|
||||||
src={quiz.config.startpage.background.desktop}
|
src={imageUrl}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: "-1",
|
zIndex: "-1",
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function Extra() {
|
|||||||
Mета заголовок
|
Mета заголовок
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который "
|
placeholder=""
|
||||||
text={quiz.config.meta}
|
text={quiz.config.meta}
|
||||||
onChange={mutationOrgMetaHC}
|
onChange={mutationOrgMetaHC}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,14 +3,12 @@ import { devtools } from "zustand/middleware";
|
|||||||
|
|
||||||
|
|
||||||
type CropModalStore = {
|
type CropModalStore = {
|
||||||
isCropModalOpen: boolean;
|
imageBlob: Blob | null;
|
||||||
imageUrl: string | null;
|
|
||||||
originalImageUrl: string | null;
|
originalImageUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: CropModalStore = {
|
export const initialState: CropModalStore = {
|
||||||
isCropModalOpen: false,
|
imageBlob: null,
|
||||||
imageUrl: null,
|
|
||||||
originalImageUrl: null,
|
originalImageUrl: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,19 +23,24 @@ export const useCropModalStore = create<CropModalStore>()(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const openCropModal = (imageUrl: string, originalImageUrl: string) => useCropModalStore.setState(
|
export const setCropModal = async (
|
||||||
{
|
imageBlob: Blob | string | null,
|
||||||
isCropModalOpen: true,
|
originalImageUrl: string | null | undefined,
|
||||||
imageUrl,
|
) => {
|
||||||
originalImageUrl,
|
if (typeof imageBlob === "string") {
|
||||||
},
|
const response = await fetch(imageBlob);
|
||||||
false,
|
imageBlob = await response.blob();
|
||||||
{
|
|
||||||
type: "openCropModal",
|
|
||||||
imageUrl,
|
|
||||||
originalImageUrl,
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
useCropModalStore.setState({
|
||||||
|
imageBlob,
|
||||||
|
originalImageUrl: originalImageUrl ?? null,
|
||||||
|
}, false, {
|
||||||
|
type: "setCropModal",
|
||||||
|
imageBlob,
|
||||||
|
originalImageUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const closeCropModal = () => useCropModalStore.setState(
|
export const closeCropModal = () => useCropModalStore.setState(
|
||||||
initialState,
|
initialState,
|
||||||
@ -45,22 +48,22 @@ export const closeCropModal = () => useCropModalStore.setState(
|
|||||||
"closeCropModal"
|
"closeCropModal"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const setCropModalImageUrl = (imageUrl: string | null) => useCropModalStore.setState(
|
export const setCropModalImageBlob = async (image: Blob | string | null) => {
|
||||||
{ imageUrl },
|
if (typeof image === "string") {
|
||||||
false,
|
const response = await fetch(image);
|
||||||
{
|
image = await response.blob();
|
||||||
type: "setCropModalImageUrl",
|
|
||||||
imageUrl,
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
export const resetToOriginalImage = (): boolean => {
|
useCropModalStore.setState({
|
||||||
if (!useCropModalStore.getState().originalImageUrl) return false;
|
imageBlob: image,
|
||||||
|
}, false, {
|
||||||
useCropModalStore.setState(
|
type: "setCropModalImageBlob",
|
||||||
state => ({ imageUrl: state.originalImageUrl }),
|
image,
|
||||||
false,
|
});
|
||||||
"resetToOriginalImage"
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setCropModalOriginalImageUrl = (originalImageUrl: string | null | undefined) => useCropModalStore.setState(
|
||||||
|
{ originalImageUrl: originalImageUrl ?? null },
|
||||||
|
false,
|
||||||
|
"setCropModalOriginalImageUrl"
|
||||||
|
);
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { create } from "zustand";
|
|
||||||
import { devtools, persist } from "zustand/middleware";
|
|
||||||
|
|
||||||
|
|
||||||
type ImageUploadModalStore = {
|
|
||||||
isOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState: ImageUploadModalStore = {
|
|
||||||
isOpen: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useImageUploadModalStore = create<ImageUploadModalStore>()(
|
|
||||||
persist(
|
|
||||||
devtools(
|
|
||||||
() => initialState,
|
|
||||||
{
|
|
||||||
name: "ImageUploadModalStore",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
name: "ImageUploadModalStore",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const openImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: true });
|
|
||||||
|
|
||||||
export const closeImageUploadModal = () => useImageUploadModalStore.setState({ isOpen: false });
|
|
||||||
@ -126,6 +126,7 @@ export const updateQuestion = <T extends AnyTypedQuizQuestion>(
|
|||||||
questionIndex: number,
|
questionIndex: number,
|
||||||
recipe: (question: T) => void,
|
recipe: (question: T) => void,
|
||||||
) => setProducedState(state => {
|
) => setProducedState(state => {
|
||||||
|
console.log("начинаю отправку fire квиза " )
|
||||||
const question = state.listQuestions[quizId][questionIndex] as T;
|
const question = state.listQuestions[quizId][questionIndex] as T;
|
||||||
|
|
||||||
recipe(question);
|
recipe(question);
|
||||||
@ -152,7 +153,6 @@ export const setVariantImageUrl = (
|
|||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const variant = question.content.variants[variantIndex];
|
const variant = question.content.variants[variantIndex];
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
if (variant.extendedText === url) return;
|
if (variant.extendedText === url) return;
|
||||||
|
|
||||||
@ -177,7 +177,6 @@ export const setVariantOriginalImageUrl = (
|
|||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const variant = question.content.variants[variantIndex];
|
const variant = question.content.variants[variantIndex];
|
||||||
if (!("originalImageUrl" in variant)) return;
|
|
||||||
|
|
||||||
if (variant.originalImageUrl === url) return;
|
if (variant.originalImageUrl === url) return;
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,9 @@
|
|||||||
import { questionApi } from "@api/question";
|
import { questionApi } from "@api/question";
|
||||||
|
import { quizApi } from "@api/quiz";
|
||||||
import { devlog } from "@frontend/kitui";
|
import { devlog } from "@frontend/kitui";
|
||||||
import { questionToEditQuestionRequest } from "@model/question/edit";
|
import { questionToEditQuestionRequest } from "@model/question/edit";
|
||||||
import {
|
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
||||||
QuestionType,
|
import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
|
||||||
RawQuestion,
|
|
||||||
rawQuestionToQuestion,
|
|
||||||
} from "@model/question/question";
|
|
||||||
import {
|
|
||||||
AnyTypedQuizQuestion,
|
|
||||||
QuestionVariant,
|
|
||||||
UntypedQuizQuestion,
|
|
||||||
createQuestionVariant,
|
|
||||||
} from "@model/questionTypes/shared";
|
|
||||||
import { defaultQuestionByType } from "../../constants/default";
|
import { defaultQuestionByType } from "../../constants/default";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@ -19,38 +11,19 @@ import { enqueueSnackbar } from "notistack";
|
|||||||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||||
import { RequestQueue } from "../../utils/requestQueue";
|
import { RequestQueue } from "../../utils/requestQueue";
|
||||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||||
import { QUESTIONS_DUMMY } from "../../constants/questions.dummy";
|
|
||||||
|
|
||||||
export const setDefaultState = (quizId: number) =>
|
|
||||||
setProducedState(
|
|
||||||
(state) => {
|
|
||||||
QUESTIONS_DUMMY.forEach((question) => {
|
|
||||||
state.questions.push(question);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "setDefaultState",
|
|
||||||
quizId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const setQuestions = (questions: RawQuestion[] | null) =>
|
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
||||||
setProducedState(
|
const untypedQuestions = state.questions.filter(q => q.type === null);
|
||||||
(state) => {
|
|
||||||
const untypedQuestions = state.questions.filter((q) => q.type === null);
|
|
||||||
|
|
||||||
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
|
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
|
||||||
state.questions.push(...untypedQuestions);
|
state.questions.push(...untypedQuestions);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "setQuestions",
|
type: "setQuestions",
|
||||||
questions,
|
questions,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const createUntypedQuestion = (quizId: number) =>
|
export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
|
||||||
setProducedState(
|
|
||||||
(state) => {
|
|
||||||
state.questions.push({
|
state.questions.push({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
quizId,
|
quizId,
|
||||||
@ -60,169 +33,130 @@ export const createUntypedQuestion = (quizId: number) =>
|
|||||||
deleted: false,
|
deleted: false,
|
||||||
expanded: true,
|
expanded: true,
|
||||||
});
|
});
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "createUntypedQuestion",
|
type: "createUntypedQuestion",
|
||||||
quizId,
|
quizId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const removeQuestion = (questionId: string) =>
|
const removeQuestion = (questionId: string) => setProducedState(state => {
|
||||||
setProducedState(
|
const index = state.questions.findIndex(q => q.id === questionId);
|
||||||
(state) => {
|
|
||||||
const index = state.questions.findIndex((q) => q.id === questionId);
|
|
||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
state.questions.splice(index, 1);
|
state.questions.splice(index, 1);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "removeQuestion",
|
type: "removeQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const updateUntypedQuestion = (
|
export const updateUntypedQuestion = (
|
||||||
questionId: string,
|
questionId: string,
|
||||||
updateFn: (question: UntypedQuizQuestion) => void
|
updateFn: (question: UntypedQuizQuestion) => void,
|
||||||
) => {
|
) => {
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
const question = state.questions.find(q => q.id === questionId);
|
||||||
const question = state.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
if (question.type !== null)
|
if (question.type !== null) throw new Error("Cannot update typed question, use 'updateQuestion' instead");
|
||||||
throw new Error(
|
|
||||||
"Cannot update typed question, use 'updateQuestion' instead"
|
|
||||||
);
|
|
||||||
|
|
||||||
updateFn(question);
|
updateFn(question);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "updateUntypedQuestion",
|
type: "updateUntypedQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
updateFn: updateFn.toString(),
|
updateFn: updateFn.toString(),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanQuestions = () =>
|
export const cleanQuestions = () => setProducedState(state => {
|
||||||
setProducedState(
|
|
||||||
(state) => {
|
|
||||||
state.questions = [];
|
state.questions = [];
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "cleanQuestions",
|
type: "cleanQuestions",
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const setQuestionBackendId = (questionId: string, backendId: number) =>
|
const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => {
|
||||||
setProducedState(
|
const question = state.questions.find(q => q.id === questionId);
|
||||||
(state) => {
|
|
||||||
const question = state.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
if (question.type === null)
|
if (question.type === null) throw new Error("Cannot set backend id for untyped question");
|
||||||
throw new Error("Cannot set backend id for untyped question");
|
|
||||||
|
|
||||||
question.backendId = backendId;
|
question.backendId = backendId;
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "setQuestionBackendId",
|
type: "setQuestionBackendId",
|
||||||
questionId: questionId,
|
questionId: questionId,
|
||||||
backendId,
|
backendId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const reorderQuestions = (
|
export const reorderQuestions = (
|
||||||
sourceIndex: number,
|
sourceIndex: number,
|
||||||
destinationIndex: number
|
destinationIndex: number,
|
||||||
) => {
|
) => {
|
||||||
if (sourceIndex === destinationIndex) return;
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
|
||||||
const [removed] = state.questions.splice(sourceIndex, 1);
|
const [removed] = state.questions.splice(sourceIndex, 1);
|
||||||
state.questions.splice(destinationIndex, 0, removed);
|
state.questions.splice(destinationIndex, 0, removed);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "reorderQuestions",
|
type: "reorderQuestions",
|
||||||
sourceIndex,
|
sourceIndex,
|
||||||
destinationIndex,
|
destinationIndex,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleExpandQuestion = (questionId: string) =>
|
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
|
||||||
setProducedState(
|
const question = state.questions.find(q => q.id === questionId);
|
||||||
(state) => {
|
|
||||||
const question = state.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
question.expanded = !question.expanded;
|
question.expanded = !question.expanded;
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "toggleExpandQuestion",
|
type: "toggleExpandQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const collapseAllQuestions = () =>
|
export const collapseAllQuestions = () => setProducedState(state => {
|
||||||
setProducedState((state) => {
|
state.questions.forEach(question => question.expanded = false);
|
||||||
state.questions.forEach((question) => (question.expanded = false));
|
|
||||||
}, "collapseAllQuestions");
|
}, "collapseAllQuestions");
|
||||||
|
|
||||||
|
|
||||||
const REQUEST_DEBOUNCE = 200;
|
const REQUEST_DEBOUNCE = 200;
|
||||||
const requestQueue = new RequestQueue();
|
const requestQueue = new RequestQueue();
|
||||||
let requestTimeoutId: ReturnType<typeof setTimeout>;
|
let requestTimeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
export const updateQuestion = (
|
export const updateQuestion = (
|
||||||
questionId: string,
|
questionId: string,
|
||||||
updateFn: (question: AnyTypedQuizQuestion) => void
|
updateFn: (question: AnyTypedQuizQuestion) => void,
|
||||||
) => {
|
) => {
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.content.id === questionId);
|
||||||
const question = state.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
if (question.type === null)
|
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
|
||||||
throw new Error(
|
|
||||||
"Cannot update untyped question, use 'updateUntypedQuestion' instead"
|
|
||||||
);
|
|
||||||
|
|
||||||
updateFn(question);
|
updateFn(question);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "updateQuestion",
|
type: "updateQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
updateFn: updateFn.toString(),
|
updateFn: updateFn.toString(),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
clearTimeout(requestTimeoutId);
|
// clearTimeout(requestTimeoutId);
|
||||||
requestTimeoutId = setTimeout(() => {
|
// requestTimeoutId = setTimeout(() => {
|
||||||
requestQueue
|
requestQueue.enqueue(async () => {
|
||||||
.enqueue(async () => {
|
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.content.id === questionId);
|
||||||
const q = useQuestionsStore
|
console.log("мы пытаемся найти вопрос ")
|
||||||
.getState()
|
|
||||||
.questions.find((q) => q.id === questionId);
|
|
||||||
if (!q) return;
|
if (!q) return;
|
||||||
if (q.type === null)
|
if (q.type === null) throw new Error("Cannot send update request for untyped question");
|
||||||
throw new Error("Cannot send update request for untyped question");
|
console.log(q.title)
|
||||||
|
|
||||||
const response = await questionApi.edit(
|
const response = await questionApi.edit(questionToEditQuestionRequest(q));
|
||||||
questionToEditQuestionRequest(q)
|
|
||||||
);
|
|
||||||
|
|
||||||
setQuestionBackendId(questionId, response.updated);
|
setQuestionBackendId(questionId, response.updated);
|
||||||
})
|
}).catch(error => {
|
||||||
.catch((error) => {
|
|
||||||
if (isAxiosCanceledError(error)) return;
|
if (isAxiosCanceledError(error)) return;
|
||||||
|
|
||||||
devlog("Error editing question", { error, questionId });
|
devlog("Error editing question", { error, questionId });
|
||||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||||
});
|
});
|
||||||
}, REQUEST_DEBOUNCE);
|
// }, REQUEST_DEBOUNCE);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addQuestionVariant = (questionId: string) => {
|
export const addQuestionVariant = (questionId: string) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, question => {
|
||||||
switch (question.type) {
|
switch (question.type) {
|
||||||
case "variant":
|
case "variant":
|
||||||
case "emoji":
|
case "emoji":
|
||||||
@ -231,24 +165,16 @@ export const addQuestionVariant = (questionId: string) => {
|
|||||||
case "varimg":
|
case "varimg":
|
||||||
question.content.variants.push(createQuestionVariant());
|
question.content.variants.push(createQuestionVariant());
|
||||||
break;
|
break;
|
||||||
default:
|
default: throw new Error(`Cannot add variant to question of type "${question.type}"`);
|
||||||
throw new Error(
|
|
||||||
`Cannot add variant to question of type "${question.type}"`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteQuestionVariant = (
|
export const deleteQuestionVariant = (questionId: string, variantId: string) => {
|
||||||
questionId: string,
|
updateQuestion(questionId, question => {
|
||||||
variantId: string
|
|
||||||
) => {
|
|
||||||
updateQuestion(questionId, (question) => {
|
|
||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const variantIndex = question.content.variants.findIndex(
|
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
|
||||||
(variant) => variant.id === variantId
|
|
||||||
);
|
|
||||||
if (variantIndex === -1) return;
|
if (variantIndex === -1) return;
|
||||||
|
|
||||||
question.content.variants.splice(variantIndex, 1);
|
question.content.variants.splice(variantIndex, 1);
|
||||||
@ -259,138 +185,82 @@ export const setQuestionVariantField = (
|
|||||||
questionId: string,
|
questionId: string,
|
||||||
variantId: string,
|
variantId: string,
|
||||||
field: keyof QuestionVariant,
|
field: keyof QuestionVariant,
|
||||||
value: QuestionVariant[keyof QuestionVariant]
|
value: QuestionVariant[keyof QuestionVariant],
|
||||||
) => {
|
) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, question => {
|
||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const variantIndex = question.content.variants.findIndex(
|
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
|
||||||
(variant) => variant.id === variantId
|
|
||||||
);
|
|
||||||
if (variantIndex === -1) return;
|
if (variantIndex === -1) return;
|
||||||
|
|
||||||
const variant = question.content.variants[variantIndex];
|
const variant = question.content.variants[variantIndex];
|
||||||
|
|
||||||
if (value) {
|
|
||||||
variant[field] = value;
|
variant[field] = value;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reorderQuestionVariants = (
|
export const reorderQuestionVariants = (
|
||||||
questionId: string,
|
questionId: string,
|
||||||
sourceIndex: number,
|
sourceIndex: number,
|
||||||
destinationIndex: number
|
destinationIndex: number,
|
||||||
) => {
|
) => {
|
||||||
if (sourceIndex === destinationIndex) return;
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, question => {
|
||||||
if (!("variants" in question.content)) return;
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
||||||
question.content.variants.splice(destinationIndex, 0, removed);
|
question.content.variants.splice(destinationIndex, 0, removed);
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setQuestionBackgroundImage = (questionId: string, url: string) => {
|
export const uploadQuestionImage = async (
|
||||||
updateQuestion(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: string,
|
questionId: string,
|
||||||
url: string
|
quizQid: string | undefined,
|
||||||
|
blob: Blob,
|
||||||
|
updateFn: (question: AnyTypedQuizQuestion, imageId: string) => void,
|
||||||
) => {
|
) => {
|
||||||
updateQuestion(questionId, (question) => {
|
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||||
if (question.content.originalBack === url) return;
|
if (!question || !quizQid) return;
|
||||||
|
|
||||||
URL.revokeObjectURL(question.content.originalBack);
|
try {
|
||||||
question.content.originalBack = url;
|
const response = await quizApi.addImages(question.quizId, blob);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setVariantImageUrl = (
|
const values = Object.values(response);
|
||||||
questionId: string,
|
if (values.length !== 1) {
|
||||||
variantId: string,
|
console.warn("Error uploading image");
|
||||||
url: string
|
return;
|
||||||
) => {
|
|
||||||
updateQuestion(questionId, (question) => {
|
|
||||||
if (!("variants" in question.content)) return;
|
|
||||||
|
|
||||||
const variant = question.content.variants.find(
|
|
||||||
(variant) => variant.id === variantId
|
|
||||||
);
|
|
||||||
if (!variant) return;
|
|
||||||
|
|
||||||
if (variant.extendedText === url) return;
|
|
||||||
|
|
||||||
if (variant.extendedText !== variant.originalImageUrl)
|
|
||||||
URL.revokeObjectURL(variant.extendedText);
|
|
||||||
variant.extendedText = url;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setVariantOriginalImageUrl = (
|
|
||||||
questionId: string,
|
|
||||||
variantId: string,
|
|
||||||
url: string
|
|
||||||
) => {
|
|
||||||
updateQuestion(questionId, (question) => {
|
|
||||||
if (!("variants" in question.content)) return;
|
|
||||||
|
|
||||||
const variant = question.content.variants.find(
|
|
||||||
(variant) => variant.id === variantId
|
|
||||||
) as QuestionVariant | undefined;
|
|
||||||
if (!variant) return;
|
|
||||||
|
|
||||||
if (variant.originalImageUrl === url) return;
|
|
||||||
|
|
||||||
if (variant.originalImageUrl) {
|
|
||||||
URL.revokeObjectURL(variant.originalImageUrl);
|
|
||||||
}
|
}
|
||||||
variant.originalImageUrl = url;
|
|
||||||
|
const imageId = values[0];
|
||||||
|
const imageUrl = `https://squiz.pena.digital/squizimages/${quizQid}/${imageId}`;
|
||||||
|
|
||||||
|
updateQuestion(questionId, question => {
|
||||||
|
updateFn(question, imageUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return imageUrl;
|
||||||
|
} catch (error) {
|
||||||
|
devlog("Error uploading question image", error);
|
||||||
|
|
||||||
|
enqueueSnackbar("Не удалось загрузить изображение");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setPageQuestionPicture = (questionId: string, url: string) => {
|
export const setQuestionInnerName = (
|
||||||
updateQuestion(questionId, (question) => {
|
|
||||||
if (question.type !== "page") return;
|
|
||||||
|
|
||||||
if (question.content.picture === url) return;
|
|
||||||
|
|
||||||
if (question.content.picture !== question.content.originalPicture)
|
|
||||||
URL.revokeObjectURL(question.content.picture);
|
|
||||||
question.content.picture = url;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setPageQuestionOriginalPicture = (
|
|
||||||
questionId: string,
|
questionId: string,
|
||||||
url: string
|
name: string,
|
||||||
) => {
|
) => {
|
||||||
updateQuestion(questionId, (question) => {
|
updateQuestion(questionId, question => {
|
||||||
if (question.type !== "page") return;
|
|
||||||
|
|
||||||
if (question.content.originalPicture === url) return;
|
|
||||||
|
|
||||||
URL.revokeObjectURL(question.content.originalPicture);
|
|
||||||
question.content.originalPicture = url;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setQuestionInnerName = (questionId: string, name: string) => {
|
|
||||||
updateQuestion(questionId, (question) => {
|
|
||||||
question.content.innerName = name;
|
question.content.innerName = name;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeQuestionType = (questionId: string, type: QuestionType) => {
|
export const changeQuestionType = (
|
||||||
updateQuestion(questionId, (question) => {
|
questionId: string,
|
||||||
|
type: QuestionType,
|
||||||
|
) => {
|
||||||
|
updateQuestion(questionId, question => {
|
||||||
question.type = type;
|
question.type = type;
|
||||||
question.content = defaultQuestionByType[type].content;
|
question.content = defaultQuestionByType[type].content;
|
||||||
});
|
});
|
||||||
@ -398,15 +268,11 @@ export const changeQuestionType = (questionId: string, type: QuestionType) => {
|
|||||||
|
|
||||||
export const createTypedQuestion = async (
|
export const createTypedQuestion = async (
|
||||||
questionId: string,
|
questionId: string,
|
||||||
type: QuestionType
|
type: QuestionType,
|
||||||
) =>
|
) => requestQueue.enqueue(async () => {
|
||||||
requestQueue.enqueue(async () => {
|
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||||
const question = useQuestionsStore
|
|
||||||
.getState()
|
|
||||||
.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
if (question.type !== null)
|
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
|
||||||
throw new Error("Cannot upgrade already typed question");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createdQuestion = await questionApi.create({
|
const createdQuestion = await questionApi.create({
|
||||||
@ -419,34 +285,25 @@ export const createTypedQuestion = async (
|
|||||||
content: JSON.stringify(defaultQuestionByType[type].content),
|
content: JSON.stringify(defaultQuestionByType[type].content),
|
||||||
});
|
});
|
||||||
|
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
const questionIndex = state.questions.findIndex(q => q.id === questionId);
|
||||||
const questionIndex = state.questions.findIndex(
|
if (questionIndex !== -1) state.questions.splice(
|
||||||
(q) => q.id === questionId
|
|
||||||
);
|
|
||||||
if (questionIndex !== -1)
|
|
||||||
state.questions.splice(
|
|
||||||
questionIndex,
|
questionIndex,
|
||||||
1,
|
1,
|
||||||
rawQuestionToQuestion(createdQuestion)
|
rawQuestionToQuestion(createdQuestion)
|
||||||
);
|
);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "createTypedQuestion",
|
type: "createTypedQuestion",
|
||||||
question,
|
question,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devlog("Error creating question", error);
|
devlog("Error creating question", error);
|
||||||
enqueueSnackbar("Не удалось создать вопрос");
|
enqueueSnackbar("Не удалось создать вопрос");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteQuestion = async (questionId: string) =>
|
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
|
||||||
requestQueue.enqueue(async () => {
|
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||||
const question = useQuestionsStore
|
|
||||||
.getState()
|
|
||||||
.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
if (question.type === null) {
|
if (question.type === null) {
|
||||||
@ -464,60 +321,72 @@ export const deleteQuestion = async (questionId: string) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const copyQuestion = async (questionId: string, quizId: number) =>
|
export const copyQuestion = async (questionId: string, quizId: number) => requestQueue.enqueue(async () => {
|
||||||
requestQueue.enqueue(async () => {
|
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||||
const question = useQuestionsStore
|
|
||||||
.getState()
|
|
||||||
.questions.find((q) => q.id === questionId);
|
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
|
|
||||||
if (question.type === null) {
|
if (question.type === null) {
|
||||||
const copiedQuestion = structuredClone(question);
|
const copiedQuestion = structuredClone(question);
|
||||||
copiedQuestion.id = nanoid();
|
copiedQuestion.id = nanoid();
|
||||||
|
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
|
||||||
state.questions.push(copiedQuestion);
|
state.questions.push(copiedQuestion);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "copyQuestion",
|
type: "copyQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
quizId,
|
quizId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { updated: newQuestionId } = await questionApi.copy(
|
const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId);
|
||||||
question.backendId,
|
|
||||||
quizId
|
|
||||||
);
|
|
||||||
|
|
||||||
const copiedQuestion = structuredClone(question);
|
const copiedQuestion = structuredClone(question);
|
||||||
copiedQuestion.backendId = newQuestionId;
|
copiedQuestion.backendId = newQuestionId;
|
||||||
copiedQuestion.id = nanoid();
|
copiedQuestion.id = nanoid();
|
||||||
|
|
||||||
setProducedState(
|
setProducedState(state => {
|
||||||
(state) => {
|
|
||||||
state.questions.push(copiedQuestion);
|
state.questions.push(copiedQuestion);
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
type: "copyQuestion",
|
type: "copyQuestion",
|
||||||
questionId,
|
questionId,
|
||||||
quizId,
|
quizId,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devlog("Error copying question", error);
|
devlog("Error copying question", error);
|
||||||
enqueueSnackbar("Не удалось скопировать вопрос");
|
enqueueSnackbar("Не удалось скопировать вопрос");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setProducedState<A extends string | { type: unknown }>(
|
function setProducedState<A extends string | { type: unknown; }>(
|
||||||
recipe: (state: QuestionsStore) => void,
|
recipe: (state: QuestionsStore) => void,
|
||||||
action?: A
|
action?: A,
|
||||||
) {
|
) {
|
||||||
useQuestionsStore.setState((state) => produce(state, recipe), false, action);
|
useQuestionsStore.setState(state => produce(state, recipe), false, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const cleardragQuestionContentId = () => {
|
||||||
|
useQuestionsStore.setState({dragQuestionContentId: null});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionById = (questionId: string | null) => {
|
||||||
|
if (questionId === null) return null;
|
||||||
|
return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null;
|
||||||
|
};
|
||||||
|
export const getQuestionByContentId = (questionContentId: string | null) => {
|
||||||
|
console.log("questionContentId " + questionContentId)
|
||||||
|
if (questionContentId === null) return null;
|
||||||
|
return useQuestionsStore.getState().questions.find(q => {
|
||||||
|
console.log(q.content.id)
|
||||||
|
console.log(q.content.id === questionContentId)
|
||||||
|
return ( q.content.id === questionContentId)}) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({openedModalSettingsId: id ? id : null});
|
||||||
|
export const updateDragQuestionContentId = (contentId?: string) => {
|
||||||
|
console.log("contentId " + contentId)
|
||||||
|
useQuestionsStore.setState({dragQuestionContentId: contentId ? contentId : null});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,15 @@ import { devtools } from "zustand/middleware";
|
|||||||
|
|
||||||
|
|
||||||
export type QuestionsStore = {
|
export type QuestionsStore = {
|
||||||
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
|
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion);
|
||||||
|
openedModalSettingsId: string | null;
|
||||||
|
dragQuestionContentId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: QuestionsStore = {
|
const initialState: QuestionsStore = {
|
||||||
questions: [],
|
questions: [],
|
||||||
|
openedModalSettingsId: null as null,
|
||||||
|
dragQuestionContentId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQuestionsStore = create<QuestionsStore>()(
|
export const useQuestionsStore = create<QuestionsStore>()(
|
||||||
|
|||||||
@ -28,6 +28,10 @@ export const toggleQuizPreview = () => useQuizPreviewStore.setState(
|
|||||||
state => ({ isPreviewShown: !state.isPreviewShown })
|
state => ({ isPreviewShown: !state.isPreviewShown })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const setCurrentQuestionIndex = (step: number) => useQuizPreviewStore.setState(
|
||||||
|
state => ({ currentQuestionIndex:state.currentQuestionIndex = step })
|
||||||
|
);
|
||||||
|
|
||||||
export const incrementCurrentQuestionIndex = (maxStep: number) => useQuizPreviewStore.setState(
|
export const incrementCurrentQuestionIndex = (maxStep: number) => useQuizPreviewStore.setState(
|
||||||
state => ({ currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep) })
|
state => ({ currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep) })
|
||||||
);
|
);
|
||||||
|
|||||||
@ -175,9 +175,44 @@ export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async (
|
|||||||
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
export const updateRootInfo = (quizId: string, have:boolean) => updateQuiz(
|
||||||
|
quizId,
|
||||||
|
quiz => {
|
||||||
|
quiz.config.haveRoot = have;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// TODO copy quiz
|
// TODO copy quiz
|
||||||
|
|
||||||
|
export const uploadQuizImage = async (
|
||||||
|
quizId: string,
|
||||||
|
blob: Blob,
|
||||||
|
updateFn: (quiz: Quiz, imageId: string) => void,
|
||||||
|
) => {
|
||||||
|
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
|
||||||
|
if (!quiz) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await quizApi.addImages(quiz.backendId, blob);
|
||||||
|
|
||||||
|
const values = Object.values(response);
|
||||||
|
if (values.length !== 1) {
|
||||||
|
console.warn("Error uploading image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageId = values[0];
|
||||||
|
|
||||||
|
updateQuiz(quizId, quiz => {
|
||||||
|
updateFn(quiz, `https://squiz.pena.digital/squizimages/${quiz.qid}/${imageId}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
devlog("Error uploading quiz image", error);
|
||||||
|
|
||||||
|
enqueueSnackbar("Не удалось загрузить изображение");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function setProducedState<A extends string | { type: unknown; }>(
|
function setProducedState<A extends string | { type: unknown; }>(
|
||||||
recipe: (state: QuizStore) => void,
|
recipe: (state: QuizStore) => void,
|
||||||
action?: A,
|
action?: A,
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default () => {
|
|||||||
>E-mail</Typography>
|
>E-mail</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
value={mail}
|
value={mail}
|
||||||
onChange={(e) => setContactFormMailField(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setContactFormMailField(e.target.value)}
|
||||||
placeholder="username@penahaub.com"
|
placeholder="username@penahaub.com"
|
||||||
name="name"
|
name="name"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export default function Header() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "30px",
|
gap: "31px",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
ml: "20px",
|
ml: "20px",
|
||||||
}}
|
}}
|
||||||
@ -89,21 +89,6 @@ export default function Header() {
|
|||||||
gap: "15px",
|
gap: "15px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<EyeIcon />}
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
fontSize: "14px",
|
|
||||||
lineHeight: "18px",
|
|
||||||
height: "34px",
|
|
||||||
"& .MuiButton-startIcon": {
|
|
||||||
mr: "3px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Предпросмотр
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -16,7 +16,8 @@ export default function MenuItem({ icon, text, isActive = false, isCollapsed, on
|
|||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
px: 0,
|
px: 0,
|
||||||
py: "3px",
|
pt: "5px",
|
||||||
|
pb: "3px"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
|
|||||||
@ -12,12 +12,11 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { closeCropModal, resetToOriginalImage, setCropModalImageUrl, useCropModalStore } from "@root/cropModal";
|
import { FC, useMemo, useRef, useState } from "react";
|
||||||
import { FC, useRef, useState } from "react";
|
|
||||||
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
||||||
import "react-image-crop/dist/ReactCrop.css";
|
import "react-image-crop/dist/ReactCrop.css";
|
||||||
import { canvasPreview } from "./utils/canvasPreview";
|
import { canvasPreview } from "./utils/canvasPreview";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { setCropModalImageBlob, useCropModalStore } from "@root/cropModal";
|
||||||
|
|
||||||
|
|
||||||
const styleSlider: SxProps<Theme> = {
|
const styleSlider: SxProps<Theme> = {
|
||||||
@ -45,21 +44,25 @@ const styleSlider: SxProps<Theme> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSaveImageClick?: (imageUrl: string) => void;
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSaveImageClick?: (imageBlob: Blob) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
export const CropModal: FC<Props> = ({isOpen, onSaveImageClick, onClose }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isCropModalOpen = useCropModalStore(state => state.isCropModalOpen);
|
|
||||||
const imageUrl = useCropModalStore(state => state.imageUrl);
|
|
||||||
const [crop, setCrop] = useState<Crop>();
|
const [crop, setCrop] = useState<Crop>();
|
||||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
||||||
|
const imageBlob = useCropModalStore(state => state.imageBlob);
|
||||||
|
const originalImageUrl = useCropModalStore(state => state.originalImageUrl);
|
||||||
const [darken, setDarken] = useState(0);
|
const [darken, setDarken] = useState(0);
|
||||||
const [rotate, setRotate] = useState(0);
|
const [rotate, setRotate] = useState(0);
|
||||||
const [width, setWidth] = useState<number>(0);
|
const [width, setWidth] = useState<number>(0);
|
||||||
const cropImageElementRef = useRef<HTMLImageElement>(null);
|
const cropImageElementRef = useRef<HTMLImageElement>(null);
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||||
|
|
||||||
|
const imageUrl = useMemo(() => imageBlob && URL.createObjectURL(imageBlob), [imageBlob]);
|
||||||
|
|
||||||
const handleCropClick = async () => {
|
const handleCropClick = async () => {
|
||||||
if (!completedCrop) throw new Error("No completed crop");
|
if (!completedCrop) throw new Error("No completed crop");
|
||||||
if (!cropImageElementRef.current) throw new Error("No image");
|
if (!cropImageElementRef.current) throw new Error("No image");
|
||||||
@ -75,27 +78,31 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
|||||||
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
|
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
|
||||||
|
|
||||||
canvasCopy.toBlob((blob) => {
|
canvasCopy.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) throw new Error("Failed to create blob");
|
||||||
throw new Error("Failed to create blob");
|
|
||||||
}
|
|
||||||
const newImageUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
setCropModalImageUrl(newImageUrl);
|
setCropModalImageBlob(blob);
|
||||||
setCrop(undefined);
|
setCrop(undefined);
|
||||||
setCompletedCrop(undefined);
|
setCompletedCrop(undefined);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleSaveClick() {
|
function handleSaveClick() {
|
||||||
if (imageUrl) onSaveImageClick?.(imageUrl);
|
if (imageBlob) onSaveImageClick?.(imageBlob);
|
||||||
setCrop(undefined);
|
setCrop(undefined);
|
||||||
setCompletedCrop(undefined);
|
setCompletedCrop(undefined);
|
||||||
closeCropModal();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadOriginalImage() {
|
async function handleLoadOriginalImage() {
|
||||||
const isSuccess = resetToOriginalImage();
|
if (!originalImageUrl) return;
|
||||||
if (!isSuccess) enqueueSnackbar("Не удалось восстановить оригинал. Приносим глубочайшие извинения");
|
|
||||||
|
const response = await fetch(originalImageUrl);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
onSaveImageClick?.(blob);
|
||||||
|
setCropModalImageBlob(blob);
|
||||||
|
setCrop(undefined);
|
||||||
|
setCompletedCrop(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImageSize = () => {
|
const getImageSize = () => {
|
||||||
@ -119,8 +126,8 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={isCropModalOpen}
|
open={ isOpen}
|
||||||
onClose={closeCropModal}
|
onClose={onClose}
|
||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
@ -257,6 +264,7 @@ export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleLoadOriginalImage}
|
onClick={handleLoadOriginalImage}
|
||||||
disableRipple
|
disableRipple
|
||||||
|
disabled={!originalImageUrl}
|
||||||
sx={{
|
sx={{
|
||||||
width: "215px",
|
width: "215px",
|
||||||
height: "48px",
|
height: "48px",
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import { Box, Button } from "@mui/material";
|
import { Box, Button } from "@mui/material";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { CropModal } from "./CropModal";
|
|
||||||
import { openCropModal } from "@root/cropModal";
|
|
||||||
|
|
||||||
const ImageCrop: FC = () => {
|
const ImageCrop: FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Button onClick={() => openCropModal("", "")}>Открыть модалку</Button>
|
<Button onClick={undefined}>Открыть модалку</Button>
|
||||||
<CropModal />
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export default function QuestionsMiniButton({ icon, text, onClick, dataCy }: Que
|
|||||||
height: "140px",
|
height: "140px",
|
||||||
border: "1px solid #9A9AAF",
|
border: "1px solid #9A9AAF",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
|
background: "transparent"
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
@ -34,6 +35,7 @@ export default function QuestionsMiniButton({ icon, text, onClick, dataCy }: Que
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
|
width: "100%",
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
lineHeight: "18.4px",
|
lineHeight: "18.4px",
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Box, Button, LinearProgress, Paper, Typography } from "@mui/material";
|
import { Box, Button, LinearProgress, Paper, Typography, FormControl, Select as MuiSelect, MenuItem, useTheme } from "@mui/material";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import {
|
import {
|
||||||
decrementCurrentQuestionIndex,
|
decrementCurrentQuestionIndex,
|
||||||
incrementCurrentQuestionIndex,
|
incrementCurrentQuestionIndex,
|
||||||
useQuizPreviewStore,
|
useQuizPreviewStore,
|
||||||
|
setCurrentQuestionIndex
|
||||||
} from "@root/quizPreview";
|
} from "@root/quizPreview";
|
||||||
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "model/questionTypes/shared";
|
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "model/questionTypes/shared";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@ -20,9 +21,10 @@ import Text from "./QuizPreviewQuestionTypes/Text";
|
|||||||
import Variant from "./QuizPreviewQuestionTypes/Variant";
|
import Variant from "./QuizPreviewQuestionTypes/Variant";
|
||||||
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
|
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
|
||||||
import { notReachable } from "../../utils/notReachable";
|
import { notReachable } from "../../utils/notReachable";
|
||||||
|
import ArrowDownIcon from "@icons/ArrowDownIcon";
|
||||||
|
|
||||||
export default function QuizPreviewLayout() {
|
export default function QuizPreviewLayout() {
|
||||||
|
const theme = useTheme();
|
||||||
const questions = useQuestionsStore(state => state.questions);
|
const questions = useQuestionsStore(state => state.questions);
|
||||||
const currentQuizStep = useQuizPreviewStore(
|
const currentQuizStep = useQuizPreviewStore(
|
||||||
(state) => state.currentQuestionIndex
|
(state) => state.currentQuestionIndex
|
||||||
@ -67,7 +69,9 @@ export default function QuizPreviewLayout() {
|
|||||||
whiteSpace: "break-spaces",
|
whiteSpace: "break-spaces",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
"&::-webkit-scrollbar": { width: 0 },
|
"&::-webkit-scrollbar": { width: 0, display: "none" },
|
||||||
|
msOverflowStyle: "none",
|
||||||
|
scrollbarWidth: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<QuestionPreviewComponent question={currentQuestion} />
|
<QuestionPreviewComponent question={currentQuestion} />
|
||||||
@ -76,11 +80,86 @@ export default function QuizPreviewLayout() {
|
|||||||
sx={{
|
sx={{
|
||||||
mt: "auto",
|
mt: "auto",
|
||||||
p: "16px",
|
p: "16px",
|
||||||
display: "flex",
|
|
||||||
borderTop: "1px solid #E3E3E3",
|
borderTop: "1px solid #E3E3E3",
|
||||||
alignItems: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box sx={{ marginBottom: "10px" }}>
|
||||||
|
<FormControl
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
sx={{ width: "100%", minWidth: "200px", height: "48px" }}
|
||||||
|
>
|
||||||
|
<MuiSelect
|
||||||
|
id="category-select"
|
||||||
|
variant="outlined"
|
||||||
|
value={currentQuizStep}
|
||||||
|
placeholder="Заголовок вопроса"
|
||||||
|
onChange={({ target }) =>
|
||||||
|
setCurrentQuestionIndex(window.Number(target.value))
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: `1px solid ${theme.palette.brightPurple.main} !important`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
mt: "8px",
|
||||||
|
p: "4px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #EEE4FC",
|
||||||
|
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MenuListProps: {
|
||||||
|
sx: {
|
||||||
|
py: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "8px",
|
||||||
|
"& .Mui-selected": {
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
px: "9px",
|
||||||
|
gap: "20px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
IconComponent={(props) => <ArrowDownIcon {...props} />}
|
||||||
|
>
|
||||||
|
{Object.values(questions).map(
|
||||||
|
({ id, title }, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={id}
|
||||||
|
value={index}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
p: "4px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`${index + 1}. ${title}`}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</MuiSelect>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
@ -134,6 +213,7 @@ export default function QuizPreviewLayout() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export default function Sidebar() {
|
|||||||
sx={{
|
sx={{
|
||||||
px: "16px",
|
px: "16px",
|
||||||
mt: "16px",
|
mt: "16px",
|
||||||
mb: "11px",
|
mb: "6px",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
lineHeight: "20px",
|
lineHeight: "20px",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
|||||||
@ -8,11 +8,7 @@ export class RequestQueue<T = unknown> {
|
|||||||
|
|
||||||
enqueue(action: () => Promise<T>) {
|
enqueue(action: () => Promise<T>) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.items.length === 2) {
|
|
||||||
this.items[1] = { action, resolve, reject };
|
|
||||||
} else {
|
|
||||||
this.items.push({ action, resolve, reject });
|
this.items.push({ action, resolve, reject });
|
||||||
}
|
|
||||||
this.dequeue();
|
this.dequeue();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/utils/useDisclosure.ts
Normal file
13
src/utils/useDisclosure.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export function useDisclosure(initialState = false) {
|
||||||
|
const [opened, setOpened] = useState(initialState);
|
||||||
|
|
||||||
|
return [
|
||||||
|
opened,
|
||||||
|
useCallback(() => setOpened(true), []),
|
||||||
|
useCallback(() => setOpened(false), []),
|
||||||
|
useCallback(() => setOpened(prev => !prev), []),
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
30
yarn.lock
30
yarn.lock
@ -1890,7 +1890,7 @@
|
|||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
source-map "^0.7.3"
|
source-map "^0.7.3"
|
||||||
|
|
||||||
"@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
|
"@popperjs/core@^2.0.0", "@popperjs/core@^2.11.6", "@popperjs/core@^2.11.8", "@popperjs/core@^2.9.2":
|
||||||
version "2.11.8"
|
version "2.11.8"
|
||||||
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
|
||||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||||
@ -2232,11 +2232,24 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/cytoscape-popper@^2.0.4":
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cytoscape-popper/-/cytoscape-popper-2.0.4.tgz#cd1e81d28b202e2bfc5608e0e60ae53c908bf0e3"
|
||||||
|
integrity sha512-vGRiAMXeEIoY5ziPO0NrS8xmJyVkT8j8ARzvyD/x6CXciAO1+80Q2/Triyd9/+5I4PeatGo1Pch5YBwDMB1D6A==
|
||||||
|
dependencies:
|
||||||
|
"@popperjs/core" "^2.0.0"
|
||||||
|
"@types/cytoscape" "*"
|
||||||
|
|
||||||
"@types/cytoscape@*":
|
"@types/cytoscape@*":
|
||||||
version "3.19.13"
|
version "3.19.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.13.tgz#2e4c5df1e85d4ad77b7bf1375f0f01c8f26457b1"
|
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.13.tgz#2e4c5df1e85d4ad77b7bf1375f0f01c8f26457b1"
|
||||||
integrity sha512-FZY6Zyh8kGqEwZ6jizCJa5rbKriOWqJaEoGR8AG2LNQyo5sy1q7hFNo8ojbRpHNBluBmCJa+/w//OWaehxolgA==
|
integrity sha512-FZY6Zyh8kGqEwZ6jizCJa5rbKriOWqJaEoGR8AG2LNQyo5sy1q7hFNo8ojbRpHNBluBmCJa+/w//OWaehxolgA==
|
||||||
|
|
||||||
|
"@types/cytoscape@^3.19.16":
|
||||||
|
version "3.19.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.16.tgz#c54ad4ff5c53a0046f42d1b08a11e730ad83dcc1"
|
||||||
|
integrity sha512-A3zkjaZ6cOGyqEvrVuC1YUgiRSJhDZOj8Qhd1ALH2/+YxH2za1BOmR4RWQsKYHsc+aMP/IWoqg1COuUbZ39t/g==
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.3":
|
"@types/eslint-scope@^3.7.3":
|
||||||
version "3.7.4"
|
version "3.7.4"
|
||||||
resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"
|
resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz"
|
||||||
@ -4192,6 +4205,13 @@ cypress@^13.4.0:
|
|||||||
untildify "^4.0.0"
|
untildify "^4.0.0"
|
||||||
yauzl "^2.10.0"
|
yauzl "^2.10.0"
|
||||||
|
|
||||||
|
cytoscape-popper@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cytoscape-popper/-/cytoscape-popper-2.0.0.tgz#d93917695a9b8af3dbda1d8ee433618ac4d4e359"
|
||||||
|
integrity sha512-b7WSOn8qXHWtdIXFNmrgc8qkaOs16tMY0EwtRXlxzvn8X+al6TAFrUwZoYATkYSlotfd/36ZMoeKMEoUck6feA==
|
||||||
|
dependencies:
|
||||||
|
"@popperjs/core" "^2.0.0"
|
||||||
|
|
||||||
cytoscape@^3.26.0:
|
cytoscape@^3.26.0:
|
||||||
version "3.26.0"
|
version "3.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.26.0.tgz#b4c6961445fd51e1fd3cca83c3ffe924d9a8abc9"
|
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.26.0.tgz#b4c6961445fd51e1fd3cca83c3ffe924d9a8abc9"
|
||||||
@ -10063,10 +10083,10 @@ typedarray-to-buffer@^3.1.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-typedarray "^1.0.0"
|
is-typedarray "^1.0.0"
|
||||||
|
|
||||||
typescript@^4.4.2:
|
typescript@^5.2.2:
|
||||||
version "4.9.3"
|
version "5.3.2"
|
||||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43"
|
||||||
integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==
|
integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==
|
||||||
|
|
||||||
unbox-primitive@^1.0.2:
|
unbox-primitive@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user