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
13
package.json
13
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,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
QuizQuestionBase,
|
ImageQuestionVariant,
|
||||||
QuestionVariant,
|
|
||||||
QuestionHint,
|
|
||||||
QuestionBranchingRule,
|
QuestionBranchingRule,
|
||||||
|
QuestionHint,
|
||||||
|
QuizQuestionBase
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
export interface QuizQuestionImages extends QuizQuestionBase {
|
export interface QuizQuestionImages extends QuizQuestionBase {
|
||||||
@ -25,10 +25,11 @@ export interface QuizQuestionImages extends QuizQuestionBase {
|
|||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
/** Варианты (картинки) */
|
/** Варианты (картинки) */
|
||||||
variants: QuestionVariant[];
|
variants: ImageQuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
largeCheck: 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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,10 +32,17 @@ export interface QuestionHint {
|
|||||||
export type QuestionVariant = {
|
export type QuestionVariant = {
|
||||||
/** Текст */
|
/** Текст */
|
||||||
answer: string;
|
answer: string;
|
||||||
|
/** Текст подсказки */
|
||||||
|
hints: string;
|
||||||
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
||||||
extendedText: string;
|
extendedText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ImageQuestionVariant extends QuestionVariant {
|
||||||
|
/** Оригинал изображения (до кропа) */
|
||||||
|
originalImageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuizQuestionBase {
|
export interface QuizQuestionBase {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@ -48,6 +55,7 @@ export interface QuizQuestionBase {
|
|||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
QuizQuestionBase,
|
ImageQuestionVariant,
|
||||||
QuestionVariant,
|
|
||||||
QuestionHint,
|
|
||||||
QuestionBranchingRule,
|
QuestionBranchingRule,
|
||||||
|
QuestionHint,
|
||||||
|
QuizQuestionBase
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||||
@ -16,10 +16,11 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
|
|||||||
innerName: string;
|
innerName: string;
|
||||||
/** Чекбокс "Необязательный вопрос" */
|
/** Чекбокс "Необязательный вопрос" */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
variants: QuestionVariant[];
|
variants: ImageQuestionVariant[];
|
||||||
hint: QuestionHint;
|
hint: QuestionHint;
|
||||||
rule: QuestionBranchingRule;
|
rule: QuestionBranchingRule;
|
||||||
back: string;
|
back: string;
|
||||||
|
originalBack: string;
|
||||||
autofill: boolean;
|
autofill: boolean;
|
||||||
largeCheck: boolean;
|
largeCheck: boolean;
|
||||||
replText: string;
|
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,8 +93,20 @@ 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}>
|
||||||
|
{(provided) => (
|
||||||
|
<Box ref={provided.innerRef} {...provided.draggableProps}>
|
||||||
<FormControl
|
<FormControl
|
||||||
key={index}
|
key={index}
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -104,14 +133,49 @@ export const AnswerItem = ({
|
|||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<>
|
<>
|
||||||
<InputAdornment {...provided.droppableProps} position="start">
|
<InputAdornment
|
||||||
<PointsIcon style={{ color: "#9A9AAF", fontSize: "30px" }} />
|
{...provided.dragHandleProps}
|
||||||
|
position="start"
|
||||||
|
>
|
||||||
|
<PointsIcon
|
||||||
|
style={{ color: "#9A9AAF", fontSize: "30px" }}
|
||||||
|
/>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
{additionalContent}
|
{additionalContent}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<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}>
|
<IconButton sx={{ padding: "0" }} onClick={deleteAnswer}>
|
||||||
<DeleteIcon
|
<DeleteIcon
|
||||||
style={{
|
style={{
|
||||||
@ -125,11 +189,7 @@ export const AnswerItem = ({
|
|||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
"& .MuiInputBase-root": {
|
"& .MuiInputBase-root": {
|
||||||
padding: additionalContent
|
padding: additionalContent ? "5px 13px" : "13px",
|
||||||
? isTablet
|
|
||||||
? "13px"
|
|
||||||
: "5px 13px"
|
|
||||||
: "13px",
|
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
background: "#ffffff",
|
background: "#ffffff",
|
||||||
"& input.MuiInputBase-input": {
|
"& input.MuiInputBase-input": {
|
||||||
@ -150,5 +210,8 @@ export const AnswerItem = ({
|
|||||||
{additionalMobile}
|
{additionalMobile}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</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,35 +11,32 @@ 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);
|
||||||
@ -54,22 +50,22 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
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 [file] = Array.from(files);
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
const clonedContent = { ...question.content };
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
clonedContent.variants[currentIndex].extendedText =
|
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
URL.createObjectURL(file);
|
setIsUploadImageModalOpen(false);
|
||||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
openCropModal(url, url);
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
setOpened(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||||
@ -79,107 +75,217 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
sx={{ cursor: "pointer" }}
|
imageSrc={variant.extendedText}
|
||||||
onClick={() => {
|
onImageClick={() => {
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setOpen(true);
|
if (variant.extendedText) return openCropModal(
|
||||||
|
variant.extendedText,
|
||||||
|
variant.originalImageUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
onPlusClick={() => {
|
||||||
{variant.extendedText ? (
|
setCurrentIndex(index);
|
||||||
<Box
|
setIsUploadImageModalOpen(true);
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "60px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
borderRadius: "3px",
|
|
||||||
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 && (
|
{isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
onClick={() => {
|
imageSrc={variant.extendedText}
|
||||||
|
onImageClick={() => {
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setOpen(true);
|
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": {
|
||||||
|
padding: "13.5px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: "#ffffff",
|
||||||
|
height: "48px",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
sx: { fontSize: "18px", lineHeight: "21px", py: 0 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isMobile && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
m: "8px",
|
m: "8px",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
borderRadius: "3px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
|
||||||
|
/>
|
||||||
|
<ImageAddIcons
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
fontSize: "20px",
|
||||||
|
left: "45%",
|
||||||
|
right: "55%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
height: "40px",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
|
||||||
>
|
|
||||||
{variant.extendedText ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "40px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
width: "20px",
|
||||||
background: "#EEE4FC",
|
background: "#EEE4FC",
|
||||||
height: "30px",
|
height: "40px",
|
||||||
borderRadius: "3px",
|
color: "white",
|
||||||
|
backgroundColor: "#7E2AEA",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<Box
|
||||||
src={variant.extendedText}
|
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
|
||||||
alt=""
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
<ImageAddIcons
|
||||||
) : (
|
style={{
|
||||||
<Button component="label" sx={{ padding: "0px" }}>
|
position: "absolute",
|
||||||
<Image
|
color: "#7E2AEA",
|
||||||
sx={{
|
fontSize: "20px",
|
||||||
height: "40px",
|
left: "45%",
|
||||||
width: "60px",
|
right: "55%",
|
||||||
margin: "0 10px",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -195,28 +301,9 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
+
|
+
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</Box>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<UploadImageModal
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
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
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -238,7 +325,9 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
const clonedContent = { ...question.content };
|
const clonedContent = { ...question.content };
|
||||||
clonedContent.variants.push({
|
clonedContent.variants.push({
|
||||||
answer: "",
|
answer: "",
|
||||||
|
hints: "",
|
||||||
extendedText: "",
|
extendedText: "",
|
||||||
|
originalImageUrl: "",
|
||||||
});
|
});
|
||||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
||||||
content: clonedContent,
|
content: clonedContent,
|
||||||
@ -277,5 +366,6 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
|||||||
/>
|
/>
|
||||||
<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,33 +6,31 @@ 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 isTablet = useMediaQuery(theme.breakpoints.down(790));
|
|
||||||
const quizId = Number(useParams().quizId);
|
const quizId = Number(useParams().quizId);
|
||||||
const [switchState, setSwitchState] = useState("setting");
|
const [switchState, setSwitchState] = useState("setting");
|
||||||
const { listQuestions } = questionStore();
|
const { listQuestions } = questionStore();
|
||||||
@ -44,31 +40,31 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
|||||||
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 [file] = Array.from(files);
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
const clonedContent = { ...question.content };
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
clonedContent.variants[currentIndex].extendedText =
|
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
URL.createObjectURL(file);
|
setIsUploadImageModalOpen(false);
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
openCropModal(url, url);
|
||||||
content: clonedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
setOpened(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
@ -78,140 +74,64 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
|||||||
additionalContent={(variant, index) => (
|
additionalContent={(variant, index) => (
|
||||||
<>
|
<>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
sx={{ cursor: "pointer" }}
|
imageSrc={variant.extendedText}
|
||||||
onClick={() => {
|
onImageClick={() => {
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setOpen(true);
|
if (variant.extendedText) {
|
||||||
|
return openCropModal(
|
||||||
|
variant.extendedText,
|
||||||
|
variant.originalImageUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
onPlusClick={() => {
|
||||||
{variant.extendedText ? (
|
setCurrentIndex(index);
|
||||||
<Box
|
setIsUploadImageModalOpen(true);
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
width: "60px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
borderRadius: "3px",
|
|
||||||
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 && (
|
{isMobile && (
|
||||||
<Box
|
<AddOrEditImageButton
|
||||||
onClick={() => {
|
imageSrc={variant.extendedText}
|
||||||
|
onImageClick={() => {
|
||||||
|
if (!("originalImageUrl" in variant)) return;
|
||||||
|
|
||||||
setCurrentIndex(index);
|
setCurrentIndex(index);
|
||||||
setOpen(true);
|
if (variant.extendedText) {
|
||||||
|
return openCropModal(
|
||||||
|
variant.extendedText,
|
||||||
|
variant.originalImageUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploadImageModalOpen(true);
|
||||||
}}
|
}}
|
||||||
sx={{
|
onPlusClick={() => {
|
||||||
overflow: "hidden",
|
setCurrentIndex(index);
|
||||||
display: "flex",
|
setIsUploadImageModalOpen(true);
|
||||||
alignItems: "center",
|
|
||||||
m: "8px",
|
|
||||||
position: "relative",
|
|
||||||
borderRadius: "3px",
|
|
||||||
}}
|
}}
|
||||||
>
|
sx={{ m: "8px", width: "auto" }}
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
background: "#EEE4FC",
|
|
||||||
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
|
<UploadImageModal
|
||||||
open={open}
|
open={isUploadImageModalOpen}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
imgHC={uploadImage}
|
imgHC={handleImageUpload}
|
||||||
/>
|
|
||||||
<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<QuizQuestionImages>(quizId, totalIndex, {
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
@ -244,15 +164,9 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</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,20 +1,18 @@
|
|||||||
|
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 = {
|
||||||
@ -43,6 +41,21 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
|||||||
setSwitchState(data);
|
setSwitchState(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleImageUpload(fileList: FileList | null) {
|
||||||
|
if (!fileList?.length) return;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(fileList[0]);
|
||||||
|
|
||||||
|
setPageQuestionPicture(quizId, totalIndex, url);
|
||||||
|
setPageQuestionOriginalPicture(quizId, totalIndex, url);
|
||||||
|
setOpenImageModal(false);
|
||||||
|
openCropModal(url, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setPageQuestionPicture(quizId, totalIndex, url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
@ -74,7 +87,6 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setOpenImageModal(true)}
|
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -82,80 +94,22 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
|||||||
gap: "20px",
|
gap: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isMobile ? (
|
<AddOrEditImageButton
|
||||||
<Box
|
imageSrc={question.content.picture}
|
||||||
sx={{
|
onImageClick={() => {
|
||||||
display: "flex",
|
if (question.content.picture) {
|
||||||
alignItems: "center",
|
return openCropModal(
|
||||||
width: "120px",
|
question.content.picture,
|
||||||
position: "relative",
|
question.content.originalPicture
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpenImageModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
onPlusClick={() => {
|
||||||
<Box
|
setOpenImageModal(true);
|
||||||
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
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
@ -172,18 +126,9 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
|||||||
<UploadImageModal
|
<UploadImageModal
|
||||||
open={openImageModal}
|
open={openImageModal}
|
||||||
onClose={() => setOpenImageModal(false)}
|
onClose={() => setOpenImageModal(false)}
|
||||||
imgHC={(fileList) => {
|
imgHC={handleImageUpload}
|
||||||
if (fileList?.length) {
|
|
||||||
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
picture: URL.createObjectURL(fileList[0]),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// onClick={() => setOpenVideoModal(true)}
|
|
||||||
/>
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
<Typography> или</Typography>
|
<Typography> или</Typography>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -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,15 +1,15 @@
|
|||||||
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;
|
||||||
@ -18,36 +18,34 @@ type UploadImageProps = {
|
|||||||
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 handleImageUpload = (files: FileList | null) => {
|
||||||
|
if (!files?.length) return;
|
||||||
|
|
||||||
const handleOpen = () => setOpen(true);
|
|
||||||
const handleClose = () => setOpen(false);
|
|
||||||
const imgHC = (files: FileList | null) => {
|
|
||||||
if (files?.length) {
|
|
||||||
const [file] = Array.from(files);
|
const [file] = Array.from(files);
|
||||||
|
|
||||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
const url = URL.createObjectURL(file);
|
||||||
content: {
|
|
||||||
...question.content,
|
|
||||||
back: URL.createObjectURL(file),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
handleClose();
|
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||||
setOpened(true);
|
setQuestionOriginalBackgroundImage(quizId, totalIndex, url);
|
||||||
}
|
setIsUploadImageModalOpen(false);
|
||||||
|
openCropModal(url, url);
|
||||||
};
|
};
|
||||||
const [opened, setOpened] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
imgHC(event.dataTransfer.files);
|
handleImageUpload(event.dataTransfer.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleCropModalSaveClick(url: string) {
|
||||||
|
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: "20px" }}>
|
<Box sx={{ padding: "20px" }}>
|
||||||
<Typography
|
<Typography
|
||||||
@ -61,22 +59,39 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
|
|||||||
Загрузить изображение
|
Загрузить изображение
|
||||||
</Typography>
|
</Typography>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
onClick={handleOpen}
|
onClick={() => setIsUploadImageModalOpen(true)}
|
||||||
sx={{ width: "100%", maxWidth: "260px" }}
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "260px",
|
||||||
|
height: "120px",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{question.content.back ?
|
||||||
|
<img
|
||||||
|
src={question.content.back}
|
||||||
|
alt="question background"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "scale-down",
|
||||||
|
display: "block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
:
|
||||||
<UploadBox
|
<UploadBox
|
||||||
handleDrop={handleDrop}
|
handleDrop={handleDrop}
|
||||||
sx={{ maxWidth: "260px" }}
|
sx={{ maxWidth: "260px" }}
|
||||||
icon={<UploadIcon />}
|
icon={<UploadIcon />}
|
||||||
text="5 MB максимум"
|
text="5 MB максимум"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<UploadImageModal open={open} onClose={handleClose} imgHC={imgHC} />
|
<UploadImageModal
|
||||||
<CropModal
|
open={isUploadImageModalOpen}
|
||||||
opened={opened}
|
onClose={() => setIsUploadImageModalOpen(false)}
|
||||||
onClose={() => setOpened(false)}
|
imgHC={handleImageUpload}
|
||||||
picture={question.content.back}
|
|
||||||
/>
|
/>
|
||||||
|
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||||
</Box>
|
</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,8 +41,7 @@ const stylesheet: Stylesheet[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Graph = () => {
|
export const Graph = () => (
|
||||||
return (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
@ -59,11 +53,10 @@ export const Graph = () => {
|
|||||||
>
|
>
|
||||||
<Cytoscape
|
<Cytoscape
|
||||||
wheelSensitivity={0.1}
|
wheelSensitivity={0.1}
|
||||||
elements={ELEMENTS}
|
elements={createGraphElements(VERTICES)}
|
||||||
style={{ height: "480px", background: "#F2F3F7" }}
|
style={{ height: "480px", background: "#F2F3F7" }}
|
||||||
layout={{ name: "breadthfirst" }}
|
layout={{ name: "preset" }}
|
||||||
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,10 +848,9 @@ export default function StartPageSettings() {
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Box>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ display: "block", marginLeft: "auto" }}
|
data-cy="setup-questions"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let SPageClone = listQuizes[params].config;
|
let SPageClone = listQuizes[params].config;
|
||||||
SPageClone.startpage.background.desktop =
|
SPageClone.startpage.background.desktop =
|
||||||
@ -868,7 +867,6 @@ export default function StartPageSettings() {
|
|||||||
Настроить вопросы
|
Настроить вопросы
|
||||||
</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,7 +19,6 @@ 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);
|
||||||
|
|
||||||
@ -36,11 +30,19 @@ interface QuestionStore {
|
|||||||
let isFirstPartialize = true;
|
let isFirstPartialize = true;
|
||||||
|
|
||||||
export const questionStore = create<QuestionStore>()(
|
export const questionStore = create<QuestionStore>()(
|
||||||
persist<QuestionStore>(
|
persist(
|
||||||
|
devtools(
|
||||||
() => ({
|
() => ({
|
||||||
listQuestions: {},
|
listQuestions: {},
|
||||||
openedModalSettings: "",
|
openedModalSettings: "",
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
name: "Question",
|
||||||
|
enabled: process.env.NODE_ENV === "development",
|
||||||
|
trace: process.env.NODE_ENV === "development",
|
||||||
|
actionsBlacklist: "ignored",
|
||||||
|
}
|
||||||
|
),
|
||||||
{
|
{
|
||||||
name: "question",
|
name: "question",
|
||||||
partialize: (state: QuestionStore) => {
|
partialize: (state: QuestionStore) => {
|
||||||
@ -66,26 +68,31 @@ export const questionStore = create<QuestionStore>()(
|
|||||||
const state = persistedState as QuestionStore;
|
const state = persistedState as QuestionStore;
|
||||||
|
|
||||||
// replace blob urls with ""
|
// replace blob urls with ""
|
||||||
Object.values(state.listQuestions).forEach((questions) => {
|
Object.values(state.listQuestions).forEach(questions => {
|
||||||
questions.forEach((question) => {
|
questions.forEach(question => {
|
||||||
if (
|
if (question.type === "page") {
|
||||||
question.type === "page" &&
|
if (question.content.picture.startsWith("blob:")) {
|
||||||
question.content.picture.startsWith("blob:")
|
|
||||||
) {
|
|
||||||
question.content.picture = "";
|
question.content.picture = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (question.type === "images") {
|
if (question.type === "images") {
|
||||||
question.content.variants.forEach((variant) => {
|
question.content.variants.forEach(variant => {
|
||||||
if (variant.extendedText.startsWith("blob:")) {
|
if (variant.extendedText.startsWith("blob:")) {
|
||||||
variant.extendedText = "";
|
variant.extendedText = "";
|
||||||
}
|
}
|
||||||
|
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||||
|
variant.originalImageUrl = "";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (question.type === "varimg") {
|
if (question.type === "varimg") {
|
||||||
question.content.variants.forEach((variant) => {
|
question.content.variants.forEach(variant => {
|
||||||
if (variant.extendedText.startsWith("blob:")) {
|
if (variant.extendedText.startsWith("blob:")) {
|
||||||
variant.extendedText = "";
|
variant.extendedText = "";
|
||||||
}
|
}
|
||||||
|
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||||
|
variant.originalImageUrl = "";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -112,6 +119,131 @@ export const updateQuestionsList = <T = AnyQuizQuestion>(
|
|||||||
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[]
|
||||||
@ -122,35 +254,27 @@ export const updateQuestionsListDragAndDrop = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateVariants = (
|
|
||||||
|
export const reorderVariants = (
|
||||||
quizId: number,
|
quizId: number,
|
||||||
index: number,
|
questionIndex: number,
|
||||||
variants: QuestionVariant[]
|
sourceIndex: number,
|
||||||
) => {
|
destinationIndex: number,
|
||||||
const listQuestions = { ...questionStore.getState()["listQuestions"] };
|
) => setProducedState(state => {
|
||||||
|
if (sourceIndex === destinationIndex) return;
|
||||||
|
|
||||||
switch (listQuestions[quizId][index].type) {
|
const question = state.listQuestions[quizId][questionIndex];
|
||||||
case "emoji":
|
if (!("variants" in question.content)) return;
|
||||||
const emojiState = listQuestions[quizId][index] as QuizQuestionEmoji;
|
|
||||||
emojiState.content.variants = variants;
|
|
||||||
return questionStore.setState({ listQuestions });
|
|
||||||
|
|
||||||
case "images":
|
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
||||||
const imagesState = listQuestions[quizId][index] as QuizQuestionImages;
|
question.content.variants.splice(destinationIndex, 0, removed);
|
||||||
imagesState.content.variants = variants;
|
}, {
|
||||||
return questionStore.setState({ listQuestions });
|
type: sourceIndex === destinationIndex ? "reorderVariants" : "ignored",
|
||||||
|
quizId,
|
||||||
case "variant":
|
questionIndex,
|
||||||
const variantState = listQuestions[quizId][index] as QuizQuestionVariant;
|
sourceIndex,
|
||||||
variantState.content.variants = variants;
|
destinationIndex,
|
||||||
return questionStore.setState({ listQuestions });
|
});
|
||||||
|
|
||||||
case "varimg":
|
|
||||||
const varImgState = listQuestions[quizId][index] as QuizQuestionVarImg;
|
|
||||||
varImgState.content.variants = variants;
|
|
||||||
return questionStore.setState({ listQuestions });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createQuestion = (
|
export const createQuestion = (
|
||||||
quizId: number,
|
quizId: number,
|
||||||
@ -183,7 +307,7 @@ export const createQuestion = (
|
|||||||
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 });
|
||||||
@ -230,6 +354,7 @@ export const findQuestionById = (quizId: number) => {
|
|||||||
found = { quiz, index };
|
found = { quiz, index };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
@ -239,3 +364,10 @@ function getRandom() {
|
|||||||
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,66 +1,26 @@
|
|||||||
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("");
|
|
||||||
|
|
||||||
const imgRef = useRef<HTMLImageElement>(null);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const hiddenAnchorRef = useRef<HTMLAnchorElement>(null);
|
|
||||||
const blobUrlRef = useRef("");
|
|
||||||
const [crop, setCrop] = useState<Crop>();
|
|
||||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
|
||||||
const [rotate, setRotate] = useState(0);
|
|
||||||
const [darken, setDarken] = useState(0);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(786));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (picture) {
|
|
||||||
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",
|
color: "#7E2AEA",
|
||||||
height: "12px",
|
height: "12px",
|
||||||
"& .MuiSlider-track": {
|
"& .MuiSlider-track": {
|
||||||
@ -82,82 +42,69 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
0px 4px 4px 3px #C3C8DD`,
|
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,27 +118,35 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Modal
|
<Modal
|
||||||
open={opened}
|
open={isCropModalOpen}
|
||||||
onClose={onClose}
|
onClose={closeCropModal}
|
||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
<Box sx={styleModal}>
|
<Box sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
boxShadow: 24,
|
||||||
|
padding: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
width: isMobile ? "343px" : "620px",
|
||||||
|
}}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
height: "320px",
|
height: "320px",
|
||||||
padding: "10px",
|
padding: "10px",
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: "no-repeat",
|
||||||
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imgSrc && (
|
{imageUrl && (
|
||||||
<ReactCrop
|
<ReactCrop
|
||||||
crop={crop}
|
crop={crop}
|
||||||
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||||
@ -203,9 +158,9 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
onLoad={getImageSize}
|
onLoad={getImageSize}
|
||||||
ref={imgRef}
|
ref={cropImageElementRef}
|
||||||
alt="Crop me"
|
alt="Crop me"
|
||||||
src={imgSrc}
|
src={imageUrl}
|
||||||
style={{
|
style={{
|
||||||
filter: `brightness(${100 - darken}%)`,
|
filter: `brightness(${100 - darken}%)`,
|
||||||
transform: ` rotate(${rotate}deg)`,
|
transform: ` rotate(${rotate}deg)`,
|
||||||
@ -243,16 +198,17 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResetIcon
|
<IconButton onClick={() => setRotate(r => (r + 90) % 360)}>
|
||||||
onClick={rotateImage}
|
<ResetIcon />
|
||||||
style={{ marginBottom: "10px", cursor: "pointer" }}
|
</IconButton>
|
||||||
/>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||||||
Размер
|
Размер
|
||||||
</Typography>
|
</Typography>
|
||||||
<Slider
|
<Slider
|
||||||
sx={styleSlider}
|
sx={[styleSlider, {
|
||||||
|
width: isMobile ? "350px" : "250px",
|
||||||
|
}]}
|
||||||
value={width}
|
value={width}
|
||||||
min={50}
|
min={50}
|
||||||
max={580}
|
max={580}
|
||||||
@ -267,7 +223,9 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
Затемнение
|
Затемнение
|
||||||
</Typography>
|
</Typography>
|
||||||
<Slider
|
<Slider
|
||||||
sx={styleSlider}
|
sx={[styleSlider, {
|
||||||
|
width: isMobile ? "350px" : "250px",
|
||||||
|
}]}
|
||||||
value={darken}
|
value={darken}
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
@ -281,18 +239,22 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
marginTop: "40px",
|
marginTop: "40px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "end",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
|
||||||
style={{ display: "none", zIndex: "-999" }}
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={onSelectFile}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={handleSaveClick}
|
||||||
|
disableRipple
|
||||||
|
sx={{
|
||||||
|
height: "48px",
|
||||||
|
color: "#7E2AEA",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #7E2AEA",
|
||||||
|
marginRight: "10px",
|
||||||
|
px: "20px",
|
||||||
|
}}
|
||||||
|
>Сохранить</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleLoadOriginalImage}
|
||||||
disableRipple
|
disableRipple
|
||||||
sx={{
|
sx={{
|
||||||
width: "215px",
|
width: "215px",
|
||||||
@ -301,14 +263,16 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
border: "1px solid #7E2AEA",
|
border: "1px solid #7E2AEA",
|
||||||
marginRight: "10px",
|
marginRight: "10px",
|
||||||
|
ml: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Загрузить оригинал
|
Загрузить оригинал
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onDownloadCropClick}
|
onClick={handleCropClick}
|
||||||
disableRipple
|
disableRipple
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={!completedCrop}
|
||||||
sx={{
|
sx={{
|
||||||
padding: "10px 20px",
|
padding: "10px 20px",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
@ -322,33 +286,5 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</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
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,10 @@ export default function Variant({ question }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<FormLabel id="quiz-question-radio-group">{question.title}</FormLabel>
|
<FormLabel
|
||||||
|
id="quiz-question-radio-group"
|
||||||
|
data-cy="variant-title"
|
||||||
|
>{question.title}</FormLabel>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
aria-labelledby="quiz-question-radio-group"
|
aria-labelledby="quiz-question-radio-group"
|
||||||
value={value}
|
value={value}
|
||||||
@ -35,11 +38,17 @@ export default function Variant({ question }: Props) {
|
|||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={index}
|
key={index}
|
||||||
value={variant.answer}
|
value={variant.answer}
|
||||||
control={<Radio />}
|
control={<Radio
|
||||||
|
inputProps={{
|
||||||
|
"data-cy": "variant-radio",
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
label={
|
label={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||||
<Typography>{variant.answer}</Typography>
|
<Typography
|
||||||
<Tooltip title="Подсказка" placement="right">
|
data-cy="variant-answer"
|
||||||
|
>{variant.answer}</Typography>
|
||||||
|
<Tooltip title={variant.hints} placement="right">
|
||||||
<Box>
|
<Box>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
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