merge graph && quiz-preview-e2e-tests
This commit is contained in:
commit
aefc939965
2
.gitignore
vendored
2
.gitignore
vendored
@ -21,3 +21,5 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.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",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject"
|
||||
"eject": "craco eject",
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [
|
||||
"warn",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@ -24,6 +24,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionInitial, "id"> = {
|
||||
],
|
||||
},
|
||||
back: "",
|
||||
originalBack: "",
|
||||
autofill: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id"> = {
|
||||
{
|
||||
answer: "",
|
||||
extendedText: "",
|
||||
hints: ""
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -19,6 +19,8 @@ export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id"> = {
|
||||
{
|
||||
answer: "",
|
||||
extendedText: "",
|
||||
originalImageUrl: "",
|
||||
hints: ""
|
||||
},
|
||||
],
|
||||
largeCheck: false,
|
||||
|
||||
@ -11,6 +11,7 @@ export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id"> = {
|
||||
innerName: "",
|
||||
text: "",
|
||||
picture: "",
|
||||
originalPicture: "",
|
||||
video: "",
|
||||
},
|
||||
};
|
||||
|
||||
@ -12,6 +12,6 @@ export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id"> = {
|
||||
innerNameCheck: false,
|
||||
innerName: "",
|
||||
default: "",
|
||||
variants: [{ answer: "", extendedText: "" }],
|
||||
variants: [{ answer: "", extendedText: "", hints: "" }],
|
||||
},
|
||||
};
|
||||
|
||||
@ -13,6 +13,6 @@ export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id"> = {
|
||||
innerNameCheck: false,
|
||||
required: false,
|
||||
innerName: "",
|
||||
variants: [{ answer: "", extendedText: "" }],
|
||||
variants: [{ answer: "", extendedText: "", hints: "" }],
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id"> = {
|
||||
innerNameCheck: false,
|
||||
innerName: "",
|
||||
required: false,
|
||||
variants: [{ answer: "", extendedText: "" }],
|
||||
variants: [{ answer: "", hints: "", extendedText: "", originalImageUrl: "" }],
|
||||
largeCheck: false,
|
||||
replText: "",
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import "./index.css";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import lightTheme from "./utils/themes/light";
|
||||
import { ThemeProvider } from "@mui/material";
|
||||
import { CssBaseline, ThemeProvider } from "@mui/material";
|
||||
import StartPage from "./pages/startPage/StartPage";
|
||||
import Main from "./pages/main";
|
||||
import QuestionsPage from "./pages/Questions/QuestionsPage";
|
||||
@ -50,6 +50,7 @@ root.render(
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<SnackbarProvider>
|
||||
<CssBaseline />
|
||||
<ContactFormModal />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
@ -18,6 +18,7 @@ export interface QuizQuestionDate extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ export interface QuizQuestionEmoji extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -29,5 +29,6 @@ export interface QuizQuestionFile extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type {
|
||||
QuizQuestionBase,
|
||||
QuestionVariant,
|
||||
QuestionHint,
|
||||
ImageQuestionVariant,
|
||||
QuestionBranchingRule,
|
||||
QuestionHint,
|
||||
QuizQuestionBase
|
||||
} from "./shared";
|
||||
|
||||
export interface QuizQuestionImages extends QuizQuestionBase {
|
||||
@ -25,10 +25,11 @@ export interface QuizQuestionImages extends QuizQuestionBase {
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
/** Варианты (картинки) */
|
||||
variants: QuestionVariant[];
|
||||
variants: ImageQuestionVariant[];
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
largeCheck: boolean;
|
||||
};
|
||||
|
||||
@ -27,6 +27,7 @@ export interface QuizQuestionNumber extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
|
||||
};
|
||||
|
||||
@ -13,10 +13,12 @@ export interface QuizQuestionPage extends QuizQuestionBase {
|
||||
innerName: string;
|
||||
text: string;
|
||||
picture: string;
|
||||
originalPicture: string;
|
||||
video: string;
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export interface QuizQuestionRating extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
/** Позитивное описание рейтинга */
|
||||
ratingPositiveDescription: string;
|
||||
|
||||
@ -22,6 +22,7 @@ export interface QuizQuestionSelect extends QuizQuestionBase {
|
||||
rule: QuestionBranchingRule;
|
||||
hint: QuestionHint;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -32,10 +32,17 @@ export interface QuestionHint {
|
||||
export type QuestionVariant = {
|
||||
/** Текст */
|
||||
answer: string;
|
||||
/** Текст подсказки */
|
||||
hints: string;
|
||||
/** Дополнительное поле для текста, emoji, ссылки на картинку */
|
||||
extendedText: string;
|
||||
};
|
||||
|
||||
export interface ImageQuestionVariant extends QuestionVariant {
|
||||
/** Оригинал изображения (до кропа) */
|
||||
originalImageUrl: string;
|
||||
}
|
||||
|
||||
export interface QuizQuestionBase {
|
||||
id: number;
|
||||
title: string;
|
||||
@ -48,6 +55,7 @@ export interface QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,5 +20,6 @@ export interface QuizQuestionText extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type {
|
||||
QuizQuestionBase,
|
||||
QuestionVariant,
|
||||
QuestionHint,
|
||||
ImageQuestionVariant,
|
||||
QuestionBranchingRule,
|
||||
QuestionHint,
|
||||
QuizQuestionBase
|
||||
} from "./shared";
|
||||
|
||||
export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||
@ -16,10 +16,11 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
|
||||
innerName: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
variants: QuestionVariant[];
|
||||
variants: ImageQuestionVariant[];
|
||||
hint: QuestionHint;
|
||||
rule: QuestionBranchingRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
autofill: boolean;
|
||||
largeCheck: boolean;
|
||||
replText: string;
|
||||
|
||||
7
src/mui.d.ts
vendored
7
src/mui.d.ts
vendored
@ -41,3 +41,10 @@ declare module "@mui/material/Typography" {
|
||||
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,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Popover,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
@ -16,8 +17,11 @@ import { questionStore, updateQuestionsList } from "@root/questions";
|
||||
|
||||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||||
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 { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||
@ -49,7 +53,6 @@ export const AnswerItem = ({
|
||||
const debounced = useDebouncedCallback((value) => {
|
||||
const answerNew = variants.slice();
|
||||
answerNew[index].answer = value;
|
||||
|
||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||
content: {
|
||||
...question.content,
|
||||
@ -58,9 +61,23 @@ export const AnswerItem = ({
|
||||
});
|
||||
}, 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 answerNew = variants.slice();
|
||||
answerNew.push({ answer: "", extendedText: "" });
|
||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||
|
||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||
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 (
|
||||
<Box>
|
||||
<Draggable draggableId={String(index)} index={index}>
|
||||
{(provided) => (
|
||||
<Box ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<FormControl
|
||||
key={index}
|
||||
fullWidth
|
||||
@ -104,14 +133,49 @@ export const AnswerItem = ({
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<>
|
||||
<InputAdornment {...provided.droppableProps} position="start">
|
||||
<PointsIcon style={{ color: "#9A9AAF", fontSize: "30px" }} />
|
||||
<InputAdornment
|
||||
{...provided.dragHandleProps}
|
||||
position="start"
|
||||
>
|
||||
<PointsIcon
|
||||
style={{ color: "#9A9AAF", fontSize: "30px" }}
|
||||
/>
|
||||
</InputAdornment>
|
||||
{additionalContent}
|
||||
</>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
sx={{ padding: "0" }}
|
||||
aria-describedby="my-popover-id"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MessageIcon
|
||||
style={{
|
||||
color: "#9A9AAF",
|
||||
fontSize: "30px",
|
||||
marginRight: "6.5px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
<Popover
|
||||
id="my-popover-id"
|
||||
open={isOpen}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
>
|
||||
<TextareaAutosize
|
||||
style={{ margin: "10px" }}
|
||||
placeholder="Подсказка для этого ответа"
|
||||
value={variant.hints}
|
||||
onChange={changeAnswerHint}
|
||||
onKeyDown={(
|
||||
event: KeyboardEvent<HTMLTextAreaElement>
|
||||
) => event.stopPropagation()}
|
||||
/>
|
||||
</Popover>
|
||||
<IconButton sx={{ padding: "0" }} onClick={deleteAnswer}>
|
||||
<DeleteIcon
|
||||
style={{
|
||||
@ -125,11 +189,7 @@ export const AnswerItem = ({
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
padding: additionalContent
|
||||
? isTablet
|
||||
? "13px"
|
||||
: "5px 13px"
|
||||
: "13px",
|
||||
padding: additionalContent ? "5px 13px" : "13px",
|
||||
borderRadius: "10px",
|
||||
background: "#ffffff",
|
||||
"& input.MuiInputBase-input": {
|
||||
@ -150,5 +210,8 @@ export const AnswerItem = ({
|
||||
{additionalMobile}
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,21 +4,21 @@ import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
|
||||
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 { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||
import type { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
|
||||
|
||||
type AnswerDraggableListProps = {
|
||||
variants: QuestionVariant[];
|
||||
totalIndex: number;
|
||||
additionalContent?: (variant: QuestionVariant, index: number) => ReactNode;
|
||||
additionalMobile?: (variant: QuestionVariant, index: number) => ReactNode;
|
||||
additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
|
||||
additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode;
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const AnswerDraggableList = ({
|
||||
variants,
|
||||
totalIndex,
|
||||
@ -29,9 +29,7 @@ export const AnswerDraggableList = ({
|
||||
|
||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||
if (destination) {
|
||||
const newItems = reorder(variants, source.index, destination.index);
|
||||
|
||||
updateVariants(quizId, totalIndex, newItems);
|
||||
reorderVariants(quizId, totalIndex, source.index, destination.index);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -171,6 +171,7 @@ export default function QuestionsPageCard({
|
||||
<>
|
||||
<Paper
|
||||
id={String(totalIndex)}
|
||||
data-cy="quiz-question-card"
|
||||
sx={{
|
||||
maxWidth: "796px",
|
||||
width: "100%",
|
||||
@ -248,6 +249,7 @@ export default function QuestionsPageCard({
|
||||
py: 0,
|
||||
paddingLeft: question.type.length === 0 ? 0 : "18px",
|
||||
},
|
||||
"data-cy": "quiz-question-title",
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
@ -263,6 +265,7 @@ export default function QuestionsPageCard({
|
||||
<IconButton
|
||||
sx={{ padding: "0", margin: "5px" }}
|
||||
disableRipple
|
||||
data-cy="expand-question"
|
||||
onClick={() =>
|
||||
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, {
|
||||
expanded: !question.expanded,
|
||||
|
||||
@ -29,7 +29,7 @@ export default function DropDown({ totalIndex }: Props) {
|
||||
|
||||
const addNewAnswer = () => {
|
||||
const answerNew = question.content.variants.slice();
|
||||
answerNew.push({ answer: "", extendedText: "" });
|
||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||
|
||||
updateQuestionsList<QuizQuestionSelect>(quizId, totalIndex, {
|
||||
content: { ...question.content, variants: answerNew },
|
||||
|
||||
@ -213,7 +213,7 @@ export default function Emoji({ totalIndex }: Props) {
|
||||
sx={{ color: theme.palette.brightPurple.main }}
|
||||
onClick={() => {
|
||||
const answerNew = question.content.variants.slice();
|
||||
answerNew.push({ answer: "", extendedText: "" });
|
||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||
|
||||
updateQuestionsList<QuizQuestionEmoji>(quizId, totalIndex, {
|
||||
content: { ...question.content, variants: answerNew },
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Link,
|
||||
@ -12,35 +11,32 @@ import {
|
||||
TextareaAutosize,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||
|
||||
import AddImage from "@icons/questionsPage/addImage";
|
||||
import Image from "@icons/questionsPage/image";
|
||||
import { ImageAddIcons } from "@icons/ImageAddIcons";
|
||||
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 ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
|
||||
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 { produce } from "immer";
|
||||
|
||||
interface Props {
|
||||
totalIndex: number;
|
||||
}
|
||||
|
||||
export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||
const [switchState, setSwitchState] = useState("setting");
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const quizId = Number(useParams().quizId);
|
||||
@ -54,22 +50,22 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
setSwitchState(data);
|
||||
};
|
||||
|
||||
const uploadImage = (files: FileList | null) => {
|
||||
if (files?.length) {
|
||||
const handleImageUpload = (files: FileList | null) => {
|
||||
if (!files?.length) return;
|
||||
|
||||
const [file] = Array.from(files);
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
const clonedContent = { ...question.content };
|
||||
clonedContent.variants[currentIndex].extendedText =
|
||||
URL.createObjectURL(file);
|
||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
||||
content: clonedContent,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
setOpened(true);
|
||||
}
|
||||
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
setIsUploadImageModalOpen(false);
|
||||
openCropModal(url, url);
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ pl: "20px", pr: "20px" }}>
|
||||
@ -79,107 +75,217 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
additionalContent={(variant, index) => (
|
||||
<>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
<AddOrEditImageButton
|
||||
imageSrc={variant.extendedText}
|
||||
onImageClick={() => {
|
||||
if (!("originalImageUrl" in variant)) return;
|
||||
|
||||
setCurrentIndex(index);
|
||||
setOpen(true);
|
||||
if (variant.extendedText) return openCropModal(
|
||||
variant.extendedText,
|
||||
variant.originalImageUrl
|
||||
);
|
||||
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{variant.extendedText ? (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
width: "60px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
background: "#EEE4FC",
|
||||
borderRadius: "3px",
|
||||
margin: "0 10px",
|
||||
height: "40px",
|
||||
onPlusClick={() => {
|
||||
setCurrentIndex(index);
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", width: "40px" }}>
|
||||
<img
|
||||
src={variant.extendedText}
|
||||
alt=""
|
||||
style={{ width: "100%" }}
|
||||
sx={{ mx: "10px" }}
|
||||
/>
|
||||
</Box>
|
||||
<PlusImage />
|
||||
</Box>
|
||||
) : (
|
||||
<Button component="label" sx={{ padding: "0px" }}>
|
||||
<AddImage
|
||||
sx={{
|
||||
height: "40px",
|
||||
width: "60px",
|
||||
margin: "0 10px",
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
additionalMobile={(variant, index) => (
|
||||
<>
|
||||
{isMobile && (
|
||||
<Box
|
||||
onClick={() => {
|
||||
<AddOrEditImageButton
|
||||
imageSrc={variant.extendedText}
|
||||
onImageClick={() => {
|
||||
if (!("originalImageUrl" in variant)) return;
|
||||
|
||||
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={{
|
||||
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",
|
||||
alignItems: "center",
|
||||
m: "8px",
|
||||
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
|
||||
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",
|
||||
width: "20px",
|
||||
background: "#EEE4FC",
|
||||
height: "30px",
|
||||
borderRadius: "3px",
|
||||
height: "40px",
|
||||
color: "white",
|
||||
backgroundColor: "#7E2AEA",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={variant.extendedText}
|
||||
alt=""
|
||||
style={{ width: "100%" }}
|
||||
<Box
|
||||
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Button component="label" sx={{ padding: "0px" }}>
|
||||
<Image
|
||||
sx={{
|
||||
height: "40px",
|
||||
width: "60px",
|
||||
margin: "0 10px",
|
||||
<ImageAddIcons
|
||||
style={{
|
||||
position: "absolute",
|
||||
color: "#7E2AEA",
|
||||
fontSize: "20px",
|
||||
left: "45%",
|
||||
right: "55%",
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -195,28 +301,9 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
+
|
||||
</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={{
|
||||
display: "flex",
|
||||
@ -238,7 +325,9 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
const clonedContent = { ...question.content };
|
||||
clonedContent.variants.push({
|
||||
answer: "",
|
||||
hints: "",
|
||||
extendedText: "",
|
||||
originalImageUrl: "",
|
||||
});
|
||||
updateQuestionsList<QuizQuestionVarImg>(quizId, totalIndex, {
|
||||
content: clonedContent,
|
||||
@ -277,5 +366,6 @@ export default function OptionsAndPicture({ totalIndex }: Props) {
|
||||
/>
|
||||
<SwitchOptionsAndPict switchState={switchState} totalIndex={totalIndex} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Link,
|
||||
@ -8,33 +6,31 @@ import {
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import ButtonsOptions from "../ButtonsOptions";
|
||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import { questionStore, setVariantImageUrl, updateQuestionsList, setVariantOriginalImageUrl } from "@root/questions";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import { AnswerDraggableList } from "../AnswerDraggableList";
|
||||
import ButtonsOptions from "../ButtonsOptions";
|
||||
import { UploadImageModal } from "../UploadImage/UploadImageModal";
|
||||
import { questionStore, updateQuestionsList } from "@root/questions";
|
||||
|
||||
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 PlusImage from "@icons/questionsPage/plus";
|
||||
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
|
||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||
import { produce } from "immer";
|
||||
|
||||
interface Props {
|
||||
totalIndex: number;
|
||||
}
|
||||
|
||||
export default function OptionsPicture({ totalIndex }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(790));
|
||||
const quizId = Number(useParams().quizId);
|
||||
const [switchState, setSwitchState] = useState("setting");
|
||||
const { listQuestions } = questionStore();
|
||||
@ -44,31 +40,31 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
||||
setSwitchState(data);
|
||||
};
|
||||
|
||||
const uploadImage = (files: FileList | null) => {
|
||||
if (files?.length) {
|
||||
const handleImageUpload = (files: FileList | null) => {
|
||||
if (!files?.length) return;
|
||||
|
||||
const [file] = Array.from(files);
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
const clonedContent = { ...question.content };
|
||||
clonedContent.variants[currentIndex].extendedText =
|
||||
URL.createObjectURL(file);
|
||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
||||
content: clonedContent,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
setOpened(true);
|
||||
}
|
||||
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
setVariantOriginalImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
setIsUploadImageModalOpen(false);
|
||||
openCropModal(url, url);
|
||||
};
|
||||
|
||||
const addNewAnswer = () => {
|
||||
const answerNew = question.content.variants.slice();
|
||||
answerNew.push({ answer: "", extendedText: "" });
|
||||
answerNew.push({ answer: "", hints: "", extendedText: "", originalImageUrl: "" });
|
||||
|
||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
||||
content: { ...question.content, variants: answerNew },
|
||||
});
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
setVariantImageUrl(quizId, totalIndex, currentIndex, url);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ padding: "20px" }}>
|
||||
@ -78,140 +74,64 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
||||
additionalContent={(variant, index) => (
|
||||
<>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
<AddOrEditImageButton
|
||||
imageSrc={variant.extendedText}
|
||||
onImageClick={() => {
|
||||
if (!("originalImageUrl" in variant)) return;
|
||||
|
||||
setCurrentIndex(index);
|
||||
setOpen(true);
|
||||
if (variant.extendedText) {
|
||||
return openCropModal(
|
||||
variant.extendedText,
|
||||
variant.originalImageUrl
|
||||
);
|
||||
}
|
||||
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{variant.extendedText ? (
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
width: "60px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
background: "#EEE4FC",
|
||||
borderRadius: "3px",
|
||||
margin: "0 10px",
|
||||
height: "40px",
|
||||
onPlusClick={() => {
|
||||
setCurrentIndex(index);
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", width: "40px" }}>
|
||||
<img
|
||||
src={variant.extendedText}
|
||||
alt=""
|
||||
style={{ width: "100%" }}
|
||||
sx={{ mx: "10px" }}
|
||||
/>
|
||||
</Box>
|
||||
<PlusImage />
|
||||
</Box>
|
||||
) : (
|
||||
<Button component="label" sx={{ padding: "0px" }}>
|
||||
<AddImage
|
||||
sx={{ height: "40px", width: "60px", margin: "0 10px" }}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
additionalMobile={(variant, index) => (
|
||||
<>
|
||||
{isMobile && (
|
||||
<Box
|
||||
onClick={() => {
|
||||
<AddOrEditImageButton
|
||||
imageSrc={variant.extendedText}
|
||||
onImageClick={() => {
|
||||
if (!("originalImageUrl" in variant)) return;
|
||||
|
||||
setCurrentIndex(index);
|
||||
setOpen(true);
|
||||
if (variant.extendedText) {
|
||||
return openCropModal(
|
||||
variant.extendedText,
|
||||
variant.originalImageUrl
|
||||
);
|
||||
}
|
||||
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
m: "8px",
|
||||
position: "relative",
|
||||
borderRadius: "3px",
|
||||
onPlusClick={() => {
|
||||
setCurrentIndex(index);
|
||||
setIsUploadImageModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<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%" }}
|
||||
sx={{ m: "8px", width: "auto" }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Button component="label" sx={{ padding: "0px" }}>
|
||||
<Image
|
||||
sx={{
|
||||
height: "40px",
|
||||
width: "60px",
|
||||
margin: "0 10px",
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "20px",
|
||||
background: "#EEE4FC",
|
||||
height: "40px",
|
||||
color: "white",
|
||||
backgroundColor: "#7E2AEA",
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<UploadImageModal
|
||||
open={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<QuizQuestionImages>(quizId, totalIndex, {
|
||||
content,
|
||||
});
|
||||
}}
|
||||
open={isUploadImageModalOpen}
|
||||
onClose={() => setIsUploadImageModalOpen(false)}
|
||||
imgHC={handleImageUpload}
|
||||
/>
|
||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<Link
|
||||
component="button"
|
||||
@ -244,15 +164,9 @@ export default function OptionsPicture({ totalIndex }: Props) {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<ButtonsOptions
|
||||
switchState={switchState}
|
||||
SSHC={SSHC}
|
||||
totalIndex={totalIndex}
|
||||
/>
|
||||
<SwitchAnswerOptionsPict
|
||||
switchState={switchState}
|
||||
totalIndex={totalIndex}
|
||||
/>
|
||||
<ButtonsOptions switchState={switchState} 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 { useParams } from "react-router-dom";
|
||||
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
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 { UploadVideoModal } from "../UploadVideoModal";
|
||||
import { AddPlusImage } from "@icons/questionsPage/addPlusImage";
|
||||
import { AddPlusVideo } from "@icons/questionsPage/addPlusVideo";
|
||||
import { ImageAddIcons } from "@icons/ImageAddIcons";
|
||||
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
|
||||
import SwitchPageOptions from "./switchPageOptions";
|
||||
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
@ -43,6 +41,21 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
||||
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 (
|
||||
<>
|
||||
<Box
|
||||
@ -74,7 +87,6 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() => setOpenImageModal(true)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
@ -82,80 +94,22 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "120px",
|
||||
position: "relative",
|
||||
<AddOrEditImageButton
|
||||
imageSrc={question.content.picture}
|
||||
onImageClick={() => {
|
||||
if (question.content.picture) {
|
||||
return openCropModal(
|
||||
question.content.picture,
|
||||
question.content.originalPicture
|
||||
);
|
||||
}
|
||||
|
||||
setOpenImageModal(true);
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
background: "#EEE4FC",
|
||||
height: "40px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderTopLeftRadius: "4px",
|
||||
borderBottomLeftRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<ImageAddIcons
|
||||
style={{
|
||||
color: "#7E2AEA",
|
||||
fontSize: "20px",
|
||||
onPlusClick={() => {
|
||||
setOpenImageModal(true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "20px",
|
||||
background: "#EEE4FC",
|
||||
height: "40px",
|
||||
color: "white",
|
||||
backgroundColor: "#7E2AEA",
|
||||
borderTopRightRadius: "4px",
|
||||
borderBottomRightRadius: "4px",
|
||||
}}
|
||||
>
|
||||
+
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: "60px",
|
||||
height: "40px",
|
||||
background: "#EEE4FC",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%" }}>
|
||||
<ImageAddIcons fontSize="22px" color="#7E2AEA" />
|
||||
</Box>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "#7E2AEA",
|
||||
height: "100%",
|
||||
width: "25px",
|
||||
color: "white",
|
||||
fontSize: "15px",
|
||||
}}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
@ -172,18 +126,9 @@ export default function PageOptions({ disableInput, totalIndex }: Props) {
|
||||
<UploadImageModal
|
||||
open={openImageModal}
|
||||
onClose={() => setOpenImageModal(false)}
|
||||
imgHC={(fileList) => {
|
||||
if (fileList?.length) {
|
||||
updateQuestionsList<QuizQuestionPage>(quizId, totalIndex, {
|
||||
content: {
|
||||
...question.content,
|
||||
picture: URL.createObjectURL(fileList[0]),
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
// onClick={() => setOpenVideoModal(true)}
|
||||
imgHC={handleImageUpload}
|
||||
/>
|
||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||
<Typography> или</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@ -110,6 +110,7 @@ export default function TypeQuestions({ totalIndex }: Props) {
|
||||
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
||||
<QuestionsMiniButton
|
||||
key={title}
|
||||
dataCy={`select-questiontype-${value}`}
|
||||
onClick={() => {
|
||||
const question = { ...listQuestions[quizId][totalIndex] };
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { Typography, Box, useTheme, ButtonBase } from "@mui/material";
|
||||
import UploadBox from "@ui_kit/UploadBox";
|
||||
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||
import { questionStore, setQuestionBackgroundImage, setQuestionOriginalBackgroundImage } from "@root/questions";
|
||||
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 { questionStore, updateQuestionsList } from "@root/questions";
|
||||
import { useParams } from "react-router-dom";
|
||||
import UploadIcon from "../../../assets/icons/UploadIcon";
|
||||
import { UploadImageModal } from "./UploadImageModal";
|
||||
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
import { QuizQuestionBase } from "model/questionTypes/shared";
|
||||
import type { DragEvent } from "react";
|
||||
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
|
||||
|
||||
type UploadImageProps = {
|
||||
totalIndex: number;
|
||||
@ -18,36 +18,34 @@ type UploadImageProps = {
|
||||
export default function UploadImage({ totalIndex }: UploadImageProps) {
|
||||
const quizId = Number(useParams().quizId);
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [isUploadImageModalOpen, setIsUploadImageModalOpen] = React.useState(false);
|
||||
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);
|
||||
|
||||
updateQuestionsList<QuizQuestionImages>(quizId, totalIndex, {
|
||||
content: {
|
||||
...question.content,
|
||||
back: URL.createObjectURL(file),
|
||||
},
|
||||
});
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
handleClose();
|
||||
setOpened(true);
|
||||
}
|
||||
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||
setQuestionOriginalBackgroundImage(quizId, totalIndex, url);
|
||||
setIsUploadImageModalOpen(false);
|
||||
openCropModal(url, url);
|
||||
};
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
imgHC(event.dataTransfer.files);
|
||||
handleImageUpload(event.dataTransfer.files);
|
||||
};
|
||||
|
||||
function handleCropModalSaveClick(url: string) {
|
||||
setQuestionBackgroundImage(quizId, totalIndex, url);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ padding: "20px" }}>
|
||||
<Typography
|
||||
@ -61,22 +59,39 @@ export default function UploadImage({ totalIndex }: UploadImageProps) {
|
||||
Загрузить изображение
|
||||
</Typography>
|
||||
<ButtonBase
|
||||
onClick={handleOpen}
|
||||
sx={{ width: "100%", maxWidth: "260px" }}
|
||||
onClick={() => setIsUploadImageModalOpen(true)}
|
||||
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
|
||||
handleDrop={handleDrop}
|
||||
sx={{ maxWidth: "260px" }}
|
||||
icon={<UploadIcon />}
|
||||
text="5 MB максимум"
|
||||
/>
|
||||
}
|
||||
</ButtonBase>
|
||||
<UploadImageModal open={open} onClose={handleClose} imgHC={imgHC} />
|
||||
<CropModal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
picture={question.content.back}
|
||||
<UploadImageModal
|
||||
open={isUploadImageModalOpen}
|
||||
onClose={() => setIsUploadImageModalOpen(false)}
|
||||
imgHC={handleImageUpload}
|
||||
/>
|
||||
<CropModal onSaveImageClick={handleCropModalSaveClick} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ export default function AnswerOptions({ totalIndex }: Props) {
|
||||
|
||||
const addNewAnswer = () => {
|
||||
const answerNew = question.content.variants.slice();
|
||||
answerNew.push({ answer: "", extendedText: "" });
|
||||
answerNew.push({ answer: "", extendedText: "", hints: "" });
|
||||
|
||||
updateQuestionsList<QuizQuestionVariant>(quizId, totalIndex, {
|
||||
content: { ...question.content, variants: answerNew },
|
||||
|
||||
@ -2,27 +2,17 @@ import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
|
||||
|
||||
import AddImage from "@icons/questionsPage/addImage";
|
||||
import AddVideofile from "@icons/questionsPage/addVideofile";
|
||||
import { useState } from "react";
|
||||
import { CropModal } from "@ui_kit/Modal/CropModal";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
|
||||
export default function ImageAndVideoButtons() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(400));
|
||||
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
mt: "20px",
|
||||
mb: "20px",
|
||||
}}
|
||||
>
|
||||
<AddImage onClick={() => setOpened(true)} />
|
||||
<CropModal opened={opened} onClose={() => setOpened(false)} />
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
||||
<AddImage onClick={() => openCropModal("", "")} />
|
||||
<CropModal />
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
|
||||
@ -34,6 +34,7 @@ export default function FirstQuiz() {
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
data-cy="create-quiz"
|
||||
onClick={() => {
|
||||
navigate(`/setting/${createBlank()}`);
|
||||
}}
|
||||
|
||||
@ -848,10 +848,9 @@ export default function StartPageSettings() {
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ display: "block", marginLeft: "auto" }}
|
||||
data-cy="setup-questions"
|
||||
onClick={() => {
|
||||
let SPageClone = listQuizes[params].config;
|
||||
SPageClone.startpage.background.desktop =
|
||||
@ -868,7 +867,6 @@ export default function StartPageSettings() {
|
||||
Настроить вопросы
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ export default function StepOne() {
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
data-cy="create-quiz-card"
|
||||
onClick={() => {
|
||||
let SPageClone = listQuizes[params].config;
|
||||
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 { 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 { devtools, persist } from "zustand/middleware";
|
||||
|
||||
import type {
|
||||
AnyQuizQuestion,
|
||||
QuizQuestionType,
|
||||
QuestionVariant,
|
||||
QuizQuestionType
|
||||
} from "../model/questionTypes/shared";
|
||||
|
||||
import { produce, setAutoFreeze } from "immer";
|
||||
import { QUIZ_QUESTION_BASE } from "../constants/base";
|
||||
import { QUIZ_QUESTION_DATE } from "../constants/date";
|
||||
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_VARIANT } from "../constants/variant";
|
||||
import { QUIZ_QUESTION_VARIMG } from "../constants/varimg";
|
||||
import { setAutoFreeze } from "immer";
|
||||
|
||||
setAutoFreeze(false);
|
||||
|
||||
@ -36,11 +30,19 @@ interface QuestionStore {
|
||||
let isFirstPartialize = true;
|
||||
|
||||
export const questionStore = create<QuestionStore>()(
|
||||
persist<QuestionStore>(
|
||||
persist(
|
||||
devtools(
|
||||
() => ({
|
||||
listQuestions: {},
|
||||
openedModalSettings: "",
|
||||
}),
|
||||
{
|
||||
name: "Question",
|
||||
enabled: process.env.NODE_ENV === "development",
|
||||
trace: process.env.NODE_ENV === "development",
|
||||
actionsBlacklist: "ignored",
|
||||
}
|
||||
),
|
||||
{
|
||||
name: "question",
|
||||
partialize: (state: QuestionStore) => {
|
||||
@ -66,26 +68,31 @@ export const questionStore = create<QuestionStore>()(
|
||||
const state = persistedState as QuestionStore;
|
||||
|
||||
// replace blob urls with ""
|
||||
Object.values(state.listQuestions).forEach((questions) => {
|
||||
questions.forEach((question) => {
|
||||
if (
|
||||
question.type === "page" &&
|
||||
question.content.picture.startsWith("blob:")
|
||||
) {
|
||||
Object.values(state.listQuestions).forEach(questions => {
|
||||
questions.forEach(question => {
|
||||
if (question.type === "page") {
|
||||
if (question.content.picture.startsWith("blob:")) {
|
||||
question.content.picture = "";
|
||||
}
|
||||
}
|
||||
if (question.type === "images") {
|
||||
question.content.variants.forEach((variant) => {
|
||||
question.content.variants.forEach(variant => {
|
||||
if (variant.extendedText.startsWith("blob:")) {
|
||||
variant.extendedText = "";
|
||||
}
|
||||
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||
variant.originalImageUrl = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
if (question.type === "varimg") {
|
||||
question.content.variants.forEach((variant) => {
|
||||
question.content.variants.forEach(variant => {
|
||||
if (variant.extendedText.startsWith("blob:")) {
|
||||
variant.extendedText = "";
|
||||
}
|
||||
if (variant.originalImageUrl.startsWith("blob:")) {
|
||||
variant.originalImageUrl = "";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -112,6 +119,131 @@ export const updateQuestionsList = <T = AnyQuizQuestion>(
|
||||
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 = (
|
||||
quizId: number,
|
||||
updatedQuestions: AnyQuizQuestion[]
|
||||
@ -122,35 +254,27 @@ export const updateQuestionsListDragAndDrop = (
|
||||
});
|
||||
};
|
||||
|
||||
export const updateVariants = (
|
||||
|
||||
export const reorderVariants = (
|
||||
quizId: number,
|
||||
index: number,
|
||||
variants: QuestionVariant[]
|
||||
) => {
|
||||
const listQuestions = { ...questionStore.getState()["listQuestions"] };
|
||||
questionIndex: number,
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
) => setProducedState(state => {
|
||||
if (sourceIndex === destinationIndex) return;
|
||||
|
||||
switch (listQuestions[quizId][index].type) {
|
||||
case "emoji":
|
||||
const emojiState = listQuestions[quizId][index] as QuizQuestionEmoji;
|
||||
emojiState.content.variants = variants;
|
||||
return questionStore.setState({ listQuestions });
|
||||
const question = state.listQuestions[quizId][questionIndex];
|
||||
if (!("variants" in question.content)) return;
|
||||
|
||||
case "images":
|
||||
const imagesState = listQuestions[quizId][index] as QuizQuestionImages;
|
||||
imagesState.content.variants = variants;
|
||||
return questionStore.setState({ listQuestions });
|
||||
|
||||
case "variant":
|
||||
const variantState = listQuestions[quizId][index] as QuizQuestionVariant;
|
||||
variantState.content.variants = variants;
|
||||
return questionStore.setState({ listQuestions });
|
||||
|
||||
case "varimg":
|
||||
const varImgState = listQuestions[quizId][index] as QuizQuestionVarImg;
|
||||
varImgState.content.variants = variants;
|
||||
return questionStore.setState({ listQuestions });
|
||||
}
|
||||
};
|
||||
const [removed] = question.content.variants.splice(sourceIndex, 1);
|
||||
question.content.variants.splice(destinationIndex, 0, removed);
|
||||
}, {
|
||||
type: sourceIndex === destinationIndex ? "reorderVariants" : "ignored",
|
||||
quizId,
|
||||
questionIndex,
|
||||
sourceIndex,
|
||||
destinationIndex,
|
||||
});
|
||||
|
||||
export const createQuestion = (
|
||||
quizId: number,
|
||||
@ -183,7 +307,7 @@ export const createQuestion = (
|
||||
newData[quizId].splice(
|
||||
placeIndex < 0 ? newData[quizId].length : placeIndex,
|
||||
0,
|
||||
{ ...defaultObject, id }
|
||||
{ ...JSON.parse(JSON.stringify(defaultObject)), id }
|
||||
);
|
||||
|
||||
questionStore.setState({ listQuestions: newData });
|
||||
@ -230,6 +354,7 @@ export const findQuestionById = (quizId: number) => {
|
||||
found = { quiz, index };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return found;
|
||||
};
|
||||
@ -239,3 +364,10 @@ function getRandom() {
|
||||
const max = Math.floor(10000000);
|
||||
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 {persist} from "zustand/middleware";
|
||||
import { removeQuestionsByQuizId } from "./questions";
|
||||
|
||||
interface QuizStore {
|
||||
listQuizes: { [key: number]: Quizes };
|
||||
@ -87,6 +88,8 @@ export const quizStore = create<QuizStore>()(
|
||||
return accumulator;
|
||||
}, {});
|
||||
set({listQuizes: newState});
|
||||
|
||||
removeQuestionsByQuizId(id);
|
||||
},
|
||||
createBlank: () => {
|
||||
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 ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
||||
import { CropIcon } from "@icons/CropIcon";
|
||||
import { ResetIcon } from "@icons/ResetIcon";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Modal,
|
||||
Slider,
|
||||
SxProps,
|
||||
Theme,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import { canvasPreview } from "./utils/canvasPreview";
|
||||
import { useDebounceEffect } from "./utils/useDebounceEffect";
|
||||
|
||||
import { ResetIcon } from "@icons/ResetIcon";
|
||||
|
||||
import { closeCropModal, resetToOriginalImage, setCropModalImageUrl, useCropModalStore } from "@root/cropModal";
|
||||
import { FC, useRef, useState } from "react";
|
||||
import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
|
||||
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 [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",
|
||||
const styleSlider: SxProps<Theme> = {
|
||||
color: "#7E2AEA",
|
||||
height: "12px",
|
||||
"& .MuiSlider-track": {
|
||||
@ -84,80 +44,67 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
},
|
||||
};
|
||||
|
||||
const rotateImage = () => {
|
||||
const newRotation = (rotate + 90) % 360;
|
||||
setRotate(newRotation);
|
||||
};
|
||||
|
||||
const onSelectFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setCrop(undefined);
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () =>
|
||||
setImgSrc(reader.result?.toString() || "")
|
||||
);
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
interface Props {
|
||||
onSaveImageClick?: (imageUrl: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const onDownloadCropClick = () => {
|
||||
if (!previewCanvasRef.current) {
|
||||
throw new Error("Crop canvas does not exist");
|
||||
}
|
||||
export const CropModal: FC<Props> = ({ onSaveImageClick }) => {
|
||||
const theme = useTheme();
|
||||
const isCropModalOpen = useCropModalStore(state => state.isCropModalOpen);
|
||||
const imageUrl = useCropModalStore(state => state.imageUrl);
|
||||
const [crop, setCrop] = useState<Crop>();
|
||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>();
|
||||
const [darken, setDarken] = useState(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 handleCropClick = async () => {
|
||||
if (!completedCrop) throw new Error("No completed crop");
|
||||
if (!cropImageElementRef.current) throw new Error("No image");
|
||||
|
||||
const canvasCopy = document.createElement("canvas");
|
||||
const ctx = canvasCopy.getContext("2d");
|
||||
canvasCopy.width = previewCanvasRef.current.width;
|
||||
canvasCopy.height = previewCanvasRef.current.height;
|
||||
ctx!.filter = `brightness(${100 - darken}%)`;
|
||||
ctx!.drawImage(previewCanvasRef.current, 0, 0);
|
||||
if (!ctx) throw new Error("No 2d context");
|
||||
|
||||
canvasCopy.width = completedCrop.width;
|
||||
canvasCopy.height = completedCrop.height;
|
||||
ctx.filter = `brightness(${100 - darken}%)`;
|
||||
|
||||
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
|
||||
|
||||
canvasCopy.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
throw new Error("Failed to create blob");
|
||||
}
|
||||
if (blobUrlRef.current) {
|
||||
URL.revokeObjectURL(blobUrlRef.current);
|
||||
}
|
||||
blobUrlRef.current = URL.createObjectURL(blob);
|
||||
hiddenAnchorRef.current!.href = blobUrlRef.current;
|
||||
hiddenAnchorRef.current!.click();
|
||||
const newImageUrl = URL.createObjectURL(blob);
|
||||
|
||||
setImgSrc(blobUrlRef.current);
|
||||
onCropPress?.(blobUrlRef.current);
|
||||
setCropModalImageUrl(newImageUrl);
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
useDebounceEffect(
|
||||
async () => {
|
||||
if (
|
||||
completedCrop?.width &&
|
||||
completedCrop?.height &&
|
||||
imgRef.current &&
|
||||
previewCanvasRef.current
|
||||
) {
|
||||
canvasPreview(
|
||||
imgRef.current,
|
||||
previewCanvasRef.current,
|
||||
completedCrop,
|
||||
rotate
|
||||
);
|
||||
function handleSaveClick() {
|
||||
if (imageUrl) onSaveImageClick?.(imageUrl);
|
||||
setCrop(undefined);
|
||||
setCompletedCrop(undefined);
|
||||
closeCropModal();
|
||||
}
|
||||
},
|
||||
100,
|
||||
[completedCrop, rotate]
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState<number>(0);
|
||||
function handleLoadOriginalImage() {
|
||||
const isSuccess = resetToOriginalImage();
|
||||
if (!isSuccess) enqueueSnackbar("Не удалось восстановить оригинал. Приносим глубочайшие извинения");
|
||||
}
|
||||
|
||||
const getImageSize = () => {
|
||||
if (imgRef.current) {
|
||||
const imageWidth = imgRef.current.naturalWidth;
|
||||
const imageHeight = imgRef.current.naturalHeight;
|
||||
if (cropImageElementRef.current) {
|
||||
const imageWidth = cropImageElementRef.current.naturalWidth;
|
||||
const imageHeight = cropImageElementRef.current.naturalHeight;
|
||||
|
||||
const aspect = imageWidth / imageHeight;
|
||||
|
||||
|
||||
if (aspect <= 1.333) {
|
||||
setWidth(240);
|
||||
}
|
||||
@ -171,27 +118,35 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={opened}
|
||||
onClose={onClose}
|
||||
open={isCropModalOpen}
|
||||
onClose={closeCropModal}
|
||||
aria-labelledby="modal-modal-title"
|
||||
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
|
||||
sx={{
|
||||
height: "320px",
|
||||
padding: "10px",
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat",
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{imgSrc && (
|
||||
{imageUrl && (
|
||||
<ReactCrop
|
||||
crop={crop}
|
||||
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||
@ -203,9 +158,9 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
>
|
||||
<img
|
||||
onLoad={getImageSize}
|
||||
ref={imgRef}
|
||||
ref={cropImageElementRef}
|
||||
alt="Crop me"
|
||||
src={imgSrc}
|
||||
src={imageUrl}
|
||||
style={{
|
||||
filter: `brightness(${100 - darken}%)`,
|
||||
transform: ` rotate(${rotate}deg)`,
|
||||
@ -243,16 +198,17 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<ResetIcon
|
||||
onClick={rotateImage}
|
||||
style={{ marginBottom: "10px", cursor: "pointer" }}
|
||||
/>
|
||||
<IconButton onClick={() => setRotate(r => (r + 90) % 360)}>
|
||||
<ResetIcon />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
|
||||
Размер
|
||||
</Typography>
|
||||
<Slider
|
||||
sx={styleSlider}
|
||||
sx={[styleSlider, {
|
||||
width: isMobile ? "350px" : "250px",
|
||||
}]}
|
||||
value={width}
|
||||
min={50}
|
||||
max={580}
|
||||
@ -267,7 +223,9 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
Затемнение
|
||||
</Typography>
|
||||
<Slider
|
||||
sx={styleSlider}
|
||||
sx={[styleSlider, {
|
||||
width: isMobile ? "350px" : "250px",
|
||||
}]}
|
||||
value={darken}
|
||||
min={0}
|
||||
max={100}
|
||||
@ -281,18 +239,22 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
marginTop: "40px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
style={{ display: "none", zIndex: "-999" }}
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={onSelectFile}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onClick={handleSaveClick}
|
||||
disableRipple
|
||||
sx={{
|
||||
height: "48px",
|
||||
color: "#7E2AEA",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #7E2AEA",
|
||||
marginRight: "10px",
|
||||
px: "20px",
|
||||
}}
|
||||
>Сохранить</Button>
|
||||
<Button
|
||||
onClick={handleLoadOriginalImage}
|
||||
disableRipple
|
||||
sx={{
|
||||
width: "215px",
|
||||
@ -301,14 +263,16 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #7E2AEA",
|
||||
marginRight: "10px",
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
Загрузить оригинал
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDownloadCropClick}
|
||||
onClick={handleCropClick}
|
||||
disableRipple
|
||||
variant="contained"
|
||||
disabled={!completedCrop}
|
||||
sx={{
|
||||
padding: "10px 20px",
|
||||
borderRadius: "8px",
|
||||
@ -322,33 +286,5 @@ export const CropModal: FC<Iprops> = ({ opened, onClose, picture, onCropPress })
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
{completedCrop && (
|
||||
<div>
|
||||
<canvas
|
||||
ref={previewCanvasRef}
|
||||
style={{
|
||||
display: "none",
|
||||
zIndex: "-999",
|
||||
border: "1px solid black",
|
||||
objectFit: "contain",
|
||||
width: completedCrop.width,
|
||||
height: completedCrop.height,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<a
|
||||
href="#hidden"
|
||||
ref={hiddenAnchorRef}
|
||||
download
|
||||
style={{
|
||||
display: "none",
|
||||
}}
|
||||
>
|
||||
Hidden download
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { FC, useState } from "react";
|
||||
import { FC } from "react";
|
||||
import { CropModal } from "./CropModal";
|
||||
import { openCropModal } from "@root/cropModal";
|
||||
|
||||
const ImageCrop: FC = () => {
|
||||
const [opened, setOpened] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button onClick={() => setOpened(true)}>Открыть модалку</Button>
|
||||
<CropModal opened={opened} onClose={() => setOpened(false)} />
|
||||
<Button onClick={() => openCropModal("", "")}>Открыть модалку</Button>
|
||||
<CropModal />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,15 +7,17 @@ interface QuestionsMiniButtonProps {
|
||||
icon: ReactNode;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
dataCy?: string;
|
||||
}
|
||||
|
||||
export default function QuestionsMiniButton({ icon, text, onClick }: QuestionsMiniButtonProps) {
|
||||
export default function QuestionsMiniButton({ icon, text, onClick, dataCy }: QuestionsMiniButtonProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
data-cy={dataCy}
|
||||
sx={{
|
||||
padding: "26px 15px 15px 15px",
|
||||
display: "flex",
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import { Box, IconButton } from "@mui/material";
|
||||
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { Rnd } from "react-rnd";
|
||||
import { useWindowSize } from "../../utils/hooks/useWindowSize";
|
||||
import QuizPreviewLayout from "./QuizPreviewLayout";
|
||||
import ResizeIcon from "./ResizeIcon";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
|
||||
const DRAG_PARENT_MARGIN = 0;
|
||||
const NAVBAR_HEIGHT = 0;
|
||||
|
||||
@ -160,4 +160,5 @@ export default function QuizPreviewLayout() {
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@ export default function Variant({ question }: Props) {
|
||||
|
||||
return (
|
||||
<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
|
||||
aria-labelledby="quiz-question-radio-group"
|
||||
value={value}
|
||||
@ -35,11 +38,17 @@ export default function Variant({ question }: Props) {
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
value={variant.answer}
|
||||
control={<Radio />}
|
||||
control={<Radio
|
||||
inputProps={{
|
||||
"data-cy": "variant-radio",
|
||||
}}
|
||||
/>}
|
||||
label={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<Typography>{variant.answer}</Typography>
|
||||
<Tooltip title="Подсказка" placement="right">
|
||||
<Typography
|
||||
data-cy="variant-answer"
|
||||
>{variant.answer}</Typography>
|
||||
<Tooltip title={variant.hints} placement="right">
|
||||
<Box>
|
||||
<InfoIcon />
|
||||
</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,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"src",
|
||||
],
|
||||
"exclude": [
|
||||
"cypress.config.ts",
|
||||
"cypress",
|
||||
"node_modules",
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user