Merge branch 'graph' into dev
This commit is contained in:
commit
c9f9f3a4b0
2
.gitignore
vendored
2
.gitignore
vendored
@ -21,3 +21,5 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|||||||
8
.idea/.gitignore
vendored
8
.idea/.gitignore
vendored
@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/squiz.iml" filepath="$PROJECT_DIR$/.idea/squiz.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
cypress.config.ts
Normal file
10
cypress.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
viewportWidth: 1440,
|
||||||
|
viewportHeight: 900,
|
||||||
|
supportFile: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
52
cypress/e2e/quizPreview.cy.ts
Normal file
52
cypress/e2e/quizPreview.cy.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
describe("Quiz preview", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/list");
|
||||||
|
|
||||||
|
cy.get("[data-cy=create-quiz]").click();
|
||||||
|
cy.get("[data-cy=create-quiz-card]").click();
|
||||||
|
cy.get("[data-cy=select-quiz-layout-standard]").click();
|
||||||
|
cy.get("[data-cy=setup-questions]").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("container and layout elements should exist", () => {
|
||||||
|
cy.get("[data-cy=quiz-preview-container]").should("exist");
|
||||||
|
cy.get("[data-cy=quiz-preview-layout]").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Variant question", () => {
|
||||||
|
beforeEach(function fillTitleAndOptions() {
|
||||||
|
cy.get("[data-cy=expand-question]").click();
|
||||||
|
cy.get("[data-cy=select-questiontype-variant]").click();
|
||||||
|
cy.get("[data-cy=quiz-question-card]").eq(0).within(() => {
|
||||||
|
cy.get("[data-cy=quiz-question-title]").type("Question Title");
|
||||||
|
cy.get("[data-cy=quiz-variant-question-answer]").eq(0).type("Answer 1{enter}");
|
||||||
|
cy.get("[data-cy=quiz-variant-question-answer]").eq(1).type("Answer 2{enter}");
|
||||||
|
cy.get("[data-cy=quiz-variant-question-answer]").eq(2).type("Answer 3");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should contain title and options, and be selected properly", () => {
|
||||||
|
cy.get("[data-cy=quiz-preview-layout]").within(() => {
|
||||||
|
cy.get("[data-cy=variant-title]").should("have.text", "Question Title");
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(0).should("have.text", "Answer 1");
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(1).should("have.text", "Answer 2");
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(2).should("have.text", "Answer 3");
|
||||||
|
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(0).click();
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(0).should("be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(1).should("not.be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(2).should("not.be.checked");
|
||||||
|
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(1).click();
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(0).should("not.be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(1).should("be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(2).should("not.be.checked");
|
||||||
|
|
||||||
|
cy.get("[data-cy=variant-answer]").eq(2).click();
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(0).should("not.be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(1).should("not.be.checked");
|
||||||
|
cy.get("[data-cy=variant-radio]").eq(2).should("be.checked");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
cypress/tsconfig.json
Normal file
12
cypress/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"types": [
|
||||||
|
"cypress"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../node_modules/cypress",
|
||||||
|
"./**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
31358
package-lock.json
generated
31358
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -49,23 +49,14 @@
|
|||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject"
|
"eject": "craco eject",
|
||||||
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
"react-app/jest"
|
"react-app/jest"
|
||||||
],
|
]
|
||||||
"rules": {
|
|
||||||
"quotes": [
|
|
||||||
"warn",
|
|
||||||
"double",
|
|
||||||
{
|
|
||||||
"avoidEscape": true,
|
|
||||||
"allowTemplateLiterals": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionInitial, "id"> = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
back: "",
|
back: "",
|
||||||
|
originalBack: "",
|
||||||
autofill: false,
|
autofill: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id"> = {
|
|||||||
{
|
{
|
||||||
answer: "",
|
answer: "",
|
||||||
extendedText: "",
|
extendedText: "",
|
||||||
|
hints: ""
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id"> = {
|
|||||||
{
|
{
|
||||||
answer: "",
|
answer: "",
|
||||||
extendedText: "",
|
extendedText: "",
|
||||||
|
originalImageUrl: "",
|
||||||
|
hints: ""
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
largeCheck: false,
|
largeCheck: false,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id"> = {
|
|||||||
innerName: "",
|
innerName: "",
|
||||||
text: "",
|
text: "",
|
||||||
picture: "",
|
picture: "",
|
||||||
|
originalPicture: "",
|
||||||
video: "",
|
video: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,6 @@ export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id"> = {
|
|||||||
innerNameCheck: false,
|
innerNameCheck: false,
|
||||||
innerName: "",
|
innerName: "",
|
||||||
default: "",
|
default: "",
|
||||||
variants: [{ answer: "", extendedText: "" }],
|
variants: [{ answer: "", extendedText: "", hints: "" }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,6 @@ export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id"> = {
|
|||||||
innerNameCheck: false,
|
innerNameCheck: false,
|
||||||
required: false,
|
required: false,
|
||||||
innerName: "",
|
innerName: "",
|
||||||
variants: [{ answer: "", extendedText: "" }],
|
variants: [{ answer: "", extendedText: "", hints: "" }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id"> = {
|
|||||||
innerNameCheck: false,
|
innerNameCheck: false,
|
||||||
innerName: "",
|
innerName: "",
|
||||||
required: false,
|
required: false,
|
||||||
variants: [{ answer: "", extendedText: "" }],
|
variants: [{ answer: "", hints: "", extendedText: "", originalImageUrl: "" }],
|
||||||
largeCheck: false,
|
largeCheck: false,
|
||||||
replText: "",
|
replText: "",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import lightTheme from "./utils/themes/light";
|
import lightTheme from "./utils/themes/light";
|
||||||
import { ThemeProvider } from "@mui/material";
|
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||||
import StartPage from "./pages/startPage/StartPage";
|
import StartPage from "./pages/startPage/StartPage";
|
||||||
import Main from "./pages/main";
|
import Main from "./pages/main";
|
||||||
import QuestionsPage from "./pages/Questions/QuestionsPage";
|
import QuestionsPage from "./pages/Questions/QuestionsPage";
|
||||||
@ -50,6 +50,7 @@ root.render(
|
|||||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
|
<CssBaseline />
|
||||||
<ContactFormModal />
|
<ContactFormModal />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface QuizQuestionDate extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface QuizQuestionEmoji extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,5 +29,6 @@ export interface QuizQuestionFile extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,36 @@
|
|||||||
import type {
|
import type {
|
||||||
QuizQuestionBase,
|
ImageQuestionVariant,
|
||||||
QuestionVariant,
|
QuestionBranchingRule,
|
||||||
QuestionHint,
|
QuestionHint,
|
||||||
QuestionBranchingRule,
|
QuizQuestionBase
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
export interface QuizQuestionImages extends QuizQuestionBase {
|
export interface QuizQuestionImages extends QuizQuestionBase {
|
||||||
type: "images";
|
type: "images";
|
||||||
content: {
|
content: {
|
||||||
/** Чекбокс "Вариант "свой ответ"" */
|
/** Чекбокс "Вариант "свой ответ"" */
|
||||||
own: boolean;
|
own: boolean;
|
||||||
/** Чекбокс "Можно несколько" */
|
/** Чекбокс "Можно несколько" */
|
||||||
multi: boolean;
|
multi: boolean;
|
||||||
/** Пропорции */
|
/** Пропорции */
|
||||||
xy: "1:1" | "1:2" | "2:1";
|
xy: "1:1" | "1:2" | "2:1";
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
innerNameCheck: boolean;
|
innerNameCheck: boolean;
|
||||||
/** Поле "Внутреннее название вопроса" */
|
/** Поле "Внутреннее название вопроса" */
|
||||||
innerName: string;
|
innerName: string;
|
||||||
/** Чекбокс "Большие картинки" */
|
/** Чекбокс "Большие картинки" */
|
||||||
large: boolean;
|
large: boolean;
|
||||||
/** Форма */
|
/** Форма */
|
||||||
format: "carousel" | "masonry";
|
format: "carousel" | "masonry";
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Варианты (картинки) */
|
/** Варианты (картинки) */
|
||||||
variants: QuestionVariant[];
|
variants: ImageQuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
autofill: boolean;
|
originalBack: string;
|
||||||
largeCheck: boolean;
|
autofill: boolean;
|
||||||
};
|
largeCheck: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export interface QuizQuestionNumber extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
|
form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,10 +13,12 @@ export interface QuizQuestionPage extends QuizQuestionBase {
|
|||||||
innerName: string;
|
innerName: string;
|
||||||
text: string;
|
text: string;
|
||||||
picture: string;
|
picture: string;
|
||||||
|
originalPicture: string;
|
||||||
video: string;
|
video: string;
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export interface QuizQuestionRating extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
/** Позитивное описание рейтинга */
|
/** Позитивное описание рейтинга */
|
||||||
ratingPositiveDescription: string;
|
ratingPositiveDescription: string;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface QuizQuestionSelect extends QuizQuestionBase {
|
|||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,45 +11,53 @@ import type { QuizQuestionVariant } from "./variant";
|
|||||||
import type { QuizQuestionVarImg } from "./varimg";
|
import type { QuizQuestionVarImg } from "./varimg";
|
||||||
|
|
||||||
export interface QuestionBranchingRule {
|
export interface QuestionBranchingRule {
|
||||||
/** Радиокнопка "Все условия обязательны" */
|
/** Радиокнопка "Все условия обязательны" */
|
||||||
or: boolean;
|
or: boolean;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
reqs: {
|
reqs: {
|
||||||
id: string;
|
id: string;
|
||||||
/** Список выбранных вариантов */
|
/** Список выбранных вариантов */
|
||||||
vars: number[];
|
vars: number[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionHint {
|
export interface QuestionHint {
|
||||||
/** Текст подсказки */
|
/** Текст подсказки */
|
||||||
text: string;
|
text: string;
|
||||||
/** URL видео подсказки */
|
/** URL видео подсказки */
|
||||||
video: string;
|
video: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuestionVariant = {
|
export type QuestionVariant = {
|
||||||
/** Текст */
|
/** Текст */
|
||||||
answer: string;
|
answer: string;
|
||||||
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
/** Текст подсказки */
|
||||||
extendedText: string;
|
hints: string;
|
||||||
|
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
||||||
|
extendedText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ImageQuestionVariant extends QuestionVariant {
|
||||||
|
/** Оригинал изображения (до кропа) */
|
||||||
|
originalImageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuizQuestionBase {
|
export interface QuizQuestionBase {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
deleteTimeoutId: number;
|
deleteTimeoutId: number;
|
||||||
content: {
|
content: {
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
autofill: boolean;
|
originalBack: string;
|
||||||
};
|
autofill: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuizQuestionInitial extends QuizQuestionBase {
|
export interface QuizQuestionInitial extends QuizQuestionBase {
|
||||||
@ -57,18 +65,18 @@ export interface QuizQuestionInitial extends QuizQuestionBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AnyQuizQuestion =
|
export type AnyQuizQuestion =
|
||||||
| QuizQuestionVariant
|
| QuizQuestionVariant
|
||||||
| QuizQuestionImages
|
| QuizQuestionImages
|
||||||
| QuizQuestionVarImg
|
| QuizQuestionVarImg
|
||||||
| QuizQuestionEmoji
|
| QuizQuestionEmoji
|
||||||
| QuizQuestionText
|
| QuizQuestionText
|
||||||
| QuizQuestionSelect
|
| QuizQuestionSelect
|
||||||
| QuizQuestionDate
|
| QuizQuestionDate
|
||||||
| QuizQuestionNumber
|
| QuizQuestionNumber
|
||||||
| QuizQuestionFile
|
| QuizQuestionFile
|
||||||
| QuizQuestionPage
|
| QuizQuestionPage
|
||||||
| QuizQuestionRating
|
| QuizQuestionRating
|
||||||
| QuizQuestionInitial;
|
| QuizQuestionInitial;
|
||||||
|
|
||||||
export type QuizQuestionType = AnyQuizQuestion["type"];
|
export type QuizQuestionType = AnyQuizQuestion["type"];
|
||||||
|
|
||||||
|
|||||||
@ -20,5 +20,6 @@ export interface QuizQuestionText extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
import type {
|
import type {
|
||||||
QuizQuestionBase,
|
ImageQuestionVariant,
|
||||||
QuestionVariant,
|
QuestionBranchingRule,
|
||||||
QuestionHint,
|
QuestionHint,
|
||||||
QuestionBranchingRule,
|
QuizQuestionBase
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||||
type: "varimg";
|
type: "varimg";
|
||||||
content: {
|
content: {
|
||||||
/** Чекбокс "Вариант "свой ответ"" */
|
/** Чекбокс "Вариант "свой ответ"" */
|
||||||
own: boolean;
|
own: boolean;
|
||||||
/** Чекбокс "Внутреннее название вопроса" */
|
/** Чекбокс "Внутреннее название вопроса" */
|
||||||
innerNameCheck: boolean;
|
innerNameCheck: boolean;
|
||||||
/** Поле "Внутреннее название вопроса" */
|
/** Поле "Внутреннее название вопроса" */
|
||||||
innerName: string;
|
innerName: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
variants: QuestionVariant[];
|
variants: ImageQuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
autofill: boolean;
|
originalBack: string;
|
||||||
largeCheck: boolean;
|
autofill: boolean;
|
||||||
replText: string;
|
largeCheck: boolean;
|
||||||
};
|
replText: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/mui.d.ts
vendored
11
src/mui.d.ts
vendored
@ -9,7 +9,7 @@ declare module "@mui/material/styles" {
|
|||||||
grey1: Palette["primary"],
|
grey1: Palette["primary"],
|
||||||
grey2: Palette["primary"],
|
grey2: Palette["primary"],
|
||||||
grey3: Palette["primary"],
|
grey3: Palette["primary"],
|
||||||
grey4: Palette ["primary"],
|
grey4: Palette["primary"],
|
||||||
orange: Palette["primary"],
|
orange: Palette["primary"],
|
||||||
navbarbg: Palette["primary"],
|
navbarbg: Palette["primary"],
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ declare module "@mui/material/styles" {
|
|||||||
grey1?: PaletteOptions["primary"],
|
grey1?: PaletteOptions["primary"],
|
||||||
grey2?: PaletteOptions["primary"],
|
grey2?: PaletteOptions["primary"],
|
||||||
grey3?: PaletteOptions["primary"],
|
grey3?: PaletteOptions["primary"],
|
||||||
grey4?: PaletteOptions ["primary"],
|
grey4?: PaletteOptions["primary"],
|
||||||
orange?: PaletteOptions["primary"],
|
orange?: PaletteOptions["primary"],
|
||||||
navbarbg?: PaletteOptions["primary"],
|
navbarbg?: PaletteOptions["primary"],
|
||||||
}
|
}
|
||||||
@ -41,3 +41,10 @@ declare module "@mui/material/Typography" {
|
|||||||
p1: true;
|
p1: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DataAttributeKey = `data-${string}`;
|
||||||
|
declare module 'react' {
|
||||||
|
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
|
||||||
|
[dataAttribute: DataAttributeKey]: unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Popover,
|
||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@ -16,8 +17,11 @@ import { questionStore, updateQuestionsList } from "@root/questions";
|
|||||||
|
|
||||||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
import { MessageIcon } from "@icons/messagIcon";
|
||||||
|
import TextareaAutosize from "@mui/base/TextareaAutosize";
|
||||||
|
import type { ChangeEvent, KeyboardEvent, ReactNode } from "react";
|
||||||
|
|
||||||
|
|
||||||
import type { KeyboardEvent, ReactNode } from "react";
|
|
||||||
import type { DroppableProvided } from "react-beautiful-dnd";
|
import type { DroppableProvided } from "react-beautiful-dnd";
|
||||||
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||||
@ -49,7 +53,6 @@ export const AnswerItem = ({
|
|||||||
const debounced = useDebouncedCallback((value) => {
|
const debounced = useDebouncedCallback((value) => {
|
||||||
const answerNew = variants.slice();
|
const answerNew = variants.slice();
|
||||||
answerNew[index].answer = value;
|
answerNew[index].answer = value;
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||||
content: {
|
content: {
|
||||||
...question.content,
|
...question.content,
|
||||||
@ -58,9 +61,23 @@ export const AnswerItem = ({
|
|||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
console.log(provided)
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const addNewAnswer = () => {
|
const addNewAnswer = () => {
|
||||||
const answerNew = variants.slice();
|
const answerNew = variants.slice();
|
||||||
answerNew.push({ answer: "", extendedText: "" });
|
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
@ -76,79 +93,125 @@ export const AnswerItem = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeAnswerHint = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const answerNew = variants.slice();
|
||||||
|
answerNew[index].hints = event.target.value;
|
||||||
|
|
||||||
|
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||||
|
content: { ...question.content, variants: answerNew },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Draggable draggableId={String(index)} index={index}>
|
||||||
<FormControl
|
{(provided) => (
|
||||||
key={index}
|
<Box ref={provided.innerRef} {...provided.draggableProps}>
|
||||||
fullWidth
|
<FormControl
|
||||||
variant="standard"
|
key={index}
|
||||||
sx={{
|
|
||||||
margin: isTablet ? " 15px 0 20px 0" : "0 0 20px 0",
|
|
||||||
borderRadius: "10px",
|
|
||||||
border: "1px solid rgba(0, 0, 0, 0.23)",
|
|
||||||
background: "white",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
defaultValue={variant.answer}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
focused={false}
|
variant="standard"
|
||||||
placeholder={"Добавьте ответ"}
|
|
||||||
multiline={question.content.largeCheck}
|
|
||||||
onChange={({ target }) => debounced(target.value)}
|
|
||||||
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.code === "Enter" && !question.content.largeCheck) {
|
|
||||||
addNewAnswer();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<>
|
|
||||||
<InputAdornment {...provided.droppableProps} position="start">
|
|
||||||
<PointsIcon style={{ color: "#9A9AAF", fontSize: "30px" }} />
|
|
||||||
</InputAdornment>
|
|
||||||
{additionalContent}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton sx={{ padding: "0" }} onClick={deleteAnswer}>
|
|
||||||
<DeleteIcon
|
|
||||||
style={{
|
|
||||||
color: theme.palette.grey2.main,
|
|
||||||
marginRight: "-1px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiInputBase-root": {
|
margin: isTablet ? " 15px 0 20px 0" : "0 0 20px 0",
|
||||||
padding: additionalContent
|
borderRadius: "10px",
|
||||||
? isTablet
|
border: "1px solid rgba(0, 0, 0, 0.23)",
|
||||||
? "13px"
|
background: "white",
|
||||||
: "5px 13px"
|
|
||||||
: "13px",
|
|
||||||
borderRadius: "10px",
|
|
||||||
background: "#ffffff",
|
|
||||||
"& input.MuiInputBase-input": {
|
|
||||||
height: "22px",
|
|
||||||
},
|
|
||||||
"& textarea.MuiInputBase-input": {
|
|
||||||
marginTop: "1px",
|
|
||||||
},
|
|
||||||
"& .MuiOutlinedInput-notchedOutline": {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
inputProps={{
|
>
|
||||||
sx: { fontSize: "18px", lineHeight: "21px", py: 0, ml: "13px" },
|
<TextField
|
||||||
}}
|
defaultValue={variant.answer}
|
||||||
/>
|
fullWidth
|
||||||
{additionalMobile}
|
focused={false}
|
||||||
</FormControl>
|
placeholder={"Добавьте ответ"}
|
||||||
</Box>
|
multiline={question.content.largeCheck}
|
||||||
|
onChange={({ target }) => debounced(target.value)}
|
||||||
|
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.code === "Enter" && !question.content.largeCheck) {
|
||||||
|
addNewAnswer();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<>
|
||||||
|
<InputAdornment
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
position="start"
|
||||||
|
>
|
||||||
|
<PointsIcon
|
||||||
|
style={{ color: "#9A9AAF", fontSize: "30px" }}
|
||||||
|
/>
|
||||||
|
</InputAdornment>
|
||||||
|
{additionalContent}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
sx={{ padding: "0" }}
|
||||||
|
aria-describedby="my-popover-id"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<MessageIcon
|
||||||
|
style={{
|
||||||
|
color: "#9A9AAF",
|
||||||
|
fontSize: "30px",
|
||||||
|
marginRight: "6.5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
id="my-popover-id"
|
||||||
|
open={isOpen}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||||
|
>
|
||||||
|
<TextareaAutosize
|
||||||
|
style={{ margin: "10px" }}
|
||||||
|
placeholder="Подсказка для этого ответа"
|
||||||
|
value={variant.hints}
|
||||||
|
onChange={changeAnswerHint}
|
||||||
|
onKeyDown={(
|
||||||
|
event: KeyboardEvent<HTMLTextAreaElement>
|
||||||
|
) => event.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<IconButton sx={{ padding: "0" }} onClick={deleteAnswer}>
|
||||||
|
<DeleteIcon
|
||||||
|
style={{
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
marginRight: "-1px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
padding: additionalContent ? "5px 13px" : "13px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: "#ffffff",
|
||||||
|
"& input.MuiInputBase-input": {
|
||||||
|
height: "22px",
|
||||||
|
},
|
||||||
|
"& textarea.MuiInputBase-input": {
|
||||||
|
marginTop: "1px",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: { fontSize: "18px", lineHeight: "21px", py: 0, ml: "13px" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{additionalMobile}
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,21 +4,21 @@ import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
|||||||
|
|
||||||
import { AnswerItem } from "./AnswerItem";
|
import { AnswerItem } from "./AnswerItem";
|
||||||
|
|
||||||
import { updateVariants } from "@root/questions";
|
import { reorderVariants } from "@root/questions";
|
||||||
|
|
||||||
import { reorder } from "./helper";
|
import { type ReactNode } from "react";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import type { DropResult } from "react-beautiful-dnd";
|
import type { DropResult } from "react-beautiful-dnd";
|
||||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
import type { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
||||||
|
|
||||||
type AnswerDraggableListProps = {
|
type AnswerDraggableListProps = {
|
||||||
variants: QuestionVariant[];
|
variants: QuestionVariant[];
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
additionalContent?: (variant: QuestionVariant, index: number) => ReactNode;
|
additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
|
||||||
additionalMobile?: (variant: QuestionVariant, index: number) => ReactNode;
|
additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const AnswerDraggableList = ({
|
export const AnswerDraggableList = ({
|
||||||
variants,
|
variants,
|
||||||
totalIndex,
|
totalIndex,
|
||||||
@ -29,9 +29,7 @@ export const AnswerDraggableList = ({
|
|||||||
|
|
||||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
const newItems = reorder(variants, source.index, destination.index);
|
reorderVariants(quizId, totalIndex, source.index, destination.index);
|
||||||
|
|
||||||
updateVariants(quizId, totalIndex, newItems);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -171,6 +171,7 @@ export default function QuestionsPageCard({
|
|||||||
<>
|
<>
|
||||||
<Paper
|
<Paper
|
||||||
id={String(totalIndex)}
|
id={String(totalIndex)}
|
||||||
|
data-cy="quiz-question-card"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: "796px",
|
maxWidth: "796px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -248,6 +249,7 @@ export default function QuestionsPageCard({
|
|||||||
py: 0,
|
py: 0,
|
||||||
paddingLeft: question.type.length === 0 ? 0 : "18px",
|
paddingLeft: question.type.length === 0 ? 0 : "18px",
|
||||||
},
|
},
|
||||||
|
"data-cy": "quiz-question-title",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -263,6 +265,7 @@ export default function QuestionsPageCard({
|
|||||||
<IconButton
|
<IconButton
|
||||||
sx={{ padding: "0", margin: "5px" }}
|
sx={{ padding: "0", margin: "5px" }}
|
||||||
disableRipple
|
disableRipple
|
||||||
|
data-cy="expand-question"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, {
|
||||||
expanded: !question.expanded,
|
expanded: !question.expanded,
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function DropDown({ totalIndex }: Props) {
|
|||||||
|
|
||||||
const addNewAnswer = () => {
|
const addNewAnswer = () => {
|
||||||
const answerNew = question.content.variants.slice();
|
const answerNew = question.content.variants.slice();
|
||||||
answerNew.push({ answer: "", extendedText: "" });
|
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
|
|||||||
@ -213,7 +213,7 @@ export default function Emoji({ totalIndex }: Props) {
|
|||||||
sx={{ color: theme.palette.brightPurple.main }}
|
sx={{ color: theme.palette.brightPurple.main }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const answerNew = question.content.variants.slice();
|
const answerNew = question.content.variants.slice();
|
||||||
answerNew.push({ answer: "", extendedText: "" });
|
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
@ -12,270 +11,361 @@ import {
|
|||||||
TextareaAutosize,
|
TextareaAutosize,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
|
|
||||||
import AddImage from "@icons/questionsPage/addImage";
|
import { ImageAddIcons } from "@icons/ImageAddIcons";
|
||||||
import Image from "@icons/questionsPage/image";
|
import { MessageIcon } from "@icons/messagIcon";
|
||||||
|
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||||||
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
import { questionStore, setVariantImageUrl, setVariantOriginalImageUrl, updateQuestionsList } from "@root/questions";
|
||||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||||
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
||||||
import SwitchOptionsAndPict from "./switchOptionsAndPict";
|
import SwitchOptionsAndPict from "./switchOptionsAndPict";
|
||||||
import React from "react";
|
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
import { ImageAddIcons } from "@icons/ImageAddIcons";
|
|
||||||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
|
||||||
import { MessageIcon } from "@icons/messagIcon";
|
|
||||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
|
||||||
import PlusImage from "@icons/questionsPage/plus";
|
|
||||||
|
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
|
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
|
||||||
import { produce } from "immer";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsAndPicture({ totalIndex }: Props) {
|
export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||||
const [open, setOpen] = useState(false);
|
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
const quizId = Number(useParams().quizId);
|
||||||
const quizId = Number(useParams().quizId);
|
const { listQuestions } = questionStore();
|
||||||
const { listQuestions } = questionStore();
|
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 question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg;
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionVarImg;
|
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadImage = (files: FileList | null) => {
|
const handleImageUpload = (files: FileList | null) => {
|
||||||
if (files?.length) {
|
if (!files?.length) return;
|
||||||
const [file] = Array.from(files);
|
|
||||||
|
|
||||||
const clonedContent = { ...question.content };
|
const [file] = Array.from(files);
|
||||||
clonedContent.variants[currentIndex].extendedText =
|
const url = URL.createObjectURL(file);
|
||||||
URL.createObjectURL(file);
|
|
||||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOpen(false);
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
setOpened(true);
|
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
|
setIsUploadImageModalOpen(false);
|
||||||
|
openCropModal(url, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
totalIndex={totalIndex}
|
||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
sx={{ cursor: "pointer" }}
|
imageSrc={variant.extendedText}
|
||||||
onClick={() => {
|
onImageClick={() => {
|
||||||
setCurrentIndex(index);
|
if (!("originalImageUrl" in variant)) return;
|
||||||
setOpen(true);
|
|
||||||
}}
|
setCurrentIndex(index);
|
||||||
>
|
if (variant.extendedText) return openCropModal(
|
||||||
{variant.extendedText ? (
|
variant.extendedText,
|
||||||
<Box
|
variant.originalImageUrl
|
||||||
sx={{
|
);
|
||||||
overflow: "hidden",
|
|
||||||
width: "60px",
|
setIsUploadImageModalOpen(true);
|
||||||
display: "flex",
|
}}
|
||||||
alignItems: "center",
|
onPlusClick={() => {
|
||||||
background: "#EEE4FC",
|
setCurrentIndex(index);
|
||||||
borderRadius: "3px",
|
setIsUploadImageModalOpen(true);
|
||||||
margin: "0 10px",
|
}}
|
||||||
height: "40px",
|
sx={{ mx: "10px" }}
|
||||||
}}
|
/>
|
||||||
>
|
)}
|
||||||
<Box sx={{ display: "flex", width: "40px" }}>
|
</>
|
||||||
<img
|
|
||||||
src={variant.extendedText}
|
|
||||||
alt=""
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<PlusImage />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Button component="label" sx={{ padding: "0px" }}>
|
|
||||||
<AddImage
|
|
||||||
sx={{
|
|
||||||
height: "40px",
|
|
||||||
width: "60px",
|
|
||||||
margin: "0 10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
additionalMobile={(variant, index) => (
|
||||||
)}
|
<>
|
||||||
additionalMobile={(variant, index) => (
|
{isMobile && (
|
||||||
<>
|
<AddOrEditImageButton
|
||||||
{isMobile && (
|
imageSrc={variant.extendedText}
|
||||||
<Box
|
onImageClick={() => {
|
||||||
onClick={() => {
|
if (!("originalImageUrl" in variant)) return;
|
||||||
setCurrentIndex(index);
|
|
||||||
setOpen(true);
|
setCurrentIndex(index);
|
||||||
|
if (variant.extendedText) return openCropModal(
|
||||||
|
variant.extendedText,
|
||||||
|
variant.originalImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsUploadImageModalOpen(true);
|
||||||
|
}}
|
||||||
|
onPlusClick={() => {
|
||||||
|
setCurrentIndex(index);
|
||||||
|
setIsUploadImageModalOpen(true);
|
||||||
|
}}
|
||||||
|
sx={{ m: "8px", width: "auto" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<UploadImageModal
|
||||||
|
open={isUploadImageModalOpen}
|
||||||
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
|
imgHC={handleImageUpload}
|
||||||
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
border: "1px solid #9A9AAF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
display: isTablet ? "block" : "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
focused={false}
|
||||||
|
placeholder={"Добавьте ответ"}
|
||||||
|
multiline={question.content.largeCheck}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<>
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<PointsIcon
|
||||||
|
style={{ color: "#9A9AAF", fontSize: "30px" }}
|
||||||
|
/>
|
||||||
|
</InputAdornment>
|
||||||
|
{!isMobile && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "60px",
|
||||||
|
height: "40px",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginRight: "20px",
|
||||||
|
marginLeft: "12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageAddIcons fontSize="22px" color="#7E2AEA" />
|
||||||
|
</Box>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "#7E2AEA",
|
||||||
|
height: "100%",
|
||||||
|
width: "25px",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
sx={{ padding: "0" }}
|
||||||
|
aria-describedby="my-popover-id"
|
||||||
|
>
|
||||||
|
<MessageIcon
|
||||||
|
style={{
|
||||||
|
color: "#9A9AAF",
|
||||||
|
fontSize: "30px",
|
||||||
|
marginRight: "6.5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Popover
|
||||||
|
id="my-popover-id"
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||||
|
open={false}
|
||||||
|
>
|
||||||
|
<TextareaAutosize
|
||||||
|
style={{ margin: "10px" }}
|
||||||
|
placeholder="Подсказка для этого ответа"
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<IconButton sx={{ padding: "0" }}>
|
||||||
|
<DeleteIcon
|
||||||
|
style={{
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
marginRight: "-1px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
overflow: "hidden",
|
"& .MuiInputBase-root": {
|
||||||
display: "flex",
|
padding: "13.5px",
|
||||||
alignItems: "center",
|
borderRadius: "10px",
|
||||||
m: "8px",
|
background: "#ffffff",
|
||||||
position: "relative",
|
height: "48px",
|
||||||
borderRadius: "3px",
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
inputProps={{
|
||||||
|
sx: { fontSize: "18px", lineHeight: "21px", py: 0 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{variant.extendedText ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "40px",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
background: "#EEE4FC",
|
m: "8px",
|
||||||
height: "30px",
|
position: "relative",
|
||||||
borderRadius: "3px",
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={variant.extendedText}
|
|
||||||
alt=""
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Button component="label" sx={{ padding: "0px" }}>
|
|
||||||
<Image
|
|
||||||
sx={{
|
|
||||||
height: "40px",
|
|
||||||
width: "60px",
|
|
||||||
margin: "0 10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "20px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
color: "white",
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
+
|
<Box
|
||||||
|
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<ImageAddIcons
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "20px",
|
||||||
|
left: "45%",
|
||||||
|
right: "55%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "20px",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
height: "40px",
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<ImageAddIcons
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "20px",
|
||||||
|
left: "45%",
|
||||||
|
right: "55%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "20px",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
height: "40px",
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</Box>
|
||||||
)}
|
<Box
|
||||||
/>
|
sx={{
|
||||||
<UploadImageModal
|
display: "flex",
|
||||||
open={open}
|
alignItems: "center",
|
||||||
onClose={() => setOpen(false)}
|
marginBottom: "17px",
|
||||||
imgHC={uploadImage}
|
}}
|
||||||
/>
|
|
||||||
<CropModal
|
|
||||||
opened={opened}
|
|
||||||
onClose={() => setOpened(false)}
|
|
||||||
picture={question.content.variants[currentIndex]?.extendedText}
|
|
||||||
onCropPress={(url) => {
|
|
||||||
const content = produce(question.content, (draft) => {
|
|
||||||
draft.variants[currentIndex].extendedText = url;
|
|
||||||
});
|
|
||||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "17px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
fontWeight: "400",
|
|
||||||
fontSize: "16px",
|
|
||||||
mr: "4px",
|
|
||||||
height: "19px",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
const clonedContent = { ...question.content };
|
|
||||||
clonedContent.variants.push({
|
|
||||||
answer: "",
|
|
||||||
extendedText: "",
|
|
||||||
});
|
|
||||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Добавьте ответ
|
<Link
|
||||||
</Link>
|
component="button"
|
||||||
{isMobile ? null : (
|
variant="body2"
|
||||||
<>
|
sx={{
|
||||||
<Typography
|
color: theme.palette.brightPurple.main,
|
||||||
sx={{
|
fontWeight: "400",
|
||||||
fontWeight: 400,
|
fontSize: "16px",
|
||||||
lineHeight: "21.33px",
|
mr: "4px",
|
||||||
color: theme.palette.grey2.main,
|
height: "19px",
|
||||||
fontSize: "16px",
|
}}
|
||||||
}}
|
onClick={() => {
|
||||||
|
const clonedContent = { ...question.content };
|
||||||
|
clonedContent.variants.push({
|
||||||
|
answer: "",
|
||||||
|
hints: "",
|
||||||
|
extendedText: "",
|
||||||
|
originalImageUrl: "",
|
||||||
|
});
|
||||||
|
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
||||||
|
content: clonedContent,
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
или нажмите Enter
|
Добавьте ответ
|
||||||
</Typography>
|
</Link>
|
||||||
<EnterIcon
|
{isMobile ? null : (
|
||||||
style={{
|
<>
|
||||||
color: "#7E2AEA",
|
<Typography
|
||||||
fontSize: "24px",
|
sx={{
|
||||||
marginLeft: "6px",
|
fontWeight: 400,
|
||||||
}}
|
lineHeight: "21.33px",
|
||||||
/>
|
color: theme.palette.grey2.main,
|
||||||
</>
|
fontSize: "16px",
|
||||||
)}
|
}}
|
||||||
</Box>
|
>
|
||||||
|
или нажмите Enter
|
||||||
|
</Typography>
|
||||||
|
<EnterIcon
|
||||||
|
style={{
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "24px",
|
||||||
|
marginLeft: "6px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptionsAndPict
|
<ButtonsOptionsAndPict
|
||||||
switchState={switchState}
|
switchState={switchState}
|
||||||
SSHC={SSHC}
|
SSHC={SSHC}
|
||||||
totalIndex={totalIndex}
|
totalIndex={totalIndex}
|
||||||
/>
|
/>
|
||||||
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} />
|
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} />
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
@ -8,251 +6,167 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import ButtonsOptions from "../ButtonsOptions";
|
import { questionStore, setVariantImageUrl, updateQuestionsList, setVariantOriginalImageUrl } from "@root/questions";
|
||||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
|
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||||
|
import ButtonsOptions from "../ButtonsOptions";
|
||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
|
|
||||||
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
|
||||||
import AddImage from "../../../assets/icons/questionsPage/addImage";
|
|
||||||
import Image from "../../../assets/icons/questionsPage/image";
|
|
||||||
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
import SwitchAnswerOptionsPict from "./switchOptionsPict";
|
||||||
import PlusImage from "@icons/questionsPage/plus";
|
|
||||||
|
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||||
import { produce } from "immer";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionsPicture({ totalIndex }: Props) {
|
export default function OptionsPicture({ totalIndex }: Props) {
|
||||||
const [open, setOpen] = useState(false);
|
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
const theme = useTheme();
|
||||||
const theme = useTheme();
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const quizId = Number(useParams().quizId);
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const quizId = Number(useParams().quizId);
|
const { listQuestions } = questionStore();
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
|
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadImage = (files: FileList | null) => {
|
const handleImageUpload = (files: FileList | null) => {
|
||||||
if (files?.length) {
|
if (!files?.length) return;
|
||||||
const [file] = Array.from(files);
|
|
||||||
|
|
||||||
const clonedContent = { ...question.content };
|
const [file] = Array.from(files);
|
||||||
clonedContent.variants[currentIndex].extendedText =
|
const url = URL.createObjectURL(file);
|
||||||
URL.createObjectURL(file);
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOpen(false);
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
setOpened(true);
|
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
}
|
setIsUploadImageModalOpen(false);
|
||||||
};
|
openCropModal(url, url);
|
||||||
|
};
|
||||||
|
|
||||||
const addNewAnswer = () => {
|
const addNewAnswer = () => {
|
||||||
const answerNew = question.content.variants.slice();
|
const answerNew = question.content.variants.slice();
|
||||||
answerNew.push({ answer: "", extendedText: "" });
|
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
function handleCropModalSaveClick(url: string) {
|
||||||
<>
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
<AnswerDraggableList
|
<AnswerDraggableList
|
||||||
variants={question.content.variants}
|
variants={question.content.variants}
|
||||||
totalIndex={totalIndex}
|
totalIndex={totalIndex}
|
||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
sx={{ cursor: "pointer" }}
|
imageSrc={variant.extendedText}
|
||||||
onClick={() => {
|
onImageClick={() => {
|
||||||
setCurrentIndex(index);
|
if (!("originalImageUrl" in variant)) return;
|
||||||
setOpen(true);
|
|
||||||
}}
|
setCurrentIndex(index);
|
||||||
>
|
if (variant.extendedText) {
|
||||||
{variant.extendedText ? (
|
return openCropModal(
|
||||||
<Box
|
variant.extendedText,
|
||||||
sx={{
|
variant.originalImageUrl
|
||||||
overflow: "hidden",
|
);
|
||||||
width: "60px",
|
}
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
setIsUploadImageModalOpen(true);
|
||||||
background: "#EEE4FC",
|
}}
|
||||||
borderRadius: "3px",
|
onPlusClick={() => {
|
||||||
margin: "0 10px",
|
setCurrentIndex(index);
|
||||||
height: "40px",
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
sx={{ mx: "10px" }}
|
||||||
<Box sx={{ display: "flex", width: "40px" }}>
|
/>
|
||||||
<img
|
)}
|
||||||
src={variant.extendedText}
|
</>
|
||||||
alt=""
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<PlusImage />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Button component="label" sx={{ padding: "0px" }}>
|
|
||||||
<AddImage
|
|
||||||
sx={{ height: "40px", width: "60px", margin: "0 10px" }}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
additionalMobile={(variant, index) => (
|
||||||
)}
|
<>
|
||||||
additionalMobile={(variant, index) => (
|
{isMobile && (
|
||||||
<>
|
<AddOrEditImageButton
|
||||||
{isMobile && (
|
imageSrc={variant.extendedText}
|
||||||
<Box
|
onImageClick={() => {
|
||||||
onClick={() => {
|
if (!("originalImageUrl" in variant)) return;
|
||||||
setCurrentIndex(index);
|
|
||||||
setOpen(true);
|
setCurrentIndex(index);
|
||||||
}}
|
if (variant.extendedText) {
|
||||||
sx={{
|
return openCropModal(
|
||||||
overflow: "hidden",
|
variant.extendedText,
|
||||||
display: "flex",
|
variant.originalImageUrl
|
||||||
alignItems: "center",
|
);
|
||||||
m: "8px",
|
}
|
||||||
position: "relative",
|
|
||||||
borderRadius: "3px",
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
onPlusClick={() => {
|
||||||
<Box
|
setCurrentIndex(index);
|
||||||
sx={{
|
setIsUploadImageModalOpen(true);
|
||||||
width: "100%",
|
}}
|
||||||
background: "#EEE4FC",
|
sx={{ m: "8px", width: "auto" }}
|
||||||
height: "40px",
|
/>
|
||||||
display: "flex",
|
)}
|
||||||
alignItems: "center",
|
</>
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{variant.extendedText ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "30px",
|
|
||||||
borderRadius: "3px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={variant.extendedText}
|
|
||||||
alt=""
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Button component="label" sx={{ padding: "0px" }}>
|
|
||||||
<Image
|
|
||||||
sx={{
|
|
||||||
height: "40px",
|
|
||||||
width: "60px",
|
|
||||||
margin: "0 10px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "20px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
color: "white",
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
/>
|
||||||
)}
|
<UploadImageModal
|
||||||
/>
|
open={isUploadImageModalOpen}
|
||||||
<UploadImageModal
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
open={open}
|
imgHC={handleImageUpload}
|
||||||
onClose={() => setOpen(false)}
|
/>
|
||||||
imgHC={uploadImage}
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
/>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<CropModal
|
<Link
|
||||||
opened={opened}
|
component="button"
|
||||||
onClose={() => setOpened(false)}
|
variant="body2"
|
||||||
picture={question.content.variants[currentIndex]?.extendedText}
|
sx={{ color: theme.palette.brightPurple.main }}
|
||||||
onCropPress={(url) => {
|
onClick={addNewAnswer}
|
||||||
const content = produce(question.content, (draft) => {
|
|
||||||
draft.variants[currentIndex].extendedText = url;
|
|
||||||
});
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
|
||||||
<Link
|
|
||||||
component="button"
|
|
||||||
variant="body2"
|
|
||||||
sx={{ color: theme.palette.brightPurple.main }}
|
|
||||||
onClick={addNewAnswer}
|
|
||||||
>
|
|
||||||
Добавьте ответ
|
|
||||||
</Link>
|
|
||||||
{isMobile ? null : (
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
fontWeight: 400,
|
|
||||||
lineHeight: "21.33px",
|
|
||||||
color: theme.palette.grey2.main,
|
|
||||||
fontSize: "16px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
или нажмите Enter
|
Добавьте ответ
|
||||||
</Typography>
|
</Link>
|
||||||
<EnterIcon
|
{isMobile ? null : (
|
||||||
style={{
|
<>
|
||||||
color: "#7E2AEA",
|
<Typography
|
||||||
fontSize: "24px",
|
sx={{
|
||||||
marginLeft: "6px",
|
fontWeight: 400,
|
||||||
}}
|
lineHeight: "21.33px",
|
||||||
/>
|
color: theme.palette.grey2.main,
|
||||||
</>
|
fontSize: "16px",
|
||||||
)}
|
}}
|
||||||
</Box>
|
>
|
||||||
|
или нажмите Enter
|
||||||
|
</Typography>
|
||||||
|
<EnterIcon
|
||||||
|
style={{
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "24px",
|
||||||
|
marginLeft: "6px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonsOptions
|
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
|
||||||
switchState={switchState}
|
<SwitchAnswerOptionsPict switchState={switchState} totalIndex={totalIndex} />
|
||||||
SSHC={SSHC}
|
</>
|
||||||
totalIndex={totalIndex}
|
|
||||||
/>
|
|
||||||
<SwitchAnswerOptionsPict
|
|
||||||
switchState={switchState}
|
|
||||||
totalIndex={totalIndex}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,299 +1,244 @@
|
|||||||
|
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
||||||
|
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
import { questionStore, setPageQuestionOriginalPicture, setPageQuestionPicture, updateQuestionsList } from "@root/questions";
|
||||||
|
import CustomTextField from "@ui_kit/CustomTextField";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import ButtonsOptions from "../ButtonsOptions";
|
import ButtonsOptions from "../ButtonsOptions";
|
||||||
import CustomTextField from "@ui_kit/CustomTextField";
|
|
||||||
import AddImage from "../../../assets/icons/questionsPage/addImage";
|
|
||||||
import AddVideofile from "../../../assets/icons/questionsPage/addVideofile";
|
|
||||||
import SwitchPageOptions from "./switchPageOptions";
|
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
|
||||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||||
import { UploadVideoModal } from "../UploadVideoModal";
|
import { UploadVideoModal } from "../UploadVideoModal";
|
||||||
import { AddPlusImage } from "@icons/questionsPage/addPlusImage";
|
import SwitchPageOptions from "./switchPageOptions";
|
||||||
import { AddPlusVideo } from "@icons/questionsPage/addPlusVideo";
|
|
||||||
import { ImageAddIcons } from "@icons/ImageAddIcons";
|
|
||||||
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
|
||||||
|
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||||
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
|
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disableInput?: boolean;
|
disableInput?: boolean;
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PageOptions({ disableInput, totalIndex }: Props) {
|
export default function PageOptions({ disableInput, totalIndex }: Props) {
|
||||||
const [openImageModal, setOpenImageModal] = useState<boolean>(false);
|
const [openImageModal, setOpenImageModal] = useState<boolean>(false);
|
||||||
const [openVideoModal, setOpenVideoModal] = useState<boolean>(false);
|
const [openVideoModal, setOpenVideoModal] = useState<boolean>(false);
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const quizId = Number(useParams().quizId);
|
const quizId = Number(useParams().quizId);
|
||||||
const { listQuestions } = questionStore();
|
const { listQuestions } = questionStore();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
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 question = listQuestions[quizId][totalIndex] as QuizQuestionPage;
|
const question = listQuestions[quizId][totalIndex] as QuizQuestionPage;
|
||||||
const debounced = useDebouncedCallback((value) => {
|
const debounced = useDebouncedCallback((value) => {
|
||||||
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
||||||
content: { ...question.content, text: value },
|
content: { ...question.content, text: value },
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const SSHC = (data: string) => {
|
const SSHC = (data: string) => {
|
||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
function handleImageUpload(fileList: FileList | null) {
|
||||||
<>
|
if (!fileList?.length) return;
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: isTablet ? "auto" : "100%",
|
|
||||||
maxWidth: isFigmaTablet ? "549px" : "640px",
|
|
||||||
display: "flex",
|
|
||||||
px: "20px",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: isMobile ? "25px" : "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: disableInput ? "none" : "", mt: isMobile ? "15px" : "0px" }}>
|
|
||||||
<CustomTextField
|
|
||||||
placeholder={"Можно добавить текст"}
|
|
||||||
text={question.content.text}
|
|
||||||
onChange={({ target }) => debounced(target.value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
const url = URL.createObjectURL(fileList[0]);
|
||||||
sx={{
|
|
||||||
mb: "20px",
|
setPageQuestionPicture(quizId, totalIndex, url);
|
||||||
ml: isTablet ? "0px" : "60px",
|
setPageQuestionOriginalPicture(quizId, totalIndex, url);
|
||||||
display: "flex",
|
setOpenImageModal(false);
|
||||||
alignItems: "center",
|
openCropModal(url, url);
|
||||||
gap: "28px",
|
}
|
||||||
justifyContent: isMobile ? "space-between" : null,
|
|
||||||
}}
|
function handleCropModalSaveClick(url: string) {
|
||||||
>
|
setPageQuestionPicture(quizId, totalIndex, url);
|
||||||
<Box
|
}
|
||||||
onClick={() => setOpenImageModal(true)}
|
|
||||||
sx={{
|
return (
|
||||||
cursor: "pointer",
|
<>
|
||||||
display: "flex",
|
<Box
|
||||||
alignItems: "center",
|
|
||||||
gap: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isMobile ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
width: isTablet ? "auto" : "100%",
|
||||||
alignItems: "center",
|
maxWidth: isFigmaTablet ? "549px" : "640px",
|
||||||
width: "120px",
|
display: "flex",
|
||||||
position: "relative",
|
px: "20px",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: isMobile ? "25px" : "20px",
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderTopLeftRadius: "4px",
|
|
||||||
borderBottomLeftRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ImageAddIcons
|
|
||||||
style={{
|
|
||||||
color: "#7E2AEA",
|
|
||||||
fontSize: "20px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "20px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
color: "white",
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
borderTopRightRadius: "4px",
|
|
||||||
borderBottomRightRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "60px",
|
|
||||||
height: "40px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%" }}>
|
|
||||||
<ImageAddIcons fontSize="22px" color="#7E2AEA" />
|
|
||||||
</Box>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
background: "#7E2AEA",
|
|
||||||
height: "100%",
|
|
||||||
width: "25px",
|
|
||||||
color: "white",
|
|
||||||
fontSize: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</span>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
sx={{
|
|
||||||
display: isMobile ? "none" : "block",
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: "16px",
|
|
||||||
lineHeight: "18.96px",
|
|
||||||
color: theme.palette.grey2.main,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Изображение
|
<Box sx={{ display: disableInput ? "none" : "", mt: isMobile ? "15px" : "0px" }}>
|
||||||
</Typography>
|
<CustomTextField
|
||||||
</Box>
|
placeholder={"Можно добавить текст"}
|
||||||
<UploadImageModal
|
text={question.content.text}
|
||||||
open={openImageModal}
|
onChange={({ target }) => debounced(target.value)}
|
||||||
onClose={() => setOpenImageModal(false)}
|
/>
|
||||||
imgHC={(fileList) => {
|
|
||||||
if (fileList?.length) {
|
|
||||||
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
picture: URL.createObjectURL(fileList[0]),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// onClick={() => setOpenVideoModal(true)}
|
|
||||||
/>
|
|
||||||
<Typography> или</Typography>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isMobile ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "120px",
|
|
||||||
position: "relative",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderTopLeftRadius: "4px",
|
|
||||||
borderBottomLeftRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VideofileIcon
|
|
||||||
style={{
|
|
||||||
color: "#7E2AEA",
|
|
||||||
fontSize: "20px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "20px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
color: "white",
|
|
||||||
backgroundColor: "#7E2AEA",
|
|
||||||
borderTopRightRadius: "4px",
|
|
||||||
borderBottomRightRadius: "4px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "60px",
|
|
||||||
height: "40px",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%" }}>
|
|
||||||
<VideofileIcon fontSize="22px" color="#7E2AEA" />
|
|
||||||
</Box>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
background: "#7E2AEA",
|
|
||||||
height: "100%",
|
|
||||||
width: "25px",
|
|
||||||
color: "white",
|
|
||||||
fontSize: "15px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</span>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Typography
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: isMobile ? "none" : "block",
|
mb: "20px",
|
||||||
fontWeight: 400,
|
ml: isTablet ? "0px" : "60px",
|
||||||
fontSize: "16px",
|
display: "flex",
|
||||||
lineHeight: "18.96px",
|
alignItems: "center",
|
||||||
color: theme.palette.grey2.main,
|
gap: "28px",
|
||||||
}}
|
justifyContent: isMobile ? "space-between" : null,
|
||||||
>
|
}}
|
||||||
Видео
|
>
|
||||||
</Typography>
|
<Box
|
||||||
</Box>
|
sx={{
|
||||||
<UploadVideoModal
|
cursor: "pointer",
|
||||||
open={openVideoModal}
|
display: "flex",
|
||||||
onClose={() => setOpenVideoModal(false)}
|
alignItems: "center",
|
||||||
video={question.content.video}
|
gap: "20px",
|
||||||
onUpload={(url) => {
|
}}
|
||||||
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
>
|
||||||
content: { ...question.content, video: url },
|
<AddOrEditImageButton
|
||||||
});
|
imageSrc={question.content.picture}
|
||||||
}}
|
onImageClick={() => {
|
||||||
/>
|
if (question.content.picture) {
|
||||||
</Box>
|
return openCropModal(
|
||||||
</Box>
|
question.content.picture,
|
||||||
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
|
question.content.originalPicture
|
||||||
<SwitchPageOptions switchState={switchState} totalIndex={totalIndex} />
|
);
|
||||||
</>
|
}
|
||||||
);
|
|
||||||
|
setOpenImageModal(true);
|
||||||
|
}}
|
||||||
|
onPlusClick={() => {
|
||||||
|
setOpenImageModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
display: isMobile ? "none" : "block",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "18.96px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Изображение
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<UploadImageModal
|
||||||
|
open={openImageModal}
|
||||||
|
onClose={() => setOpenImageModal(false)}
|
||||||
|
imgHC={handleImageUpload}
|
||||||
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
|
<Typography> или</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "120px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
height: "40px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderTopLeftRadius: "4px",
|
||||||
|
borderBottomLeftRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VideofileIcon
|
||||||
|
style={{
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "20px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "20px",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
height: "40px",
|
||||||
|
color: "white",
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
|
borderTopRightRadius: "4px",
|
||||||
|
borderBottomRightRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "60px",
|
||||||
|
height: "40px",
|
||||||
|
background: "#EEE4FC",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%" }}>
|
||||||
|
<VideofileIcon fontSize="22px" color="#7E2AEA" />
|
||||||
|
</Box>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: "#7E2AEA",
|
||||||
|
height: "100%",
|
||||||
|
width: "25px",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
display: isMobile ? "none" : "block",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "18.96px",
|
||||||
|
color: theme.palette.grey2.main,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Видео
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<UploadVideoModal
|
||||||
|
open={openVideoModal}
|
||||||
|
onClose={() => setOpenVideoModal(false)}
|
||||||
|
video={question.content.video}
|
||||||
|
onUpload={(url) => {
|
||||||
|
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
||||||
|
content: { ...question.content, video: url },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<ButtonsOptions switchState={switchState} SSHC={SSHC} totalIndex={totalIndex} />
|
||||||
|
<SwitchPageOptions switchState={switchState} totalIndex={totalIndex} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,6 +110,7 @@ export default function TypeQuestions({ totalIndex }: Props) {
|
|||||||
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
||||||
<QuestionsMiniButton
|
<QuestionsMiniButton
|
||||||
key={title}
|
key={title}
|
||||||
|
dataCy={`select-questiontype-${value}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const question = { ...listQuestions[quizId][totalIndex] };
|
const question = { ...listQuestions[quizId][totalIndex] };
|
||||||
|
|
||||||
|
|||||||
@ -1,82 +1,97 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { questionStore, setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions";
|
||||||
import { Typography, Box, useTheme, ButtonBase } from "@mui/material";
|
|
||||||
import UploadBox from "@ui_kit/UploadBox";
|
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||||
import UploadIcon from "../../../assets/icons/UploadIcon";
|
import UploadBox from "@ui_kit/UploadBox";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
import { useParams } from "react-router-dom";
|
||||||
|
import UploadIcon from "../../../assets/icons/UploadIcon";
|
||||||
import { UploadImageModal } from "./UploadImageModal";
|
import { UploadImageModal } from "./UploadImageModal";
|
||||||
|
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
import { QuizQuestionBase } from "model/questionTypes/shared";
|
||||||
import type { DragEvent } from "react";
|
import type { DragEvent } from "react";
|
||||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
|
||||||
|
|
||||||
type UploadImageProps = {
|
type UploadImageProps = {
|
||||||
totalIndex: number;
|
totalIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function UploadImage({ totalIndex }: UploadImageProps) {
|
export default function UploadImage({ totalIndex }: UploadImageProps) {
|
||||||
const quizId = Number(useParams().quizId);
|
const quizId = Number(useParams().quizId);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = React.useState(false);
|
||||||
const { listQuestions } = questionStore();
|
const { listQuestions } = questionStore();
|
||||||
const question = listQuestions[quizId][totalIndex] as QuizQuestionImages;
|
const question = listQuestions[quizId][totalIndex] as QuizQuestionBase;
|
||||||
|
|
||||||
const handleOpen = () => setOpen(true);
|
const handleImageUpload = (files: FileList | null) => {
|
||||||
const handleClose = () => setOpen(false);
|
if (!files?.length) return;
|
||||||
const imgHC = (files: FileList | null) => {
|
|
||||||
if (files?.length) {
|
|
||||||
const [file] = Array.from(files);
|
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
const [file] = Array.from(files);
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
back: URL.createObjectURL(file),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
handleClose();
|
const url = URL.createObjectURL(file);
|
||||||
setOpened(true);
|
|
||||||
|
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||||
|
setQuestionOriginalBackgroundImage(quizId, totalIndex, url);
|
||||||
|
setIsUploadImageModalOpen(false);
|
||||||
|
openCropModal(url, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
handleImageUpload(event.dataTransfer.files);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
return (
|
||||||
event.preventDefault();
|
<Box sx={{ padding: "20px" }}>
|
||||||
event.stopPropagation();
|
<Typography
|
||||||
|
sx={{
|
||||||
imgHC(event.dataTransfer.files);
|
fontWeight: 500,
|
||||||
};
|
color: theme.palette.grey3.main,
|
||||||
|
mt: "11px",
|
||||||
return (
|
mb: "14px",
|
||||||
<Box sx={{ padding: "20px" }}>
|
}}
|
||||||
<Typography
|
>
|
||||||
sx={{
|
Загрузить изображение
|
||||||
fontWeight: 500,
|
</Typography>
|
||||||
color: theme.palette.grey3.main,
|
<ButtonBase
|
||||||
mt: "11px",
|
onClick={() => setIsUploadImageModalOpen(true)}
|
||||||
mb: "14px",
|
sx={{
|
||||||
}}
|
width: "100%",
|
||||||
>
|
maxWidth: "260px",
|
||||||
Загрузить изображение
|
height: "120px",
|
||||||
</Typography>
|
}}
|
||||||
<ButtonBase
|
>
|
||||||
onClick={handleOpen}
|
{question.content.back ?
|
||||||
sx={{ width: "100%", maxWidth: "260px" }}
|
<img
|
||||||
>
|
src={question.content.back}
|
||||||
<UploadBox
|
alt="question background"
|
||||||
handleDrop={handleDrop}
|
style={{
|
||||||
sx={{ maxWidth: "260px" }}
|
width: "100%",
|
||||||
icon={<UploadIcon />}
|
height: "100%",
|
||||||
text="5 MB максимум"
|
objectFit: "scale-down",
|
||||||
/>
|
display: "block",
|
||||||
</ButtonBase>
|
}}
|
||||||
<UploadImageModal open={open} onClose={handleClose} imgHC={imgHC} />
|
/>
|
||||||
<CropModal
|
:
|
||||||
opened={opened}
|
<UploadBox
|
||||||
onClose={() => setOpened(false)}
|
handleDrop={handleDrop}
|
||||||
picture={question.content.back}
|
sx={{ maxWidth: "260px" }}
|
||||||
/>
|
icon={<UploadIcon />}
|
||||||
</Box>
|
text="5 MB максимум"
|
||||||
);
|
/>
|
||||||
|
}
|
||||||
|
</ButtonBase>
|
||||||
|
<UploadImageModal
|
||||||
|
open={isUploadImageModalOpen}
|
||||||
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
|
imgHC={handleImageUpload}
|
||||||
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export default function AnswerOptions({ totalIndex }: Props) {
|
|||||||
|
|
||||||
const addNewAnswer = () => {
|
const addNewAnswer = () => {
|
||||||
const answerNew = question.content.variants.slice();
|
const answerNew = question.content.variants.slice();
|
||||||
answerNew.push({ answer: "", extendedText: "" });
|
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||||
content: { ...question.content, variants: answerNew },
|
content: { ...question.content, variants: answerNew },
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,8 @@
|
|||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import Cytoscape from "react-cytoscapejs";
|
import Cytoscape from "react-cytoscapejs";
|
||||||
|
import { createGraphElements } from "./helper";
|
||||||
|
|
||||||
import { ELEMENTS } from "./elements";
|
import { VERTICES } from "./elements";
|
||||||
|
|
||||||
import type { Stylesheet } from "cytoscape";
|
import type { Stylesheet } from "cytoscape";
|
||||||
|
|
||||||
@ -13,11 +14,6 @@ const stylesheet: Stylesheet[] = [
|
|||||||
width: 130,
|
width: 130,
|
||||||
height: 130,
|
height: 130,
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "node[label]",
|
|
||||||
style: {
|
|
||||||
label: "data(label)",
|
label: "data(label)",
|
||||||
"font-size": "16",
|
"font-size": "16",
|
||||||
color: "#4D4D4D",
|
color: "#4D4D4D",
|
||||||
@ -31,9 +27,8 @@ const stylesheet: Stylesheet[] = [
|
|||||||
width: 30,
|
width: 30,
|
||||||
"line-color": "#DEDFE7",
|
"line-color": "#DEDFE7",
|
||||||
"curve-style": "taxi",
|
"curve-style": "taxi",
|
||||||
"taxi-direction": "downward",
|
"taxi-direction": "horizontal",
|
||||||
"taxi-turn": 20,
|
"taxi-turn": 60,
|
||||||
"taxi-turn-min-distance": 5,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -46,24 +41,22 @@ const stylesheet: Stylesheet[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Graph = () => {
|
export const Graph = () => (
|
||||||
return (
|
<Box
|
||||||
<Box
|
sx={{
|
||||||
sx={{
|
padding: "20px",
|
||||||
padding: "20px",
|
background: "#FFFFFF",
|
||||||
background: "#FFFFFF",
|
borderRadius: "12px",
|
||||||
borderRadius: "12px",
|
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||||
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
marginTop: "30px",
|
||||||
marginTop: "30px",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Cytoscape
|
||||||
<Cytoscape
|
wheelSensitivity={0.1}
|
||||||
wheelSensitivity={0.1}
|
elements={createGraphElements(VERTICES)}
|
||||||
elements={ELEMENTS}
|
style={{ height: "480px", background: "#F2F3F7" }}
|
||||||
style={{ height: "480px", background: "#F2F3F7" }}
|
layout={{ name: "preset" }}
|
||||||
layout={{ name: "breadthfirst" }}
|
stylesheet={stylesheet}
|
||||||
stylesheet={stylesheet}
|
/>
|
||||||
/>
|
</Box>
|
||||||
</Box>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
140
src/pages/QuestionsMap/helper.ts
Normal file
140
src/pages/QuestionsMap/helper.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import type { ElementDefinition } from "cytoscape";
|
||||||
|
import type { Vertex } from "./elements";
|
||||||
|
|
||||||
|
const sortElementsByLevel = (vertices: Vertex[]): Record<number, Vertex[]> => {
|
||||||
|
const sortedVertices: Record<number, Vertex[]> = {};
|
||||||
|
|
||||||
|
vertices.forEach((vertex) => {
|
||||||
|
if (!sortedVertices[vertex.level]) {
|
||||||
|
sortedVertices[vertex.level] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedVertices[vertex.level].push(vertex);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedVertices;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentIndent = (
|
||||||
|
id: string,
|
||||||
|
edges: string[] | undefined,
|
||||||
|
vertices: Vertex[]
|
||||||
|
): [number, number] => {
|
||||||
|
let currentIndent = 0;
|
||||||
|
let bottomIndent = 0;
|
||||||
|
|
||||||
|
if (!edges?.length) {
|
||||||
|
return [100, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePointIndent = (pointId: string, subIndent = false) => {
|
||||||
|
const point = vertices.find(({ id }) => pointId === id);
|
||||||
|
|
||||||
|
if (point?.edges) {
|
||||||
|
if (!subIndent) {
|
||||||
|
currentIndent += point.edges.length * 100;
|
||||||
|
} else {
|
||||||
|
bottomIndent += (point.edges.length - 1) * 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
point.edges.forEach((edge) => {
|
||||||
|
calculatePointIndent(edge, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
calculatePointIndent(id);
|
||||||
|
|
||||||
|
return [currentIndent, bottomIndent];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortVertices = (
|
||||||
|
elements: ElementDefinition[],
|
||||||
|
vertices: Vertex[]
|
||||||
|
): ElementDefinition[] => {
|
||||||
|
const sortedVertices: ElementDefinition[] = [];
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
const currentVertex = vertices.find(({ id }) => element.data.id === id);
|
||||||
|
|
||||||
|
if (!currentVertex || (currentVertex?.edges?.length || 0) < 2) {
|
||||||
|
sortedVertices.push(element);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstChildId = currentVertex.edges?.at(0);
|
||||||
|
const lastChildId = currentVertex.edges?.at(-1);
|
||||||
|
const firstChild = elements.find(({ data }) => data.id === firstChildId);
|
||||||
|
const lastChild = elements.find(({ data }) => data.id === lastChildId);
|
||||||
|
|
||||||
|
if (!firstChild?.position || !lastChild?.position) {
|
||||||
|
sortedVertices.push(element);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentVertexTopIndent =
|
||||||
|
firstChild.position.y +
|
||||||
|
(lastChild.position.y - firstChild.position.y) / 2;
|
||||||
|
|
||||||
|
sortedVertices.push({
|
||||||
|
...element,
|
||||||
|
position: { x: element.position?.x || 0, y: parentVertexTopIndent },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedVertices;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createGraphElements = (
|
||||||
|
vertices: Vertex[]
|
||||||
|
): ElementDefinition[] => {
|
||||||
|
const elements: ElementDefinition[] = [];
|
||||||
|
const bridges: ElementDefinition[] = [];
|
||||||
|
const sortedVertices = sortElementsByLevel(vertices);
|
||||||
|
|
||||||
|
Object.values(sortedVertices).forEach((vertexItems) => {
|
||||||
|
let indent = 0;
|
||||||
|
|
||||||
|
vertexItems.forEach(({ id, label, level, edges }) => {
|
||||||
|
const [currentIndent, bottomIndent] = getCurrentIndent(
|
||||||
|
id,
|
||||||
|
edges,
|
||||||
|
vertices
|
||||||
|
);
|
||||||
|
const parentVertex = vertices.find(({ edges }) => edges?.includes(id));
|
||||||
|
const parentElement = elements.find(
|
||||||
|
({ data }) => parentVertex?.id === data.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const minTopIndent =
|
||||||
|
(parentElement?.position?.y || 0) -
|
||||||
|
(parentVertex?.edges?.length || 0) * 100;
|
||||||
|
|
||||||
|
if (minTopIndent > indent) {
|
||||||
|
indent = minTopIndent;
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push({
|
||||||
|
data: { id, label },
|
||||||
|
position: {
|
||||||
|
x: level * 250,
|
||||||
|
y: indent + currentIndent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
edges?.forEach((edge) => {
|
||||||
|
bridges.push({
|
||||||
|
data: { source: id, target: edge },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
indent += currentIndent * 2 + bottomIndent;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedElements = sortVertices(elements, vertices);
|
||||||
|
|
||||||
|
return [...sortedElements, ...bridges];
|
||||||
|
};
|
||||||
@ -2,27 +2,17 @@ import { Box, Typography, useTheme, useMediaQuery } 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 { useState } from "react";
|
|
||||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
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();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(400));
|
const isMobile = useMediaQuery(theme.breakpoints.down(400));
|
||||||
|
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
||||||
sx={{
|
<AddImage onClick={() => openCropModal("", "")} />
|
||||||
display: "flex",
|
<CropModal />
|
||||||
alignItems: "center",
|
|
||||||
gap: "12px",
|
|
||||||
mt: "20px",
|
|
||||||
mb: "20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AddImage onClick={() => setOpened(true)} />
|
|
||||||
<CropModal opened={opened} onClose={() => setOpened(false)} />
|
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export default function FirstQuiz() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
data-cy="create-quiz"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/setting/${createBlank()}`);
|
navigate(`/setting/${createBlank()}`);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -848,26 +848,24 @@ export default function StartPageSettings() {
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Button
|
||||||
<Button
|
variant="contained"
|
||||||
variant="contained"
|
data-cy="setup-questions"
|
||||||
sx={{ display: "block", marginLeft: "auto" }}
|
onClick={() => {
|
||||||
onClick={() => {
|
let SPageClone = listQuizes[params].config;
|
||||||
let SPageClone = listQuizes[params].config;
|
SPageClone.startpage.background.desktop =
|
||||||
SPageClone.startpage.background.desktop =
|
"https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg";
|
||||||
"https://happypik.ru/wp-content/uploads/2019/09/njashnye-kotiki8.jpg";
|
SPageClone.startpage.background.mobile =
|
||||||
SPageClone.startpage.background.mobile =
|
"https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png";
|
||||||
"https://krot.info/uploads/posts/2022-03/1646156155_3-krot-info-p-smeshnie-tolstie-koti-smeshnie-foto-3.png";
|
SPageClone.startpage.background.video =
|
||||||
SPageClone.startpage.background.video =
|
"https://youtu.be/dbaPkCiLPKQ";
|
||||||
"https://youtu.be/dbaPkCiLPKQ";
|
updateQuizesList(params, { config: SPageClone });
|
||||||
updateQuizesList(params, { config: SPageClone });
|
handleNext();
|
||||||
handleNext();
|
createQuestion(params);
|
||||||
createQuestion(params);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Настроить вопросы
|
||||||
Настроить вопросы
|
</Button>
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export default function StepOne() {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
|
data-cy="create-quiz-card"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let SPageClone = listQuizes[params].config;
|
let SPageClone = listQuizes[params].config;
|
||||||
SPageClone.type = "quize";
|
SPageClone.type = "quize";
|
||||||
|
|||||||
66
src/stores/cropModal.ts
Normal file
66
src/stores/cropModal.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
|
||||||
|
type CropModalStore = {
|
||||||
|
isCropModalOpen: boolean;
|
||||||
|
imageUrl: string | null;
|
||||||
|
originalImageUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: CropModalStore = {
|
||||||
|
isCropModalOpen: false,
|
||||||
|
imageUrl: null,
|
||||||
|
originalImageUrl: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCropModalStore = create<CropModalStore>()(
|
||||||
|
devtools(
|
||||||
|
() => initialState,
|
||||||
|
{
|
||||||
|
name: "CropModalStore",
|
||||||
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: process.env.NODE_ENV === "development",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const openCropModal = (imageUrl: string, originalImageUrl: string) => useCropModalStore.setState(
|
||||||
|
{
|
||||||
|
isCropModalOpen: true,
|
||||||
|
imageUrl,
|
||||||
|
originalImageUrl,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "openCropModal",
|
||||||
|
imageUrl,
|
||||||
|
originalImageUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const closeCropModal = () => useCropModalStore.setState(
|
||||||
|
initialState,
|
||||||
|
false,
|
||||||
|
"closeCropModal"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setCropModalImageUrl = (imageUrl: string | null) => useCropModalStore.setState(
|
||||||
|
{ imageUrl },
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
type: "setCropModalImageUrl",
|
||||||
|
imageUrl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resetToOriginalImage = (): boolean => {
|
||||||
|
if (!useCropModalStore.getState().originalImageUrl) return false;
|
||||||
|
|
||||||
|
useCropModalStore.setState(
|
||||||
|
state => ({ imageUrl: state.originalImageUrl }),
|
||||||
|
false,
|
||||||
|
"resetToOriginalImage"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@ -1,17 +1,12 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { devtools, persist } from "zustand/middleware";
|
||||||
|
|
||||||
import { QuizQuestionEmoji } from "../model/questionTypes/emoji";
|
|
||||||
import { QuizQuestionImages } from "../model/questionTypes/images";
|
|
||||||
import { QuizQuestionVariant } from "../model/questionTypes/variant";
|
|
||||||
import { QuizQuestionVarImg } from "../model/questionTypes/varimg";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnyQuizQuestion,
|
AnyQuizQuestion,
|
||||||
QuizQuestionType,
|
QuizQuestionType
|
||||||
QuestionVariant,
|
|
||||||
} from "../model/questionTypes/shared";
|
} from "../model/questionTypes/shared";
|
||||||
|
|
||||||
|
import { produce, setAutoFreeze } from "immer";
|
||||||
import { QUIZ_QUESTION_BASE } from "../constants/base";
|
import { QUIZ_QUESTION_BASE } from "../constants/base";
|
||||||
import { QUIZ_QUESTION_DATE } from "../constants/date";
|
import { QUIZ_QUESTION_DATE } from "../constants/date";
|
||||||
import { QUIZ_QUESTION_EMOJI } from "../constants/emoji";
|
import { QUIZ_QUESTION_EMOJI } from "../constants/emoji";
|
||||||
@ -24,174 +19,303 @@ import { QUIZ_QUESTION_SELECT } from "../constants/select";
|
|||||||
import { QUIZ_QUESTION_TEXT } from "../constants/text";
|
import { QUIZ_QUESTION_TEXT } from "../constants/text";
|
||||||
import { QUIZ_QUESTION_VARIANT } from "../constants/variant";
|
import { QUIZ_QUESTION_VARIANT } from "../constants/variant";
|
||||||
import { QUIZ_QUESTION_VARIMG } from "../constants/varimg";
|
import { QUIZ_QUESTION_VARIMG } from "../constants/varimg";
|
||||||
import { setAutoFreeze } from "immer";
|
|
||||||
|
|
||||||
setAutoFreeze(false);
|
setAutoFreeze(false);
|
||||||
|
|
||||||
interface QuestionStore {
|
interface QuestionStore {
|
||||||
listQuestions: Record<string, AnyQuizQuestion[]>;
|
listQuestions: Record<string, AnyQuizQuestion[]>;
|
||||||
openedModalSettings: string;
|
openedModalSettings: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFirstPartialize = true;
|
let isFirstPartialize = true;
|
||||||
|
|
||||||
export const questionStore = create<QuestionStore>()(
|
export const questionStore = create<QuestionStore>()(
|
||||||
persist<QuestionStore>(
|
persist(
|
||||||
() => ({
|
devtools(
|
||||||
listQuestions: {},
|
() => ({
|
||||||
openedModalSettings: "",
|
listQuestions: {},
|
||||||
}),
|
openedModalSettings: "",
|
||||||
{
|
}),
|
||||||
name: "question",
|
{
|
||||||
partialize: (state: QuestionStore) => {
|
name: "Question",
|
||||||
if (isFirstPartialize) {
|
enabled: process.env.NODE_ENV === "development",
|
||||||
isFirstPartialize = false;
|
trace: process.env.NODE_ENV === "development",
|
||||||
|
actionsBlacklist: "ignored",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: "question",
|
||||||
|
partialize: (state: QuestionStore) => {
|
||||||
|
if (isFirstPartialize) {
|
||||||
|
isFirstPartialize = false;
|
||||||
|
|
||||||
Object.keys(state.listQuestions).forEach((quizId) => {
|
Object.keys(state.listQuestions).forEach((quizId) => {
|
||||||
[...state.listQuestions[quizId]].forEach(({ id, deleted }) => {
|
[...state.listQuestions[quizId]].forEach(({ id, deleted }) => {
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
const removedItemIndex = state.listQuestions[quizId].findIndex(
|
const removedItemIndex = state.listQuestions[quizId].findIndex(
|
||||||
(item) => item.id === id
|
(item) => item.id === id
|
||||||
);
|
);
|
||||||
|
|
||||||
state.listQuestions[quizId].splice(removedItemIndex, 1);
|
state.listQuestions[quizId].splice(removedItemIndex, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
merge: (persistedState, currentState) => {
|
||||||
|
const state = persistedState as QuestionStore;
|
||||||
|
|
||||||
|
// replace blob urls with ""
|
||||||
|
Object.values(state.listQuestions).forEach(questions => {
|
||||||
|
questions.forEach(question => {
|
||||||
|
if (question.type === "page") {
|
||||||
|
if (question.content.picture.startsWith("blob:")) {
|
||||||
|
question.content.picture = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.type === "images") {
|
||||||
|
question.content.variants.forEach(variant => {
|
||||||
|
if (variant.extendedText.startsWith("blob:")) {
|
||||||
|
variant.extendedText = "";
|
||||||
|
}
|
||||||
|
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||||
|
variant.originalImageUrl = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (question.type === "varimg") {
|
||||||
|
question.content.variants.forEach(variant => {
|
||||||
|
if (variant.extendedText.startsWith("blob:")) {
|
||||||
|
variant.extendedText = "";
|
||||||
|
}
|
||||||
|
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||||
|
variant.originalImageUrl = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
return state;
|
|
||||||
},
|
|
||||||
merge: (persistedState, currentState) => {
|
|
||||||
const state = persistedState as QuestionStore;
|
|
||||||
|
|
||||||
// replace blob urls with ""
|
|
||||||
Object.values(state.listQuestions).forEach((questions) => {
|
|
||||||
questions.forEach((question) => {
|
|
||||||
if (
|
|
||||||
question.type === "page" &&
|
|
||||||
question.content.picture.startsWith("blob:")
|
|
||||||
) {
|
|
||||||
question.content.picture = "";
|
|
||||||
}
|
|
||||||
if (question.type === "images") {
|
|
||||||
question.content.variants.forEach((variant) => {
|
|
||||||
if (variant.extendedText.startsWith("blob:")) {
|
|
||||||
variant.extendedText = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (question.type === "varimg") {
|
|
||||||
question.content.variants.forEach((variant) => {
|
|
||||||
if (variant.extendedText.startsWith("blob:")) {
|
|
||||||
variant.extendedText = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...currentState,
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
export const updateQuestionsList = <T = AnyQuizQuestion>(
|
export const updateQuestionsList = <T = AnyQuizQuestion>(
|
||||||
quizId: number,
|
quizId: number,
|
||||||
index: number,
|
index: number,
|
||||||
data: Partial<T>
|
data: Partial<T>
|
||||||
) => {
|
) => {
|
||||||
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
||||||
questionListClone[quizId][index] = {
|
questionListClone[quizId][index] = {
|
||||||
...questionListClone[quizId][index],
|
...questionListClone[quizId][index],
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
questionStore.setState({ listQuestions: questionListClone });
|
questionStore.setState({ listQuestions: questionListClone });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateQuestion = <T extends AnyQuizQuestion>(
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
recipe: (question: T) => void,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex] as T;
|
||||||
|
|
||||||
|
recipe(question);
|
||||||
|
}, {
|
||||||
|
type: "updateQuestion",
|
||||||
|
quizId,
|
||||||
|
questionIndex,
|
||||||
|
recipe,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeQuestionsByQuizId = (quizId: number) => setProducedState(state => {
|
||||||
|
delete state.listQuestions[quizId];
|
||||||
|
}, "removeQuestionsByQuizId");
|
||||||
|
|
||||||
|
export const setVariantImageUrl = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
variantIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants[variantIndex];
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
|
if (variant.extendedText === url) return;
|
||||||
|
|
||||||
|
if (variant.extendedText !== variant.originalImageUrl) URL.revokeObjectURL(variant.extendedText);
|
||||||
|
variant.extendedText = url;
|
||||||
|
}, {
|
||||||
|
type: "setVariantImageUrl",
|
||||||
|
quizId,
|
||||||
|
questionIndex,
|
||||||
|
variantIndex,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setVariantOriginalImageUrl = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
variantIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
if (!("variants" in question.content)) return;
|
||||||
|
|
||||||
|
const variant = question.content.variants[variantIndex];
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
|
if (variant.originalImageUrl === url) return;
|
||||||
|
|
||||||
|
URL.revokeObjectURL(variant.originalImageUrl);
|
||||||
|
variant.originalImageUrl = url;
|
||||||
|
}, {
|
||||||
|
type: "setVariantOriginalImageUrl",
|
||||||
|
quizId,
|
||||||
|
questionIndex,
|
||||||
|
variantIndex,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setPageQuestionPicture = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
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 = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
if (question.type !== "page") return;
|
||||||
|
|
||||||
|
if (question.content.originalPicture === url) return;
|
||||||
|
|
||||||
|
URL.revokeObjectURL(question.content.originalPicture);
|
||||||
|
question.content.originalPicture = url;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setQuestionBackgroundImage = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
quizId: number,
|
||||||
|
questionIndex: number,
|
||||||
|
url: string,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
|
|
||||||
|
if (question.content.originalBack === url) return;
|
||||||
|
|
||||||
|
URL.revokeObjectURL(question.content.originalBack);
|
||||||
|
question.content.originalBack = url;
|
||||||
|
})
|
||||||
|
|
||||||
export const updateQuestionsListDragAndDrop = (
|
export const updateQuestionsListDragAndDrop = (
|
||||||
quizId: number,
|
quizId: number,
|
||||||
updatedQuestions: AnyQuizQuestion[]
|
updatedQuestions: AnyQuizQuestion[]
|
||||||
) => {
|
) => {
|
||||||
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
||||||
questionStore.setState({
|
questionStore.setState({
|
||||||
listQuestions: { ...questionListClone, [quizId]: updatedQuestions },
|
listQuestions: { ...questionListClone, [quizId]: updatedQuestions },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateVariants = (
|
|
||||||
quizId: number,
|
|
||||||
index: number,
|
|
||||||
variants: QuestionVariant[]
|
|
||||||
) => {
|
|
||||||
const listQuestions = { ...questionStore.getState()["listQuestions"] };
|
|
||||||
|
|
||||||
switch (listQuestions[quizId][index].type) {
|
export const reorderVariants = (
|
||||||
case "emoji":
|
quizId: number,
|
||||||
const emojiState = listQuestions[quizId][index] as QuizQuestionEmoji;
|
questionIndex: number,
|
||||||
emojiState.content.variants = variants;
|
sourceIndex: number,
|
||||||
return questionStore.setState({ listQuestions });
|
destinationIndex: number,
|
||||||
|
) => setProducedState(state => {
|
||||||
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
case "images":
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
const imagesState = listQuestions[quizId][index] as QuizQuestionImages;
|
if (!("variants" in question.content)) return;
|
||||||
imagesState.content.variants = variants;
|
|
||||||
return questionStore.setState({ listQuestions });
|
|
||||||
|
|
||||||
case "variant":
|
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
||||||
const variantState = listQuestions[quizId][index] as QuizQuestionVariant;
|
question.content.variants.splice(destinationIndex, 0, removed);
|
||||||
variantState.content.variants = variants;
|
}, {
|
||||||
return questionStore.setState({ listQuestions });
|
type: sourceIndex === destinationIndex ? "reorderVariants" : "ignored",
|
||||||
|
quizId,
|
||||||
case "varimg":
|
questionIndex,
|
||||||
const varImgState = listQuestions[quizId][index] as QuizQuestionVarImg;
|
sourceIndex,
|
||||||
varImgState.content.variants = variants;
|
destinationIndex,
|
||||||
return questionStore.setState({ listQuestions });
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createQuestion = (
|
export const createQuestion = (
|
||||||
quizId: number,
|
quizId: number,
|
||||||
questionType: QuizQuestionType = "nonselected",
|
questionType: QuizQuestionType = "nonselected",
|
||||||
placeIndex = -1
|
placeIndex = -1
|
||||||
) => {
|
) => {
|
||||||
const id = getRandom();
|
const id = getRandom();
|
||||||
const newData = { ...questionStore.getState()["listQuestions"] };
|
const newData = { ...questionStore.getState()["listQuestions"] };
|
||||||
|
|
||||||
if (!newData[quizId]) {
|
if (!newData[quizId]) {
|
||||||
newData[quizId] = [];
|
newData[quizId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultObject = [
|
const defaultObject = [
|
||||||
QUIZ_QUESTION_BASE,
|
QUIZ_QUESTION_BASE,
|
||||||
QUIZ_QUESTION_DATE,
|
QUIZ_QUESTION_DATE,
|
||||||
QUIZ_QUESTION_EMOJI,
|
QUIZ_QUESTION_EMOJI,
|
||||||
QUIZ_QUESTION_FILE,
|
QUIZ_QUESTION_FILE,
|
||||||
QUIZ_QUESTION_IMAGES,
|
QUIZ_QUESTION_IMAGES,
|
||||||
QUIZ_QUESTION_NUMBER,
|
QUIZ_QUESTION_NUMBER,
|
||||||
QUIZ_QUESTION_PAGE,
|
QUIZ_QUESTION_PAGE,
|
||||||
QUIZ_QUESTION_RATING,
|
QUIZ_QUESTION_RATING,
|
||||||
QUIZ_QUESTION_SELECT,
|
QUIZ_QUESTION_SELECT,
|
||||||
QUIZ_QUESTION_TEXT,
|
QUIZ_QUESTION_TEXT,
|
||||||
QUIZ_QUESTION_VARIANT,
|
QUIZ_QUESTION_VARIANT,
|
||||||
QUIZ_QUESTION_VARIMG,
|
QUIZ_QUESTION_VARIMG,
|
||||||
].find((defaultObjectItem) => defaultObjectItem.type === questionType);
|
].find((defaultObjectItem) => defaultObjectItem.type === questionType);
|
||||||
|
|
||||||
if (defaultObject) {
|
if (defaultObject) {
|
||||||
newData[quizId].splice(
|
newData[quizId].splice(
|
||||||
placeIndex < 0 ? newData[quizId].length : placeIndex,
|
placeIndex < 0 ? newData[quizId].length : placeIndex,
|
||||||
0,
|
0,
|
||||||
{ ...defaultObject, id }
|
{ ...JSON.parse(JSON.stringify(defaultObject)), id }
|
||||||
);
|
);
|
||||||
|
|
||||||
questionStore.setState({ listQuestions: newData });
|
questionStore.setState({ listQuestions: newData });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
|
export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
|
||||||
const listQuestions = { ...questionStore.getState()["listQuestions"] };
|
const listQuestions = { ...questionStore.getState()["listQuestions"] };
|
||||||
|
|
||||||
const copiedQuiz = { ...listQuestions[quizId][copiedQuestionIndex] };
|
const copiedQuiz = { ...listQuestions[quizId][copiedQuestionIndex] };
|
||||||
listQuestions[quizId].splice(copiedQuestionIndex, 0, {
|
listQuestions[quizId].splice(copiedQuestionIndex, 0, {
|
||||||
@ -199,43 +323,51 @@ export const copyQuestion = (quizId: number, copiedQuestionIndex: number) => {
|
|||||||
id: getRandom(),
|
id: getRandom(),
|
||||||
});
|
});
|
||||||
|
|
||||||
questionStore.setState({ listQuestions });
|
questionStore.setState({ listQuestions });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeQuestionForce = (quizId: number, removedId: number) => {
|
export const removeQuestionForce = (quizId: number, removedId: number) => {
|
||||||
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
||||||
const removedItemIndex = questionListClone[quizId].findIndex(
|
const removedItemIndex = questionListClone[quizId].findIndex(
|
||||||
({ id }) => id === removedId
|
({ id }) => id === removedId
|
||||||
);
|
);
|
||||||
questionListClone[quizId].splice(removedItemIndex, 1);
|
questionListClone[quizId].splice(removedItemIndex, 1);
|
||||||
questionStore.setState({ listQuestions: questionListClone });
|
questionStore.setState({ listQuestions: questionListClone });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeQuestion = (quizId: number, index: number) => {
|
export const removeQuestion = (quizId: number, index: number) => {
|
||||||
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
const questionListClone = { ...questionStore.getState()["listQuestions"] };
|
||||||
questionListClone[quizId][index].deleted = true;
|
questionListClone[quizId][index].deleted = true;
|
||||||
questionStore.setState({ listQuestions: questionListClone });
|
questionStore.setState({ listQuestions: questionListClone });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetSomeField = (data: Record<string, string>) => {
|
export const resetSomeField = (data: Record<string, string>) => {
|
||||||
questionStore.setState(data);
|
questionStore.setState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findQuestionById = (quizId: number) => {
|
export const findQuestionById = (quizId: number) => {
|
||||||
let found = null;
|
let found = null;
|
||||||
questionStore
|
questionStore
|
||||||
.getState()
|
.getState()
|
||||||
["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => {
|
["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => {
|
||||||
if (quiz.id === quizId) {
|
if (quiz.id === quizId) {
|
||||||
found = { quiz, index };
|
found = { quiz, index };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getRandom() {
|
function getRandom() {
|
||||||
const min = Math.ceil(1000000);
|
const min = Math.ceil(1000000);
|
||||||
const max = Math.floor(10000000);
|
const max = Math.floor(10000000);
|
||||||
return Math.floor(Math.random() * (max - min)) + min;
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProducedState<A extends string | { type: unknown; }>(
|
||||||
|
recipe: (state: QuestionStore) => void,
|
||||||
|
action?: A,
|
||||||
|
) {
|
||||||
|
questionStore.setState(state => produce(state, recipe), false, action);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
import {persist} from "zustand/middleware";
|
import {persist} from "zustand/middleware";
|
||||||
|
import { removeQuestionsByQuizId } from "./questions";
|
||||||
|
|
||||||
interface QuizStore {
|
interface QuizStore {
|
||||||
listQuizes: { [key: number]: Quizes };
|
listQuizes: { [key: number]: Quizes };
|
||||||
@ -87,6 +88,8 @@ export const quizStore = create<QuizStore>()(
|
|||||||
return accumulator;
|
return accumulator;
|
||||||
}, {});
|
}, {});
|
||||||
set({listQuizes: newState});
|
set({listQuizes: newState});
|
||||||
|
|
||||||
|
removeQuestionsByQuizId(id);
|
||||||
},
|
},
|
||||||
createBlank: () => {
|
createBlank: () => {
|
||||||
const id = getRandom(1000000, 10000000)
|
const id = getRandom(1000000, 10000000)
|
||||||
|
|||||||
63
src/ui_kit/AddOrEditImageButton.tsx
Normal file
63
src/ui_kit/AddOrEditImageButton.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import Plus from "@icons/questionsPage/plus";
|
||||||
|
import { Box, Button, SxProps, Theme } from "@mui/material";
|
||||||
|
import Image from "../assets/icons/questionsPage/image";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
imageSrc?: string;
|
||||||
|
onImageClick?: () => void;
|
||||||
|
onPlusClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddOrEditImageButton({ onImageClick, onPlusClick, sx, imageSrc }: Props) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
height: "40px",
|
||||||
|
minWidth: "60px",
|
||||||
|
width: "60px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
overflow: "hidden",
|
||||||
|
...sx,
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
onClick={onImageClick}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
minWidth: "40px",
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: "#EEE4FC",
|
||||||
|
}}>
|
||||||
|
{imageSrc ? (
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
alt=""
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "scale-down",
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image sx={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onPlusClick}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
minWidth: "20px",
|
||||||
|
width: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,163 +1,110 @@
|
|||||||
import React, { useState, useRef, useEffect, FC } from "react";
|
import { CropIcon } from "@icons/CropIcon";
|
||||||
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
import { ResetIcon } from "@icons/ResetIcon";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
Slider,
|
Slider,
|
||||||
|
SxProps,
|
||||||
|
Theme,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { closeCropModal, resetToOriginalImage, setCropModalImageUrl, useCropModalStore } from "@root/cropModal";
|
||||||
import { canvasPreview } from "./utils/canvasPreview";
|
import { FC, useRef, useState } from "react";
|
||||||
import { useDebounceEffect } from "./utils/useDebounceEffect";
|
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
||||||
|
|
||||||
import { ResetIcon } from "@icons/ResetIcon";
|
|
||||||
|
|
||||||
import "react-image-crop/dist/ReactCrop.css";
|
import "react-image-crop/dist/ReactCrop.css";
|
||||||
import { CropIcon } from "@icons/CropIcon";
|
import { canvasPreview } from "./utils/canvasPreview";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
interface Iprops {
|
|
||||||
opened: boolean;
|
|
||||||
onClose: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
picture?: string;
|
|
||||||
onCropPress?: (imageUrl: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress }) => {
|
const styleSlider: SxProps<Theme> = {
|
||||||
const [imgSrc, setImgSrc] = useState("");
|
color: "#7E2AEA",
|
||||||
|
height: "12px",
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
"& .MuiSlider-track": {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
border: "none",
|
||||||
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
},
|
||||||
const hiddenAnchorRef = useRef<HTMLAnchorElement>(null);
|
"& .MuiSlider-rail": {
|
||||||
const blobUrlRef = useRef("");
|
backgroundColor: "#F2F3F7",
|
||||||
const [crop, setCrop] = useState<Crop>();
|
border: `1px solid #9A9AAF`,
|
||||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
},
|
||||||
const [rotate, setRotate] = useState(0);
|
"& .MuiSlider-thumb": {
|
||||||
const [darken, setDarken] = useState(0);
|
height: 26,
|
||||||
|
width: 26,
|
||||||
const theme = useTheme();
|
border: `6px solid #7E2AEA`,
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
backgroundColor: "white",
|
||||||
|
boxShadow: `0px 0px 0px 3px white,
|
||||||
useEffect(() => {
|
0px 4px 4px 3px #C3C8DD`,
|
||||||
if (picture) {
|
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
|
||||||
setImgSrc(picture);
|
|
||||||
}
|
|
||||||
}, [picture]);
|
|
||||||
|
|
||||||
const styleModal = {
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
width: isMobile ? "343px" : "620px",
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
boxShadow: 24,
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
};
|
|
||||||
|
|
||||||
const styleSlider = {
|
|
||||||
width: isMobile ? "350px" : "250px",
|
|
||||||
color: "#7E2AEA",
|
|
||||||
height: "12px",
|
|
||||||
"& .MuiSlider-track": {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
"& .MuiSlider-rail": {
|
|
||||||
backgroundColor: "#F2F3F7",
|
|
||||||
border: `1px solid #9A9AAF`,
|
|
||||||
},
|
|
||||||
"& .MuiSlider-thumb": {
|
|
||||||
height: 26,
|
|
||||||
width: 26,
|
|
||||||
border: `6px solid #7E2AEA`,
|
|
||||||
backgroundColor: "white",
|
|
||||||
boxShadow: `0px 0px 0px 3px white,
|
boxShadow: `0px 0px 0px 3px white,
|
||||||
0px 4px 4px 3px #C3C8DD`,
|
0px 4px 4px 3px #C3C8DD`,
|
||||||
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
|
|
||||||
boxShadow: `0px 0px 0px 3px white,
|
|
||||||
0px 4px 4px 3px #C3C8DD`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const rotateImage = () => {
|
interface Props {
|
||||||
const newRotation = (rotate + 90) % 360;
|
onSaveImageClick?: (imageUrl: string) => void;
|
||||||
setRotate(newRotation);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
const theme = useTheme();
|
||||||
setCrop(undefined);
|
const isCropModalOpen = useCropModalStore(state => state.isCropModalOpen);
|
||||||
const reader = new FileReader();
|
const imageUrl = useCropModalStore(state => state.imageUrl);
|
||||||
reader.addEventListener("load", () =>
|
const [crop, setCrop] = useState<Crop>();
|
||||||
setImgSrc(reader.result?.toString() || "")
|
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
||||||
);
|
const [darken, setDarken] = useState(0);
|
||||||
reader.readAsDataURL(event.target.files[0]);
|
const [rotate, setRotate] = useState(0);
|
||||||
}
|
const [width, setWidth] = useState<number>(0);
|
||||||
};
|
const cropImageElementRef = useRef<HTMLImageElement>(null);
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
||||||
|
|
||||||
const onDownloadCropClick = () => {
|
const handleCropClick = async () => {
|
||||||
if (!previewCanvasRef.current) {
|
if (!completedCrop) throw new Error("No completed crop");
|
||||||
throw new Error("Crop canvas does not exist");
|
if (!cropImageElementRef.current) throw new Error("No image");
|
||||||
}
|
|
||||||
|
|
||||||
const canvasCopy = document.createElement("canvas");
|
const canvasCopy = document.createElement("canvas");
|
||||||
const ctx = canvasCopy.getContext("2d");
|
const ctx = canvasCopy.getContext("2d");
|
||||||
canvasCopy.width = previewCanvasRef.current.width;
|
if (!ctx) throw new Error("No 2d context");
|
||||||
canvasCopy.height = previewCanvasRef.current.height;
|
|
||||||
ctx!.filter = `brightness(${100 - darken}%)`;
|
canvasCopy.width = completedCrop.width;
|
||||||
ctx!.drawImage(previewCanvasRef.current, 0, 0);
|
canvasCopy.height = completedCrop.height;
|
||||||
|
ctx.filter = `brightness(${100 - darken}%)`;
|
||||||
|
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
if (blobUrlRef.current) {
|
const newImageUrl = URL.createObjectURL(blob);
|
||||||
URL.revokeObjectURL(blobUrlRef.current);
|
|
||||||
}
|
|
||||||
blobUrlRef.current = URL.createObjectURL(blob);
|
|
||||||
hiddenAnchorRef.current!.href = blobUrlRef.current;
|
|
||||||
hiddenAnchorRef.current!.click();
|
|
||||||
|
|
||||||
setImgSrc(blobUrlRef.current);
|
setCropModalImageUrl(newImageUrl);
|
||||||
onCropPress?.(blobUrlRef.current);
|
setCrop(undefined);
|
||||||
|
setCompletedCrop(undefined);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useDebounceEffect(
|
function handleSaveClick() {
|
||||||
async () => {
|
if (imageUrl) onSaveImageClick?.(imageUrl);
|
||||||
if (
|
setCrop(undefined);
|
||||||
completedCrop?.width &&
|
setCompletedCrop(undefined);
|
||||||
completedCrop?.height &&
|
closeCropModal();
|
||||||
imgRef.current &&
|
}
|
||||||
previewCanvasRef.current
|
|
||||||
) {
|
|
||||||
canvasPreview(
|
|
||||||
imgRef.current,
|
|
||||||
previewCanvasRef.current,
|
|
||||||
completedCrop,
|
|
||||||
rotate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
100,
|
|
||||||
[completedCrop, rotate]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [width, setWidth] = useState<number>(0);
|
function handleLoadOriginalImage() {
|
||||||
|
const isSuccess = resetToOriginalImage();
|
||||||
|
if (!isSuccess) enqueueSnackbar("Не удалось восстановить оригинал. Приносим глубочайшие извинения");
|
||||||
|
}
|
||||||
|
|
||||||
const getImageSize = () => {
|
const getImageSize = () => {
|
||||||
if (imgRef.current) {
|
if (cropImageElementRef.current) {
|
||||||
const imageWidth = imgRef.current.naturalWidth;
|
const imageWidth = cropImageElementRef.current.naturalWidth;
|
||||||
const imageHeight = imgRef.current.naturalHeight;
|
const imageHeight = cropImageElementRef.current.naturalHeight;
|
||||||
|
|
||||||
const aspect = imageWidth / imageHeight;
|
const aspect = imageWidth / imageHeight;
|
||||||
|
|
||||||
|
|
||||||
if (aspect <= 1.333) {
|
if (aspect <= 1.333) {
|
||||||
setWidth(240);
|
setWidth(240);
|
||||||
}
|
}
|
||||||
@ -171,184 +118,173 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal
|
||||||
<Modal
|
open={isCropModalOpen}
|
||||||
open={opened}
|
onClose={closeCropModal}
|
||||||
onClose={onClose}
|
aria-labelledby="modal-modal-title"
|
||||||
aria-labelledby="modal-modal-title"
|
aria-describedby="modal-modal-description"
|
||||||
aria-describedby="modal-modal-description"
|
>
|
||||||
>
|
<Box sx={{
|
||||||
<Box sx={styleModal}>
|
position: "absolute",
|
||||||
<Box
|
top: "50%",
|
||||||
sx={{
|
left: "50%",
|
||||||
height: "320px",
|
transform: "translate(-50%, -50%)",
|
||||||
padding: "10px",
|
bgcolor: "background.paper",
|
||||||
backgroundSize: "cover",
|
boxShadow: 24,
|
||||||
backgroundRepeat: "no-repeat",
|
padding: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
display: "flex",
|
width: isMobile ? "343px" : "620px",
|
||||||
alignItems: "center",
|
}}>
|
||||||
justifyContent: "center",
|
<Box
|
||||||
}}
|
sx={{
|
||||||
>
|
height: "320px",
|
||||||
{imgSrc && (
|
padding: "10px",
|
||||||
<ReactCrop
|
backgroundSize: "cover",
|
||||||
crop={crop}
|
backgroundRepeat: "no-repeat",
|
||||||
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
display: "flex",
|
||||||
onComplete={(c) => setCompletedCrop(c)}
|
alignItems: "center",
|
||||||
maxWidth={500}
|
justifyContent: "center",
|
||||||
minWidth={50}
|
|
||||||
maxHeight={320}
|
|
||||||
minHeight={50}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
onLoad={getImageSize}
|
|
||||||
ref={imgRef}
|
|
||||||
alt="Crop me"
|
|
||||||
src={imgSrc}
|
|
||||||
style={{
|
|
||||||
filter: `brightness(${100 - darken}%)`,
|
|
||||||
transform: ` rotate(${rotate}deg)`,
|
|
||||||
maxWidth: "580px",
|
|
||||||
maxHeight: "320px",
|
|
||||||
}}
|
|
||||||
width={width}
|
|
||||||
/>
|
|
||||||
</ReactCrop>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
color: "#7E2AEA",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
fontSize: "16xp",
|
|
||||||
fontWeight: "600",
|
|
||||||
marginBottom: "50px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
|
||||||
{crop?.width ? Math.round(crop.width) + "px" : ""}
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
|
||||||
{crop?.height ? Math.round(crop.height) + "px" : ""}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: isMobile ? "block" : "flex",
|
|
||||||
alignItems: "end",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ResetIcon
|
|
||||||
onClick={rotateImage}
|
|
||||||
style={{ marginBottom: "10px", cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
<Box>
|
|
||||||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
|
||||||
Размер
|
|
||||||
</Typography>
|
|
||||||
<Slider
|
|
||||||
sx={styleSlider}
|
|
||||||
value={width}
|
|
||||||
min={50}
|
|
||||||
max={580}
|
|
||||||
step={1}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
setWidth(newValue as number);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
|
||||||
Затемнение
|
|
||||||
</Typography>
|
|
||||||
<Slider
|
|
||||||
sx={styleSlider}
|
|
||||||
value={darken}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onChange={(_, newValue) => setDarken(newValue as number)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
marginTop: "40px",
|
|
||||||
width: "100%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
style={{ display: "none", zIndex: "-999" }}
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={onSelectFile}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
disableRipple
|
|
||||||
sx={{
|
|
||||||
width: "215px",
|
|
||||||
height: "48px",
|
|
||||||
color: "#7E2AEA",
|
|
||||||
borderRadius: "8px",
|
|
||||||
border: "1px solid #7E2AEA",
|
|
||||||
marginRight: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Загрузить оригинал
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={onDownloadCropClick}
|
|
||||||
disableRipple
|
|
||||||
variant="contained"
|
|
||||||
sx={{
|
|
||||||
padding: "10px 20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
background: theme.palette.brightPurple.main,
|
|
||||||
fontSize: "18px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CropIcon />
|
|
||||||
Обрезать
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
{completedCrop && (
|
|
||||||
<div>
|
|
||||||
<canvas
|
|
||||||
ref={previewCanvasRef}
|
|
||||||
style={{
|
|
||||||
display: "none",
|
|
||||||
zIndex: "-999",
|
|
||||||
border: "1px solid black",
|
|
||||||
objectFit: "contain",
|
|
||||||
width: completedCrop.width,
|
|
||||||
height: completedCrop.height,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="#hidden"
|
|
||||||
ref={hiddenAnchorRef}
|
|
||||||
download
|
|
||||||
style={{
|
|
||||||
display: "none",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hidden download
|
{imageUrl && (
|
||||||
</a>
|
<ReactCrop
|
||||||
</div>
|
crop={crop}
|
||||||
</>
|
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||||
|
onComplete={(c) => setCompletedCrop(c)}
|
||||||
|
maxWidth={500}
|
||||||
|
minWidth={50}
|
||||||
|
maxHeight={320}
|
||||||
|
minHeight={50}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
onLoad={getImageSize}
|
||||||
|
ref={cropImageElementRef}
|
||||||
|
alt="Crop me"
|
||||||
|
src={imageUrl}
|
||||||
|
style={{
|
||||||
|
filter: `brightness(${100 - darken}%)`,
|
||||||
|
transform: ` rotate(${rotate}deg)`,
|
||||||
|
maxWidth: "580px",
|
||||||
|
maxHeight: "320px",
|
||||||
|
}}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
</ReactCrop>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
color: "#7E2AEA",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: "16xp",
|
||||||
|
fontWeight: "600",
|
||||||
|
marginBottom: "50px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
||||||
|
{crop?.width ? Math.round(crop.width) + "px" : ""}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
|
||||||
|
{crop?.height ? Math.round(crop.height) + "px" : ""}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: isMobile ? "block" : "flex",
|
||||||
|
alignItems: "end",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton onClick={() => setRotate(r => (r + 90) % 360)}>
|
||||||
|
<ResetIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||||||
|
Размер
|
||||||
|
</Typography>
|
||||||
|
<Slider
|
||||||
|
sx={[styleSlider, {
|
||||||
|
width: isMobile ? "350px" : "250px",
|
||||||
|
}]}
|
||||||
|
value={width}
|
||||||
|
min={50}
|
||||||
|
max={580}
|
||||||
|
step={1}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
setWidth(newValue as number);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||||||
|
Затемнение
|
||||||
|
</Typography>
|
||||||
|
<Slider
|
||||||
|
sx={[styleSlider, {
|
||||||
|
width: isMobile ? "350px" : "250px",
|
||||||
|
}]}
|
||||||
|
value={darken}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onChange={(_, newValue) => setDarken(newValue as number)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: "40px",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveClick}
|
||||||
|
disableRipple
|
||||||
|
sx={{
|
||||||
|
height: "48px",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #7E2AEA",
|
||||||
|
marginRight: "10px",
|
||||||
|
px: "20px",
|
||||||
|
}}
|
||||||
|
>Сохранить</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleLoadOriginalImage}
|
||||||
|
disableRipple
|
||||||
|
sx={{
|
||||||
|
width: "215px",
|
||||||
|
height: "48px",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #7E2AEA",
|
||||||
|
marginRight: "10px",
|
||||||
|
ml: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Загрузить оригинал
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCropClick}
|
||||||
|
disableRipple
|
||||||
|
variant="contained"
|
||||||
|
disabled={!completedCrop}
|
||||||
|
sx={{
|
||||||
|
padding: "10px 20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
background: theme.palette.brightPurple.main,
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CropIcon />
|
||||||
|
Обрезать
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Box, Button } from "@mui/material";
|
import { Box, Button } from "@mui/material";
|
||||||
import { FC, useState } from "react";
|
import { FC } from "react";
|
||||||
import { CropModal } from "./CropModal";
|
import { CropModal } from "./CropModal";
|
||||||
|
import { openCropModal } from "@root/cropModal";
|
||||||
|
|
||||||
const ImageCrop: FC = () => {
|
const ImageCrop: FC = () => {
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Button onClick={() => setOpened(true)}>Открыть модалку</Button>
|
<Button onClick={() => openCropModal("", "")}>Открыть модалку</Button>
|
||||||
<CropModal opened={opened} onClose={() => setOpened(false)} />
|
<CropModal />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,15 +7,17 @@ interface QuestionsMiniButtonProps {
|
|||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
text: string;
|
text: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
dataCy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuestionsMiniButton({ icon, text, onClick }: QuestionsMiniButtonProps) {
|
export default function QuestionsMiniButton({ icon, text, onClick, dataCy }: QuestionsMiniButtonProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
data-cy={dataCy}
|
||||||
sx={{
|
sx={{
|
||||||
padding: "26px 15px 15px 15px",
|
padding: "26px 15px 15px 15px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import { Box, IconButton } from "@mui/material";
|
import { Box, IconButton } from "@mui/material";
|
||||||
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
|
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
|
||||||
import { useLayoutEffect, useRef } from "react";
|
import { useLayoutEffect, useRef } from "react";
|
||||||
import { Rnd } from "react-rnd";
|
import { Rnd } from "react-rnd";
|
||||||
|
import { useWindowSize } from "../../utils/hooks/useWindowSize";
|
||||||
import QuizPreviewLayout from "./QuizPreviewLayout";
|
import QuizPreviewLayout from "./QuizPreviewLayout";
|
||||||
import ResizeIcon from "./ResizeIcon";
|
import ResizeIcon from "./ResizeIcon";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
||||||
|
|
||||||
const DRAG_PARENT_MARGIN = 0;
|
const DRAG_PARENT_MARGIN = 0;
|
||||||
const NAVBAR_HEIGHT = 0;
|
const NAVBAR_HEIGHT = 0;
|
||||||
|
|||||||
@ -160,4 +160,5 @@ export default function QuizPreviewLayout() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,32 +23,41 @@ export default function Variant({ question }: Props) {
|
|||||||
setValue((event.target as HTMLInputElement).value);
|
setValue((event.target as HTMLInputElement).value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
|
<FormLabel
|
||||||
<RadioGroup
|
id="quiz-question-radio-group"
|
||||||
aria-labelledby="quiz-question-radio-group"
|
data-cy="variant-title"
|
||||||
value={value}
|
>{question.title}</FormLabel>
|
||||||
onChange={handleChange}
|
<RadioGroup
|
||||||
>
|
aria-labelledby="quiz-question-radio-group"
|
||||||
{question.content.variants.map((variant, index) => (
|
value={value}
|
||||||
<FormControlLabel
|
onChange={handleChange}
|
||||||
key={index}
|
>
|
||||||
value={variant.answer}
|
{question.content.variants.map((variant, index) => (
|
||||||
control={<Radio />}
|
<FormControlLabel
|
||||||
label={
|
key={index}
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
value={variant.answer}
|
||||||
<Typography>{variant.answer}</Typography>
|
control={<Radio
|
||||||
<Tooltip title="Подсказка" placement="right">
|
inputProps={{
|
||||||
<Box>
|
"data-cy": "variant-radio",
|
||||||
<InfoIcon />
|
}}
|
||||||
</Box>
|
/>}
|
||||||
</Tooltip>
|
label={
|
||||||
</Box>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
}
|
<Typography
|
||||||
/>
|
data-cy="variant-answer"
|
||||||
))}
|
>{variant.answer}</Typography>
|
||||||
</RadioGroup>
|
<Tooltip title={variant.hints} placement="right">
|
||||||
</FormControl>
|
<Box>
|
||||||
);
|
<InfoIcon />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/utils/hooks/useWindowSize.ts
Normal file
35
src/utils/hooks/useWindowSize.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
interface WindowSize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWindowSize(): WindowSize {
|
||||||
|
const [windowSize, setWindowSize] = useState<WindowSize>({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSize = () => {
|
||||||
|
setWindowSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("resize", handleSize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", handleSize);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
handleSize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return windowSize;
|
||||||
|
}
|
||||||
@ -19,9 +19,14 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"cypress.config.ts",
|
||||||
|
"cypress",
|
||||||
|
"node_modules",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user