Merge branch 'dev' into 'main'

chore: Restrict image uploads to JPG and PNG formats

See merge request frontend/squiz!76
This commit is contained in:
Nastya 2023-12-16 12:52:32 +00:00
commit b657ab4ac4
50 changed files with 3402 additions and 2677 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,296 @@
import "cypress-file-upload";
describe("Форма Входа", () => {
beforeEach(() => {
cy.visit("http://localhost:3000");
cy.wait(1000);
cy.contains("Регистрация / Войти").click();
const login = "valid_user@exammple.com";
const password = "valid_password";
cy.get("#email").type(login);
cy.get("#password").type(password);
cy.get('button[type="submit"]').click();
});
it("Тестирование создания, публикации и удаления опросника с проверкой обязательных вопросов", () => {
cy.get('[data-cy="create-quiz"]').click();
cy.wait(1000);
cy.get('button[data-cy="create-quiz-card"]').eq(0).click();
cy.wait(1000);
cy.get('button[data-cy="select-quiz-layout-standard"]').click();
cy.get('input[type="checkbox"]').click();
cy.get('[data-cy="setup-questions"]').click();
cy.contains("button", "Варианты с картинками").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('[data-cy="quiz-variant-question-answer"]').eq(0).type("1").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(1).should("have.value", "").type("2").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(2).should("have.value", "").type("3").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(3).should("have.value", "").type("4").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.contains("div", "1").click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Варианты ответов +
cy.contains("button", "Варианты ответов").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('[data-cy="quiz-variant-question-answer"]').eq(0).type("1").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(1).should("have.value", "").type("2").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(2).should("have.value", "").type("3").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(3).should("have.value", "").type("4").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.contains("label", "1").click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
// Варианты и картинка +
cy.contains("button", "Варианты и картинка").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('[data-cy="quiz-variant-question-answer"]').eq(0).type("1").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(1).should("have.value", "").type("2").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(2).should("have.value", "").type("3").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(3).should("have.value", "").type("4").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.contains("div", "1").click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Эмоджи +
cy.contains("button", "Эмоджи").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('[data-cy="quiz-variant-question-answer"]').eq(0).type("1").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(1).should("have.value", "").type("2").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(2).should("have.value", "").type("3").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(3).should("have.value", "").type("4").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.contains("div", "1").click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Своё поле для ввода +
cy.contains("button", "Своё поле для ввода").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('input[placeholder="Пример ответа"]').eq(0).type("1").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.get('input[type="text"]').type("email@invalid.com");
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Выпадающий список +
cy.contains("button", "Выпадающий список").click();
cy.contains("label", "Необязательный вопрос").click();
cy.get('[data-cy="quiz-variant-question-answer"]').eq(0).type("1").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(1).should("have.value", "").type("2").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(2).should("have.value", "").type("3").type("{enter}");
cy.get('[data-cy="quiz-variant-question-answer"]').eq(3).should("have.value", "").type("4").type("{enter}");
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.get("#display-select").click();
cy.get("li").eq(0).click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Дата +
cy.contains("button", "Дата").click();
cy.contains("label", "Необязательный вопрос").click();
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.get('button[data-cy="open-datepicker"]').click();
cy.wait(500);
cy.get('button[role="gridcell"]').eq(16).click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Ползунок +
cy.contains("button", "Ползунок").click();
cy.contains("label", "Необязательный вопрос").click();
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.get('input[aria-invalid="false"][id=":r0:"][placeholder="0"]').type("10");
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
//Загрузка файла +
cy.contains("button", "Загрузка файла").click();
cy.contains("label", "Необязательный вопрос").click();
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.get('label.MuiButtonBase-root input[type="file"]').attachFile("./image/Bunner.png");
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
// Рейтинг
cy.contains("button", "Рейтинг").click();
cy.contains("label", "Необязательный вопрос").click();
cy.visit("http://localhost:3000/view");
cy.wait(500);
cy.contains("Далее →").should("be.disabled");
cy.contains("label", "4 Stars").click();
cy.wait(500);
cy.contains("Далее →").should("not.be.disabled");
cy.visit("http://localhost:3000/view");
cy.visit("http://localhost:3000/edit");
cy.wait(500);
cy.get('[data-cy="delete-question"]').click();
cy.wait(5000);
cy.get('[data-cy="create-question"]').click();
// Удаления Квиза
cy.visit("http://localhost:3000/list");
cy.wait(500);
cy.get('[data-cy="delete-quiz"]').each(($button) => {
cy.wrap($button).click();
cy.wait(500);
cy.contains("button", "Удалить").click();
});
});
});

@ -21,6 +21,7 @@
"@types/react-dnd": "^3.0.2", "@types/react-dnd": "^3.0.2",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"cypress-file-upload": "^5.0.8",
"cytoscape": "^3.26.0", "cytoscape": "^3.26.0",
"cytoscape-popper": "^2.0.0", "cytoscape-popper": "^2.0.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
@ -78,6 +79,6 @@
"@types/react-beautiful-dnd": "^13.1.4", "@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4", "@types/react-cytoscapejs": "^1.2.4",
"craco-alias": "^3.0.1", "craco-alias": "^3.0.1",
"cypress": "^13.4.0" "cypress": "^13.6.1"
} }
} }

@ -0,0 +1,27 @@
import {Box, SxProps, Theme, useTheme} from "@mui/material";
interface Props {
right: boolean
}
export default function ArrowLeftSP({right} : Props) {
const theme = useTheme();
return (
<Box
sx={{
height: "14px",
width: "19px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transform: right ? "rotate(180deg)" : undefined
}}
>
<svg width="13" height="11" viewBox="0 0 13 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.12 10.808H5.24L0.855999 5.88L5.24 0.952H8.12L4.648 4.936H12.184V6.824H4.648L8.12 10.808Z" fill={theme.palette.brightPurple.main}/>
</svg>
</Box>
);
}

@ -1,40 +1,10 @@
import { FC, SVGProps } from "react"; import { FC, SVGProps } from "react";
export const CropIcon: FC = () => ( export const CropIcon: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg <svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
xmlns="http://www.w3.org/2000/svg" <path d="M6 6H2.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
width="24" <path d="M6 2.25V18H21.75" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
height="24" <path d="M18 15V6H9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
viewBox="0 0 24 24" <path d="M18 21.75V18" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
fill="none"
>
<path
d="M6 6H2.25"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 2.25V18H21.75"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18 15V6H9"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18 21.75V18"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
); );

@ -96,6 +96,8 @@ export type AnyTypedQuizQuestion =
| QuizQuestionRating | QuizQuestionRating
| QuizQuestionResult; | QuizQuestionResult;
type FilterQuestionsWithVariants<T> = T extends { type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[]; }; content: { variants: QuestionVariant[]; };
} ? T : never; } ? T : never;

@ -40,7 +40,7 @@ export interface QuizConfig {
theme: string, theme: string,
reply: string, reply: string,
replname: string, replname: string,
} }
startpage: { startpage: {
description: string; description: string;
button: string; button: string;
@ -58,6 +58,16 @@ export interface QuizConfig {
cycle: boolean; cycle: boolean;
}; };
}; };
formContact: {
title: string;
desc: string;
name: FCField;
email: FCField;
phone: FCField;
text: FCField;
address: FCField;
button: string
};
info: { info: {
phonenumber: string; phonenumber: string;
clickable: boolean; clickable: boolean;
@ -68,6 +78,14 @@ export interface QuizConfig {
meta: string; meta: string;
} }
type FCField = {
text: string
innerText: string
key: string
required: boolean
used: boolean
}
export const defaultQuizConfig: QuizConfig = { export const defaultQuizConfig: QuizConfig = {
type: null, type: null,
noStartPage: false, noStartPage: false,
@ -81,7 +99,7 @@ export const defaultQuizConfig: QuizConfig = {
theme: "", theme: "",
reply: "", reply: "",
replname: "", replname: "",
}, },
startpage: { startpage: {
description: "", description: "",
button: "", button: "",
@ -106,5 +124,45 @@ export const defaultQuizConfig: QuizConfig = {
site: "", site: "",
law: "", law: "",
}, },
formContact: {
title: "",
desc: "",
name: {
text: "",
innerText: "",
key: "",
required: false,
used: true
},
email: {
text: "",
innerText: "",
key: "",
required: false,
used: true
},
phone: {
text: "",
innerText: "",
key: "",
required: false,
used: true
},
text: {
text: "",
innerText: "",
key: "",
required: false,
used: false
},
address: {
text: "",
innerText: "",
key: "",
required: false,
used: false
},
button: ""
},
meta: "", meta: "",
}; };

@ -1,129 +1,122 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { Box, Button, IconButton, Link, Paper, SxProps, TextField, Theme, Typography, useTheme } from "@mui/material"; import { Box, Button, IconButton, Link, Paper, SxProps, TextField, Theme, Typography, useTheme, Popover, useMediaQuery, Divider } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions"; import { incrementCurrentStep, updateQuiz } from "@root/quizes/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import React from "react"; import React from "react";
import InfoIcon from "../../assets/icons/InfoIcon"; import Info from "../../assets/icons/Info";
import Trash from "@icons/trash";
import { OneIcon } from "../../assets/icons/questionsPage/OneIcon"; import { OneIcon } from "../../assets/icons/questionsPage/OneIcon";
import AddPlus from "../../assets/icons/questionsPage/addPlus"; import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import { decrementCurrentStep } from "@root/quizes/actions";
import ButtonSettingForms from "./ButtonSettingForms"; import ButtonSettingForms from "./ButtonSettingForms";
import DrawerNewField from "./DrawerParent"; import DrawerNewField from "./DrawerParent";
import WindowMessengers from "./Massengers/WindowMessengers"; import WindowMessengers from "./Massengers/WindowMessengers";
import WindowNewField from "./NewField/WindowNewField"; import WindowNewField from "./NewField/WindowNewField";
import SwitchContactForm from "./switchContactForm"; import SwitchContactForm from "./switchContactForm";
import GearIcon from "@icons/GearIcon";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
const buttons = [
{
name: "Имя",
desc: "Дмитрий",
key: "name"
},
{
name: "Email",
desc: "mail@xample.ru",
key: "email"
},
{
name: "Номер",
desc: "+7 900 000 00 00",
key: "phone"
},
{
name: "Фамилия",
desc: "Иванов",
key: "text"
},
{
name: "Адрес",
desc: "Москва, Лаврушинский пер., 10",
key: "address"
},
]
export default function ContactFormPage() { export default function ContactFormPage() {
const [drawerNewField, setDrawerNewField] = React.useState(false); const theme = useTheme();
const quiz = useCurrentQuiz()
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const [drawerNewField, setDrawerNewField] = React.useState("");
const [drawerMessenger, setDrawerMessenger] = React.useState(false); const [drawerMessenger, setDrawerMessenger] = React.useState(false);
const drawerNewFieldHC = (bool: boolean) => { const drawerNewFieldHC = (str: string) => {
setDrawerNewField(bool); setDrawerNewField(str);
}; };
const drawerMessengerHC = (bool: boolean) => { const drawerMessengerHC = (bool: boolean) => {
setDrawerMessenger(bool); setDrawerMessenger(bool);
}; };
const theme = useTheme();
return ( return (
<> <Box
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> sx={{
p: isTablet ? "0 0 150px 0" : "0"
}}
>
<Box sx={{
display: "flex",
alignItems: "center",
gap: "10px",
m: "67px 0 41px 0",
}}>
<Link <Link
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
lineHeight: "19px", lineHeight: "19px",
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main, textDecorationColor: theme.palette.brightPurple.main,
}} }}
> >
Как собрать данные посетителя Как собрать данные посетителя
</Link>{" "} </Link>{" "}
<InfoIcon /> {/* <Popover>
<Info />
</Popover> */}
</Box> </Box>
<ContactFormParent>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "end" }}>
<OneIcon />
<IconButton>
{" "}
<ExpandMoreIcon />{" "}
</IconButton>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: "20px", padding: "10px 20px" }}>
<Typography>Имя*</Typography>
<CustomTextField placeholder="Дмитрий" text={""} />
<Typography>E-mail*</Typography>
<CustomTextField placeholder="+7 900 000 00 00" text={""} />
<Typography>Телефон*</Typography>
<CustomTextField placeholder="+7 900 000 00 00" text={""} />
<Button
onClick={() => drawerNewFieldHC(true)}
variant="contained"
sx={{ maxWidth: "fit-content", padding: "10px 20px" }}
>
Добавить поле +
</Button>
<DrawerNewField isOpen={drawerNewField} openHC={drawerNewFieldHC}>
<WindowNewField />
</DrawerNewField>
<Link
component="button"
onClick={() => drawerMessengerHC(true)}
sx={{
fontSize: "16px",
lineHeight: "19px",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
textAlign: "left",
}}
>
Добавить мессенджеры
</Link>
<DrawerNewField isOpen={drawerMessenger} openHC={drawerMessengerHC}>
<WindowMessengers />
</DrawerNewField>
<Button variant="contained" sx={{ padding: "10px 20px", maxWidth: "fit-content" }}>
Название кнопки
</Button>
</Box>
</ContactFormParent>
<ContactFormParent> {
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "end" }}> !quiz?.config.formContact.name.used &&
<OneIcon /> !quiz?.config.formContact.email.used &&
<IconButton> !quiz?.config.formContact.phone.used &&
{" "} !quiz?.config.formContact.text.used &&
<ExpandMoreIcon />{" "} !quiz?.config.formContact.address.used ?
</IconButton> <ContactFormParent>
</Box> <EmptyCard drawerNewFieldHC={drawerNewFieldHC} />
<Button variant="contained" sx={{ maxWidth: "fit-content", padding: "10px 20px" }}> </ContactFormParent>
Добавить поле + :
</Button> <ContactFormParent>
<Box sx={{ display: "flex" }}> <ButtonsCard drawerNewFieldHC={drawerNewFieldHC} />
<Typography sx={{ color: theme.palette.orange.main }}>Будут показаны поля по умолчанию</Typography> </ContactFormParent>
<InfoIcon /> }
</Box>
<Link
sx={{
mt: "20px",
fontSize: "16px",
lineHeight: "19px",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Добавить мессенджеры
</Link>
<Button variant="contained" sx={{ padding: "10px 20px", maxWidth: "fit-content" }}>
Название кнопки
</Button>
</ContactFormParent>
<Box sx={{ display: "flex", justifyContent: "space-between", maxWidth: "796px" }}> <Box sx={{ display: "flex", justifyContent: "space-between", maxWidth: "796px" }}>
<IconButton>
<AddPlus />
</IconButton>
<Box sx={{ display: "flex", gap: "8px" }}> <Box sx={{ display: "flex", gap: "8px" }}>
<Button variant="outlined"> <Button
onClick={decrementCurrentStep}
variant="outlined">
<ArrowLeft /> <ArrowLeft />
</Button> </Button>
<Button variant="contained" sx={{ padding: "10px 20px" }} onClick={incrementCurrentStep}> <Button variant="contained" sx={{ padding: "10px 20px" }} onClick={incrementCurrentStep}>
@ -131,7 +124,13 @@ export default function ContactFormPage() {
</Button> </Button>
</Box> </Box>
</Box> </Box>
</>
<DrawerNewField isOpenDrawer={drawerNewField} drawerNewFieldHC={drawerNewFieldHC}>
<WindowNewField type={drawerNewField} drawerNewFieldHC={drawerNewFieldHC} />
</DrawerNewField>
</Box>
); );
} }
@ -142,10 +141,8 @@ interface Props {
function ContactFormParent({ outerContainerSx: sx, children }: Props) { function ContactFormParent({ outerContainerSx: sx, children }: Props) {
const theme = useTheme(); const theme = useTheme();
const [switchState, setSwitchState] = React.useState("setting"); const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const SSHC = (data: string) => { const quiz = useCurrentQuiz()
setSwitchState(data);
};
return ( return (
<Paper <Paper
sx={{ sx={{
@ -154,23 +151,36 @@ function ContactFormParent({ outerContainerSx: sx, children }: Props) {
borderRadius: "12px", borderRadius: "12px",
margin: "20px 0", margin: "20px 0",
display: "flex", display: "flex",
flexDirection: "column",
}} }}
> >
<Box sx={{ width: "100%", display: "flex" }}> <Box sx={{ width: "100%", display: "flex", flexDirection: isTablet ? "column" : "row", }}>
<Box <Box
sx={{ sx={{
borderRight: `1px solid ${theme.palette.grey2.main}`, // borderRight: isTablet ? "none" : `1px solid ${theme.palette.grey2.main}`,
maxWidth: "386px", maxWidth: "386px",
width: "100%", width: "100%",
padding: "113px 20px 20px 20px", padding: "100px 20px 20px 20px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "20px", gap: "20px",
}} }}
> >
<CustomTextField placeholder="Заголовок формы" text={""} /> <CustomTextField
onChange={({ target }) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact.title = target.value
})
}}
value={quiz.config.formContact.title}
placeholder="Заголовок формы" text={""} />
<TextField <TextField
onChange={({ target }) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact.desc = target.value
})
}}
value={quiz.config.formContact.desc}
id="outlined-multiline-static" id="outlined-multiline-static"
multiline multiline
rows={8} rows={8}
@ -182,18 +192,216 @@ function ContactFormParent({ outerContainerSx: sx, children }: Props) {
borderRadius: "10px", borderRadius: "10px",
alignItems: "start", alignItems: "start",
color: theme.palette.grey2.main, color: theme.palette.grey2.main,
color:"black",
}, },
}} }}
/> />
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "column", padding: "20px", width: "100%", gap: "20px" }}> <Divider sx={{
{children} height: isTablet ? "1px" : "80%",
</Box> width: isTablet ? "80%" : "1px",
</Box> margin: "auto"
<Box> }}
<ButtonSettingForms switchState={switchState} SSHC={SSHC} /> orientation={isTablet ? "horisontal" : "vertical"}
<SwitchContactForm switchState={switchState} /> />
{children}
</Box> </Box>
</Paper> </Paper>
); );
} }
const SettingField = ({ name, placeholder, type, drawerNewFieldHC }: { name: string, placeholder: string, type: string; drawerNewFieldHC: any }) => {
const theme = useTheme();
const quiz = useCurrentQuiz()
return (
<>
<Typography>{quiz.config.formContact[type].text || name}</Typography>
<Box
sx={{ display: "flex", mb: "10px" }}
>
<Typography
sx={{
color: quiz.config.formContact[type].innerText ? "black" : "#9A9AAF",
fontSize: "20px",
backgroundColor: theme.palette.background.default,
height: "48px",
borderRadius: "10px",
border: "1px #9A9AAF solid",
lineHeight: "21px",
p: " 0 25px 0 14px ",
display: "flex",
alignItems: "center",
width: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
position: "relative"
}}
>
{quiz.config.formContact[type].innerText || placeholder}
<IconButton
onClick={() => drawerNewFieldHC(type)}
sx={{
position: "absolute",
right: "0"
}}>
<GearIcon height="20px" width="20px" color="#7e2aea" />
</IconButton>
</Typography>
<IconButton
onClick={() => updateQuiz(quiz?.id, (quiz) => {
quiz.config.formContact[type].used = false
})}
sx={{
width: "48px",
ml: "5px"
}}
>
<Trash />
</IconButton>
</Box>
</>
)
}
const ButtonsCard = ({ drawerNewFieldHC }: any) => {
const theme = useTheme();
const quiz = useCurrentQuiz()
return (
<Box sx={{ display: "flex", flexDirection: "column", padding: "20px", width: "100%", gap: "20px" }}>
{
buttons.map((contentData) => {
const content = quiz?.config.formContact[contentData.key]
return content.used ? <SettingField drawerNewFieldHC={drawerNewFieldHC} key={contentData.key} type={contentData.key} name={content.text || contentData.name} placeholder={content.innerText || contentData.desc} /> : <></>
})
}
{
(
quiz?.config.formContact.name.used &&
quiz?.config.formContact.email.used &&
quiz?.config.formContact.phone.used &&
quiz?.config.formContact.text.used &&
quiz?.config.formContact.address.used
) ?
<></>
:
<Button
onClick={() => drawerNewFieldHC("all")}
variant="contained"
sx={{ maxWidth: "fit-content", padding: "10px 20px" }}
>
Добавить поле +
</Button>
}
<Link
component="button"
// onClick={() => drawerMessengerHC(true)}
sx={{
fontSize: "16px",
lineHeight: "19px",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
textAlign: "left",
}}
>
Добавить мессенджеры
</Link>
<PseudoButton />
</Box>
)
}
const EmptyCard = ({ drawerNewFieldHC }: { drawerNewFieldHC: (a: string) => void }) => {
const theme = useTheme();
const [FC, setFC] = React.useState(false)
const openFC = () => setFC(true)
const closeFC = () => setFC(false)
const popover = React.useRef(null);
return (
<Box sx={{ display: "flex", flexDirection: "column", padding: "100px 20px 20px 20px", width: "100%", gap: "20px", }}>
<Button
onClick={() => drawerNewFieldHC("all")}
variant="contained" sx={{ maxWidth: "fit-content", padding: "10px 20px" }}>
Добавить поле +
</Button>
<Box sx={{ display: "flex" }}>
<Typography sx={{ color: theme.palette.orange.main }}>Будут показаны поля по умолчанию</Typography>
<Box ref={popover}>
<Info sx={{ ml: "6px", p: "0" }} onClick={openFC} />
</Box>
<Popover
open={FC}
onClose={closeFC}
anchorEl={popover.current}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
disableRestoreFocus
>
<Typography sx={{ m: "20px", textAlign: "center" }}>
Если вам не нужно собирать контакты, <br></br> отключите форму контактов
</Typography>
</Popover>
</Box>
<Link
sx={{
mt: "20px",
fontSize: "16px",
lineHeight: "19px",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
>
Добавить мессенджеры
</Link>
<PseudoButton />
</Box>
)
}
const PseudoButton = () => {
const quiz = useCurrentQuiz()
return (
<TextField
onChange={({ target }) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact.button = target.value
})
}}
value={quiz.config.formContact.button}
sx={{
heigth: "44px",
width: "190px",
"& .MuiInputBase-root": {
backgroundColor: "#7E2AEA",
borderRadius: "8px",
color: "white",
},
"& .MuiInputBase-input": {
padding: "10px 20px",
textAlign: "center"
},
"& .MuiInputBase-input::placeholder": {
color: "white",
opacity: "1"
}
}}
placeholder="Название кнопки"
>
{quiz?.config.formContact.button || "Название кнопки"}
</TextField>
)
}

@ -7,18 +7,18 @@ import {SxProps, Theme} from "@mui/material";
interface Props { interface Props {
outerContainerSx?: SxProps<Theme>; outerContainerSx?: SxProps<Theme>;
children?: React.ReactNode; children?: React.ReactNode;
isOpen: boolean; isOpenDrawer: string;
openHC: (arg0: boolean) => void drawerNewFieldHC: (str: string) => void
} }
export default function DrawerNewField({outerContainerSx: sx, children, isOpen, openHC }: Props) { export default function DrawerNewField({outerContainerSx: sx, children, isOpenDrawer: isOpen, drawerNewFieldHC }: Props) {
return ( return (
<> <>
<Drawer <Drawer
anchor='right' anchor='right'
open={isOpen} open={Boolean(isOpen)}
onClose={() => openHC(false)} onClose={() => drawerNewFieldHC("")}
> >
<Box <Box
sx={{ width: 450 }} sx={{ width: 450 }}

@ -6,14 +6,19 @@ import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon"; import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon"; import TextIcon from "@icons/ContactFormIcon/TextIcon";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon"; import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props { interface Props {
switchState: string switchState: string
SSHC: (data:string) => void SSHC: (data:string) => void
type: string
} }
export default function ButtonsNewField ({SSHC, switchState}:Props) { export default function ButtonsNewField ({SSHC, switchState, type}:Props) {
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz()
console.log(quiz)
console.log(type)
const buttonSetting: {icon: JSX.Element; title: string; value: string} [] =[ const buttonSetting: {icon: JSX.Element; title: string; value: string} [] =[
{icon: <NameIcon color={switchState === 'name' ? '#ffffff' : theme.palette.grey3.main}/>, title: 'Имя', value: 'name'}, {icon: <NameIcon color={switchState === 'name' ? '#ffffff' : theme.palette.grey3.main}/>, title: 'Имя', value: 'name'},
{icon: <EmailIcon color={switchState === 'email' ? '#ffffff' : theme.palette.grey3.main}/>, title: 'Email', value: 'email'}, {icon: <EmailIcon color={switchState === 'email' ? '#ffffff' : theme.palette.grey3.main}/>, title: 'Email', value: 'email'},
@ -31,16 +36,20 @@ export default function ButtonsNewField ({SSHC, switchState}:Props) {
}} }}
> >
{buttonSetting.map( (e,i) => ( {buttonSetting.map( (e,i) => (
type === e.value || type === "all" ?
<MiniButtonSetting <MiniButtonSetting
disabled = {quiz?.config.formContact[e.value]?.used}
key={i} key={i}
onClick={()=>{SSHC(e.value)}} onClick={()=>{SSHC(e.value)}}
sx={{backgroundColor: switchState === e.value ? theme.palette.brightPurple.main : 'transparent', sx={{backgroundColor: switchState === e.value ? theme.palette.brightPurple.main : 'transparent',
color: switchState === e.value ? '#ffffff' : theme.palette.grey3.main, color: switchState === e.value && type === "all" ? '#ffffff' : theme.palette.grey3.main,
}} }}
> >
{e.icon} {e.icon}
{e.title} {e.title}
</MiniButtonSetting> </MiniButtonSetting>
:
<></>
))} ))}
</Box> </Box>
) )

@ -1,9 +1,11 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import {FormControl, SxProps, TextField, Theme, Typography} from "@mui/material"; import { FormControl, SxProps, TextField, Theme, Typography } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import * as React from "react"; import * as React from "react";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateQuiz } from "@root/quizes/actions";
interface Props { interface Props {
outerContainerSx?: SxProps<Theme>; outerContainerSx?: SxProps<Theme>;
@ -11,51 +13,88 @@ interface Props {
defaultValue?: string; defaultValue?: string;
placeholderHelp: string; placeholderHelp: string;
placeholderField: string; placeholderField: string;
drawerNewFieldHC:(a:string)=>void
} }
export default function NewFieldParent ({defaultValue, placeholderHelp, placeholderField, outerContainerSx: sx, children}: Props) { export default function NewFieldParent({ drawerNewFieldHC, defaultValue, placeholderHelp, placeholderField, outerContainerSx: sx, children }: Props) {
return( const quiz = useCurrentQuiz()
<Box sx={{padding: '20px', display: 'flex', flexDirection: 'column', gap: '20px'}}> console.log({ defaultValue, placeholderHelp, placeholderField, outerContainerSx: sx, children })
<Box sx={{display: 'flex', flexDirection: 'column', gap: '15px'}}> return (
<Typography>Подсказка</Typography> <Box sx={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
<CustomTextField placeholder={placeholderHelp} text={''}/> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<Typography
>Подсказка</Typography>
<CustomTextField
onChange={({ target }) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact[defaultValue].text = target.value
})
}}
value={quiz.config.formContact[defaultValue].text}
placeholder={placeholderHelp} text={''} />
</Box> </Box>
<Box sx={{display: 'flex', flexDirection: 'column', gap: '15px'}}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<Typography>Подсказка внутри поля</Typography> <Typography
<CustomTextField placeholder={placeholderField} text={''}/> >Подсказка внутри поля</Typography>
<CustomTextField
onChange={({ target }) => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact[defaultValue].innerText = target.value
})
}}
value={quiz.config.formContact[defaultValue].innerText}
placeholder={placeholderField} text={''} />
</Box> </Box>
<Box sx={{display: 'flex', flexDirection: 'column', gap: '15px'}}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<Typography>Ключ</Typography> <Typography>Ключ</Typography>
<FormControl <TextField
fullWidth value={quiz.config.formContact[defaultValue].key}
variant="standard" onChange={({ target }) => {
sx={{ p: 0, border: 0 }} updateQuiz(quiz.id, (quiz) => {
> quiz.config.formContact[defaultValue].key = target.value
<TextField })
disabled }}
id="outlined-disabled" placeholder="text"
value={defaultValue} sx={{
sx={{ "& .css-1d3z3hw-MuiOutlinedInput-notchedOutline": {
"& .css-1d3z3hw-MuiOutlinedInput-notchedOutline": { border: 'none'
border: 'none' },
}, "& .MuiInputBase-root": {
"& .MuiInputBase-root": { height: "48px",
height: "48px", borderRadius: "10px",
borderRadius: "10px", backgroundColor: '#EEE4FC',
backgroundColor: '#EEE4FC', },
}, }}
}}
/> />
</FormControl> <CustomCheckbox
<CustomCheckbox label={"Обязательно к заполнению"}/> checked={quiz.config.formContact[defaultValue].required}
handleChange={({ target }) => {
console.log("click")
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact[defaultValue].required = target.checked
})
}}
label={"Обязательно к заполнению"} />
</Box> </Box>
<Box> {/* <Box>
<Typography>Запрашивать на</Typography> <Typography>Запрашивать на</Typography>
<CustomCheckbox label={'Шаг 1'}/> <CustomCheckbox label={'Шаг 1'}/>
<CustomCheckbox label={'Шаг 2'}/> <CustomCheckbox label={'Шаг 2'}/>
</Box> </Box> */}
{children} {children}
<Button variant='contained' sx={{maxWidth: '125px'}}>Добавить</Button> <Button
onClick={() => {
updateQuiz(quiz.id, (quiz) => {
quiz.config.formContact[defaultValue].used = true
})
drawerNewFieldHC("")
}}
variant='contained' sx={{ maxWidth: '125px' }}>Добавить</Button>
</Box> </Box>
) )
} }

@ -8,27 +8,29 @@ import CustomizedSwitch from "@ui_kit/CustomSwitch";
interface Props { interface Props {
switchState: string, switchState: string,
drawerNewFieldHC:(a:string)=>void
} }
export default function SwitchNewField({switchState ='name'}: Props) { export default function SwitchNewField({switchState ='name', drawerNewFieldHC}: Props) {
const [SwitchMask, setSwitchMask] = React.useState(false); const [SwitchMask, setSwitchMask] = React.useState(false);
console.log(switchState)
const SwitchMaskHC = (bool:boolean) => { const SwitchMaskHC = (bool:boolean) => {
setSwitchMask(bool) setSwitchMask(bool)
} }
switch (switchState) { switch (switchState) {
case 'name': case 'name':
return (<NewFieldParent placeholderHelp={'Введите имя'} placeholderField={'Дмитрий'} defaultValue={'name'}/>); return (<NewFieldParent drawerNewFieldHC={drawerNewFieldHC} placeholderHelp={'Введите имя'} placeholderField={'Дмитрий'} defaultValue={'name'}/>);
break; break;
case 'email': case 'email':
return (<NewFieldParent placeholderHelp={'Введите Email'} placeholderField={'mail@example.ru'} defaultValue={'email'}/>); return (<NewFieldParent drawerNewFieldHC={drawerNewFieldHC} placeholderHelp={'Введите Email'} placeholderField={'mail@example.ru'} defaultValue={'email'}/>);
break; break;
case 'phone': case 'phone':
return ( return (
<NewFieldParent placeholderHelp={'Введите номер'} placeholderField={'+7 900 000 00 00'} defaultValue={'phone'}> <NewFieldParent drawerNewFieldHC={drawerNewFieldHC} placeholderHelp={'Введите номер'} placeholderField={'+7 900 000 00 00'} defaultValue={'phone'}>
<FormControlLabel {/* <FormControlLabel
value="start" value="start"
control={<CustomizedSwitch/>} control={<CustomizedSwitch/>}
label="Маска для телефона" label="Маска для телефона"
@ -41,7 +43,7 @@ export default function SwitchNewField({switchState ='name'}: Props) {
SwitchMaskHC(!SwitchMask) SwitchMaskHC(!SwitchMask)
}} }}
/> /> */}
{SwitchMask ? {SwitchMask ?
<SelectMask/> <SelectMask/>
@ -52,10 +54,10 @@ export default function SwitchNewField({switchState ='name'}: Props) {
); );
break; break;
case 'text': case 'text':
return (<NewFieldParent placeholderHelp={'Введите фамилию'} placeholderField={'Иванов'} defaultValue={'text'}/>); return (<NewFieldParent drawerNewFieldHC={drawerNewFieldHC} placeholderHelp={'Введите фамилию'} placeholderField={'Иванов'} defaultValue={'text'}/>);
break; break;
case 'address': case 'address':
return (<NewFieldParent placeholderHelp={'Введите адрес'} placeholderField={'Москва, Лаврушинский пер., 10'} defaultValue={'address'}/>); return (<NewFieldParent drawerNewFieldHC={drawerNewFieldHC} placeholderHelp={'Введите адрес'} placeholderField={'Москва, Лаврушинский пер., 10'} defaultValue={'address'}/>);
break; break;
default: default:
return (<></>) return (<></>)

@ -1,29 +1,39 @@
import * as React from 'react'; import * as React from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import {Typography, useTheme} from "@mui/material"; import { Typography, useTheme } from "@mui/material";
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import ButtonsNewField from "./ButtonsNewField"; import ButtonsNewField from "./ButtonsNewField";
import SwitchNewField from "./SwitchNewField"; import SwitchNewField from "./SwitchNewField";
import { useCurrentQuiz } from '@root/quizes/hooks';
export default function WindowNewField() { export default function WindowNewField({ type, drawerNewFieldHC }: { type: string, drawerNewFieldHC: (a: string) => void }) {
const quiz = useCurrentQuiz()
const theme = useTheme(); const theme = useTheme();
const [switchState, setSwitchState] = React.useState('name'); const [switchState, setSwitchState] = React.useState("");
React.useEffect(() => {
for (let val in quiz?.config.formContact) {
if (!quiz?.config.formContact[val]?.used && (val !== "title" && val !== "desc" && val !== "button")) {
setSwitchState(val)
return
}
}
}, [])
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data) setSwitchState(data)
} }
return (
return(
<> <>
<Box sx={{padding: '10px 10px 10px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: theme.palette.background.default}}> <Box sx={{ padding: '10px 10px 10px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: theme.palette.background.default }}>
<Typography>Новое поле</Typography> <Typography>Новое поле</Typography>
<Button sx={{padding: 0}}><CloseIcon fontSize='large'/></Button> <Button onClick={() => drawerNewFieldHC("")} sx={{ padding: 0 }}><CloseIcon fontSize='large' /></Button>
</Box> </Box>
<Box><ButtonsNewField switchState={switchState} SSHC={SSHC}/></Box> <Box><ButtonsNewField type={type} switchState={switchState} SSHC={SSHC} /></Box>
<SwitchNewField switchState={switchState}/> <SwitchNewField switchState={switchState} drawerNewFieldHC={drawerNewFieldHC} />
</> </>
) )
} }

@ -104,10 +104,7 @@ export default function InstallQuiz() {
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<LinkIcon <LinkIcon color={theme.palette.brightPurple.main} bgcolor={"#EEE4FC"} />
color={theme.palette.brightPurple.main}
bgcolor={"#EEE4FC"}
/>
<Typography>Ссылка на квиз</Typography> <Typography>Ссылка на квиз</Typography>
<Tooltip <Tooltip
@ -242,11 +239,7 @@ export default function InstallQuiz() {
}} }}
/> />
</FormControl> </FormControl>
<CopyIcon <CopyIcon color={theme.palette.brightPurple.main} bgcolor={"#EEE4FC"} marL={"10px"} />
color={theme.palette.brightPurple.main}
bgcolor={"#EEE4FC"}
marL={"10px"}
/>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -274,16 +267,11 @@ export default function InstallQuiz() {
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<VkIcon <VkIcon color={theme.palette.brightPurple.main} bgcolor={"#EEE4FC"} />
color={theme.palette.brightPurple.main}
bgcolor={"#EEE4FC"}
/>
<Typography> Вконтакте</Typography> <Typography> Вконтакте</Typography>
</Box> </Box>
<Typography> <Typography>Для публикации сниппета на стене группы, призывающего пройти тест.</Typography>
Для публикации сниппета на стене группы, призывающего пройти тест.
</Typography>
<Link <Link
component="button" component="button"
onClick={handleOpenVk} onClick={handleOpenVk}
@ -312,16 +300,10 @@ export default function InstallQuiz() {
}} }}
> >
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<DomenIcon <DomenIcon color={theme.palette.brightPurple.main} bgcolor={"#EEE4FC"} />
color={theme.palette.brightPurple.main}
bgcolor={"#EEE4FC"}
/>
<Typography>Свой домен</Typography> <Typography>Свой домен</Typography>
</Box> </Box>
<Typography> <Typography>Подключите свой домен, если хотите, чтобы квиз открывался по вашей ссылке.</Typography>
Подключите свой домен, если хотите, чтобы квиз открывался по вашей
ссылке.
</Typography>
<Link <Link
component="button" component="button"
onClick={handleOpenDom} onClick={handleOpenDom}
@ -397,16 +379,13 @@ export default function InstallQuiz() {
<AutoOpenInstall /> <AutoOpenInstall />
<VidjetInstall /> <VidjetInstall />
<InstallQzCode /> <InstallQzCode />
<Box <Box sx={{ display: "flex", gap: "8px", justifyContent: "end", mt: "30px" }}>
sx={{ display: "flex", gap: "8px", justifyContent: "end", mt: "30px" }} <Button variant="outlined" sx={{ padding: "10px 20px", borderRadius: "8px" }}>
>
<Button
variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px" }}
>
<ArrowLeft /> <ArrowLeft />
</Button> </Button>
<Button variant="contained" onClick={incrementCurrentStep}>Запустить рекламу</Button> <Button variant="contained" onClick={incrementCurrentStep}>
Запустить рекламу
</Button>
</Box> </Box>
<Modal <Modal
@ -438,9 +417,7 @@ export default function InstallQuiz() {
background: theme.palette.background.default, background: theme.palette.background.default,
}} }}
> >
<Typography sx={{ color: theme.palette.grey2.main }}> <Typography sx={{ color: theme.palette.grey2.main }}>Добавить квиз в группу ВК</Typography>
Добавить квиз в группу ВК
</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -473,19 +450,12 @@ export default function InstallQuiz() {
Добавить приложение Добавить приложение
</Button> </Button>
<Typography sx={{ color: theme.palette.grey2.main }}> <Typography sx={{ color: theme.palette.grey2.main }}>
Для публикации сниппета на стене, призывающего пройти тест, Для публикации сниппета на стене, призывающего пройти тест, вставьте в новую запись ссылку на приложение
вставьте в новую запись ссылку на приложение
</Typography> </Typography>
<CustomTextField <CustomTextField placeholder={""} text={"https://vk.com/app6656524_-XXXXXXXXXXX"} />
placeholder={""} <Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>
text={"https://vk.com/app6656524_-XXXXXXXXXXX"} где XXXXXXXXXXX - id вашего сообщества (полный адрес ссылки можно узнать в браузерной строке, открыв
/> приложение в вашей группе
<Typography
sx={{ fontSize: "14px", color: theme.palette.grey2.main }}
>
где XXXXXXXXXXX - id вашего сообщества (полный адрес ссылки
можно узнать в браузерной строке, открыв приложение в вашей
группе
</Typography> </Typography>
</Box> </Box>
@ -496,24 +466,13 @@ export default function InstallQuiz() {
flexDirection: "column", flexDirection: "column",
}} }}
> >
<Typography> <Typography>2. Откройте квиз в группе (вы должны быть администратором группы)</Typography>
2. Откройте квиз в группе (вы должны быть администратором <Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>
группы) Справа снизу нажмите на значок "редактировать" В появившемся окне введите id квиза и нажмите
"Привязать". Готово! Квиз привязан к группе
</Typography> </Typography>
<Typography <Typography sx={{ color: theme.palette.grey2.main }}>ID этого квиза</Typography>
sx={{ fontSize: "14px", color: theme.palette.grey2.main }} <CustomTextField placeholder={""} text={"639727c5177be5004f11a0f2"} />
>
Справа снизу нажмите на значок "редактировать" В появившемся
окне введите id квиза и нажмите "Привязать". Готово! Квиз
привязан к группе
</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>
ID этого квиза
</Typography>
<CustomTextField
placeholder={""}
text={"639727c5177be5004f11a0f2"}
/>
</Box> </Box>
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}> <Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
@ -555,9 +514,7 @@ export default function InstallQuiz() {
background: theme.palette.background.default, background: theme.palette.background.default,
}} }}
> >
<Typography sx={{ color: theme.palette.grey2.main }}> <Typography sx={{ color: theme.palette.grey2.main }}>Подключить свой домен</Typography>
Подключить свой домен
</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -575,21 +532,14 @@ export default function InstallQuiz() {
}} }}
> >
<Typography sx={{ color: theme.palette.grey2.main }}> <Typography sx={{ color: theme.palette.grey2.main }}>
Подключите домен к проекту, чтобы создать несколько квизов на Подключите домен к проекту, чтобы создать несколько квизов на одном домене
одном домене
</Typography> </Typography>
<Typography>1. Настройте записи в регистраторе домена</Typography> <Typography>1. Настройте записи в регистраторе домена</Typography>
<Box sx={{ display: "flex" }}> <Box sx={{ display: "flex" }}>
<SelectableButton <SelectableButton isSelected={backgroundType === "text"} onClick={() => setBackgroundType("text")}>
isSelected={backgroundType === "text"}
onClick={() => setBackgroundType("text")}
>
Для поддоменов Для поддоменов
</SelectableButton> </SelectableButton>
<SelectableButton <SelectableButton isSelected={backgroundType === "video"} onClick={() => setBackgroundType("video")}>
isSelected={backgroundType === "video"}
onClick={() => setBackgroundType("video")}
>
Для доменов Для доменов
</SelectableButton> </SelectableButton>
</Box> </Box>
@ -623,8 +573,7 @@ export default function InstallQuiz() {
maxWidth: "372px", maxWidth: "372px",
}} }}
> >
Как подключить свой домен/поддомен к квизу? Ошибки при Как подключить свой домен/поддомен к квизу? Ошибки при подключении домена
подключении домена
</Link> </Link>
</Box> </Box>
@ -655,10 +604,7 @@ export default function InstallQuiz() {
// onMouseDown={} // onMouseDown={}
edge="end" edge="end"
> >
<CopyIcon <CopyIcon color={"#ffffff"} bgcolor={theme.palette.brightPurple.main} />
color={"#ffffff"}
bgcolor={theme.palette.brightPurple.main}
/>
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }
@ -668,11 +614,8 @@ export default function InstallQuiz() {
</Box> </Box>
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}> <Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
<Typography <Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>
sx={{ fontSize: "14px", color: theme.palette.grey2.main }} Привязка домена и обновление DNS записей может занять до 48 часов
>
Привязка домена и обновление DNS записей может занять до 48
часов
</Typography> </Typography>
<Button variant="outlined" onClick={handleCloseDom}> <Button variant="outlined" onClick={handleCloseDom}>
Отмена Отмена

@ -18,7 +18,7 @@ export default function Component() {
const [select, setSelect] = React.useState(0); const [select, setSelect] = React.useState(0);
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation() const location = useLocation();
const onClick = () => (userId ? navigate("/list") : navigate("/signin")); const onClick = () => (userId ? navigate("/list") : navigate("/signin"));
@ -41,7 +41,9 @@ export default function Component() {
padding: 0, padding: 0,
}} }}
> >
<QuizLogo width={isMobile ? 100 : 150} /> <Link to="/">
<QuizLogo width={isMobile ? 100 : 150} />
</Link>
{/*<Box*/} {/*<Box*/}
{/* sx={{*/} {/* sx={{*/}
{/* maxWidth: '595px',*/} {/* maxWidth: '595px',*/}
@ -67,7 +69,7 @@ export default function Component() {
{/* ))}*/} {/* ))}*/}
{/*</Box>*/} {/*</Box>*/}
<Button <Button
variant="outlined" variant="outlined"
onClick={onClick} onClick={onClick}
sx={{ sx={{
color: "black", color: "black",

@ -4,17 +4,17 @@ import {} from "react-router-dom";
import { useLocation, Link } from "react-router-dom"; import { useLocation, Link } from "react-router-dom";
import { import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
Button, Button,
Dialog, Dialog,
ListItem, ListItem,
AppBar, AppBar,
List, List,
Toolbar, Toolbar,
IconButton, IconButton,
Box, Box,
Slide, Slide,
} from "@mui/material"; } from "@mui/material";
import { TransitionProps } from "@mui/material/transitions"; import { TransitionProps } from "@mui/material/transitions";
@ -38,212 +38,202 @@ import Logotip from "./images/icons/QuizLogo";
// ]; // ];
interface Props { interface Props {
theme?: "dark" | "light"; theme?: "dark" | "light";
bgColor?: string; bgColor?: string;
} }
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement; children: React.ReactElement;
}, },
ref: React.Ref<unknown> ref: React.Ref<unknown>
) { ) {
return <Slide direction={"left"} ref={ref} {...props} />; return <Slide direction={"left"} ref={ref} {...props} />;
}); });
const height = "80px"; const height = "80px";
export default function FullScreenDialog({ export default function FullScreenDialog({ theme = "dark", bgColor = "#F2F3F7" }: Props) {
theme = "dark", const [open, setOpen] = useState(false);
bgColor = "#F2F3F7", const location = useLocation();
}: Props) { const themeMUI = useTheme();
const [open, setOpen] = useState(false); const isMobile = useMediaQuery(themeMUI.breakpoints.down("md"));
const location = useLocation();
const themeMUI = useTheme();
const isMobile = useMediaQuery(themeMUI.breakpoints.down("md"));
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true); setOpen(true);
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
return ( return (
<> <>
<SectionStyled <SectionStyled
tag="header" tag="header"
bg={bgColor} bg={bgColor}
mwidth={"1200px"} mwidth={"1200px"}
sxsect={{ sxsect={{
minHeight: isMobile? "50px" : {height}, minHeight: isMobile ? "50px" : { height },
position: "fixed", position: "fixed",
zIndex: 11 zIndex: 11,
}}
sxcont={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
svg: { color: "#000000" },
padding: isMobile ? 0 : "0 22px 0 40px",
}}
>
<Box sx={{ bgcolor: "none", paddingTop: isMobile ? "6px" : 0 }}>
<Link to="/">
<Logotip width={150} />
</Link>
</Box>
{!isMobile && (
<Button
// onClick={() => setIsContactFormOpen(true)}
disableRipple
sx={{
color: "black",
border: "1px solid black",
ml: "auto",
mr: "38px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
"&:hover": {
background: "rgba(126, 42, 234, 0.07)",
bgcolor: "#7E2AEA",
},
}}
>
Предрегистрация
</Button>
)}
}} <Button disableRipple variant="text" onClick={handleClickOpen}>
sxcont={{ <MenuIcon />
display: "flex", </Button>
justifyContent: "space-between", <Dialog
alignItems: "center", fullScreen
svg: { color: "#000000" }, sx={{ width: isMobile ? "100%" : "320px", ml: "auto", height: "100%" }}
padding: isMobile ? 0 : "0 22px 0 40px" open={open}
}} onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar
sx={{
position: "relative",
background: theme === "dark" ? "#252734" : "#F2F3F7",
boxShadow: "none",
height: isMobile ? "66px" : "100px",
}}
>
<Toolbar
sx={{
display: "flex",
justifyContent: "space-between",
svg: { color: theme === "dark" ? "#ffffff" : "#000000" },
pt: "12px",
}}
> >
<Box sx={{ bgcolor: "none", paddingTop: isMobile? "6px" : 0 }}> {isMobile && <Logotip width={150} />}
<Logotip width={150}/> <IconButton sx={{ ml: "auto" }} edge="start" color="inherit" onClick={handleClose} aria-label="close">
</Box> <CloseIcon />
{!isMobile && ( </IconButton>
<Button </Toolbar>
// onClick={() => setIsContactFormOpen(true)} </AppBar>
disableRipple <List
sx={{ sx={{
color: "black", background: theme === "dark" ? "#252734" : "#F2F3F7",
border: "1px solid black", height: "100vh",
ml: "auto", p: "0",
mr: "38px", }}
textTransform: "none", >
fontWeight: "400", {/*<ListItem*/}
fontSize: "18px", {/* sx={{*/}
lineHeight: "24px", {/* pl: "40px",*/}
borderRadius: "8px", {/* flexDirection: "column",*/}
padding: "8px 17px", {/* alignItems: isMobile ? "stretch" : "end",*/}
"&:hover": { {/* }}*/}
background: "rgba(126, 42, 234, 0.07)", {/*>*/}
bgcolor: "#7E2AEA", {/* {buttonMenu.map(({ path, title }) => (*/}
}, {/* <Link*/}
}} {/* key={path}*/}
> {/* to={path}*/}
Предрегистрация {/* style={{*/}
</Button> {/* textDecoration: "none",*/}
)} {/* height: "20px",*/}
{/* marginBottom: "25px",*/}
<Button disableRipple variant="text" onClick={handleClickOpen}> {/* }}*/}
<MenuIcon /> {/* >*/}
</Button> {/* <Button*/}
<Dialog {/* disableRipple*/}
fullScreen {/* variant="text"*/}
sx={{ width: isMobile ? "100%" : "320px", ml: "auto", height: "100%" }} {/* sx={{*/}
open={open} {/* color:*/}
onClose={handleClose} {/* location.pathname === path*/}
TransitionComponent={Transition} {/* ? "#7E2AEA"*/}
> {/* : theme === "dark"*/}
<AppBar {/* ? "white"*/}
sx={{ {/* : "black",*/}
position: "relative", {/* height: "20px",*/}
background: theme === "dark" ? "#252734" : "#F2F3F7", {/* textTransform: "none",*/}
boxShadow: "none", {/* fontSize: "16px",*/}
height: isMobile ? "66px" : "100px", {/* "&:hover": {*/}
}} {/* background: "none",*/}
> {/* color: "#7E2AEA",*/}
<Toolbar {/* },*/}
sx={{ {/* }}*/}
display: "flex", {/* >*/}
justifyContent: "space-between", {/* {title}*/}
svg: { color: theme === "dark" ? "#ffffff" : "#000000" }, {/* </Button>*/}
pt: "12px", {/* </Link>*/}
}} {/* ))}*/}
> {/*</ListItem>*/}
{isMobile && ( {isMobile ? (
<Logotip width={150}/> <Button
)} // onClick={() => setIsContactFormOpen(true)}
<IconButton variant="outlined"
sx={{ ml: "auto" }} sx={{
edge="start" position: "absolute",
color="inherit" bottom: 0,
onClick={handleClose} mb: "60px",
aria-label="close" width: "188px",
> color: theme === "dark" ? "white" : "black",
<CloseIcon /> border: "1px solid black",
</IconButton> ml: "40px",
</Toolbar> mt: "180px",
</AppBar> textTransform: "none",
<List fontWeight: "400",
sx={{ fontSize: "18px",
background: theme === "dark" ? "#252734" : "#F2F3F7", lineHeight: "24px",
height: "100vh", borderRadius: "8px",
p: "0", padding: "8px 17px",
}} }}
> >
{/*<ListItem*/} Предрегистрация
{/* sx={{*/} </Button>
{/* pl: "40px",*/} ) : (
{/* flexDirection: "column",*/} <Box
{/* alignItems: isMobile ? "stretch" : "end",*/} sx={{
{/* }}*/} position: "absolute",
{/*>*/} right: "40px",
{/* {buttonMenu.map(({ path, title }) => (*/} bottom: "60px",
{/* <Link*/} }}
{/* key={path}*/} >
{/* to={path}*/} <Logotip width={150} />
{/* style={{*/} </Box>
{/* textDecoration: "none",*/} )}
{/* height: "20px",*/} </List>
{/* marginBottom: "25px",*/} </Dialog>
{/* }}*/} </SectionStyled>
{/* >*/} <Box sx={{ height: isMobile ? "50px" : { height } }} />
{/* <Button*/} </>
{/* disableRipple*/} );
{/* variant="text"*/}
{/* sx={{*/}
{/* color:*/}
{/* location.pathname === path*/}
{/* ? "#7E2AEA"*/}
{/* : theme === "dark"*/}
{/* ? "white"*/}
{/* : "black",*/}
{/* height: "20px",*/}
{/* textTransform: "none",*/}
{/* fontSize: "16px",*/}
{/* "&:hover": {*/}
{/* background: "none",*/}
{/* color: "#7E2AEA",*/}
{/* },*/}
{/* }}*/}
{/* >*/}
{/* {title}*/}
{/* </Button>*/}
{/* </Link>*/}
{/* ))}*/}
{/*</ListItem>*/}
{isMobile ? (
<Button
// onClick={() => setIsContactFormOpen(true)}
variant="outlined"
sx={{
position: "absolute",
bottom: 0,
mb: "60px",
width: "188px",
color: theme === "dark" ? "white" : "black",
border: "1px solid black",
ml: "40px",
mt: "180px",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
Предрегистрация
</Button>
) : (
<Box
sx={{
position: "absolute",
right: "40px",
bottom: "60px",
}}
>
<Logotip width={150}/>
</Box>
)}
</List>
</Dialog>
</SectionStyled>
<Box sx={{height: isMobile ? "50px" : {height}}} />
</>
);
} }

@ -7,7 +7,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions" import { updateRootContentId } from "@root/quizes/actions"
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared" import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions"; import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll, createFrontResult } from "@root/questions/actions";
import { updateOpenedModalSettingsId, } from "@root/uiTools/actions"; import { updateOpenedModalSettingsId, } from "@root/uiTools/actions";
import { cleardragQuestionContentId } from "@root/uiTools/actions"; import { cleardragQuestionContentId } from "@root/uiTools/actions";
import { withErrorBoundary } from "react-error-boundary"; import { withErrorBoundary } from "react-error-boundary";
@ -161,7 +161,6 @@ function CsComponent({
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => { const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
//запрещаем работу родителя-ребенка если это один и тот же вопрос //запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return if (parentNodeContentId === targetNodeContentId) return
@ -170,10 +169,10 @@ function CsComponent({
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || dragQuestionContentId) } as AnyTypedQuizQuestion const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || dragQuestionContentId) } as AnyTypedQuizQuestion
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren }) clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed', true) cy?.data('changed', true)
createFrontResult(quiz.backendId, targetQuestion.content.id)
const es = cy?.add([ const es = cy?.add([
{ {
data: { data: {
@ -203,6 +202,7 @@ function CsComponent({
//смотрим не добавлен ли родителю result. Если да - убираем его. Веточкам result не нужен //смотрим не добавлен ли родителю result. Если да - убираем его. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => { trashQuestions.forEach((targetQuestion) => {
if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) { if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) {
console.log('deleteQ', targetQuestion.id)
deleteQuestion(targetQuestion.id); deleteQuestion(targetQuestion.id);
} }
}) })
@ -271,7 +271,8 @@ function CsComponent({
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
if (targetNodeContentId && parentQuestionContentId) { if (targetNodeContentId && parentQuestionContentId) {
if (cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0)
createFrontResult(quiz.backendId, parentQuestionContentId)
clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId }) clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId })
cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).run() cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).run()
} }

@ -73,6 +73,7 @@ export default function ButtonsOptionsAndPict({
display: "flex", display: "flex",
flexWrap: isMobile ? "wrap" : "nowrap", flexWrap: isMobile ? "wrap" : "nowrap",
gap: "6px", gap: "6px",
maxWidth: isMobile ? "200px" : undefined
}} }}
> >
<MiniButtonSetting <MiniButtonSetting

@ -18,18 +18,28 @@ import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider"; import Slider from "@icons/questionsPage/slider";
import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { import {
Box, Box,
Checkbox, Checkbox,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
IconButton, IconButton,
InputAdornment, InputAdornment,
Paper, Paper,
TextField, TextField,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { copyQuestion, createUntypedQuestion, deleteQuestion, clearRuleForAll, toggleExpandQuestion, updateQuestion, updateUntypedQuestion, getQuestionByContentId, deleteQuestionWithTimeout } from "@root/questions/actions"; import {
copyQuestion,
createUntypedQuestion,
deleteQuestion,
clearRuleForAll,
toggleExpandQuestion,
updateQuestion,
updateUntypedQuestion,
getQuestionByContentId,
deleteQuestionWithTimeout,
} from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions"; import { updateRootContentId } from "@root/quizes/actions";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
@ -44,445 +54,387 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
interface Props { interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
draggableProps: DraggableProvidedDragHandleProps | null | undefined; draggableProps: DraggableProvidedDragHandleProps | null | undefined;
isDragging: boolean; isDragging: boolean;
index: number; index: number;
} }
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) { export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
const { questions } = useQuestionsStore(); const { questions } = useQuestionsStore();
const [plusVisible, setPlusVisible] = useState<boolean>(false); const [plusVisible, setPlusVisible] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const anchorRef = useRef(null); const anchorRef = useRef(null);
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion; const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
updateQuestionFn(question.id, question => { updateQuestionFn(question.id, (question) => {
question.title = title; question.title = title;
}); });
}, 200); }, 200);
return ( return (
<> <>
<Paper <Paper
id={question.id} id={question.id}
data-cy="quiz-question-card" data-cy="quiz-question-card"
sx={{ sx={{
maxWidth: "796px", maxWidth: "796px",
width: "100%", width: "100%",
borderRadius: "12px", borderRadius: "12px",
backgroundColor: question.expanded ? "white" : "#EEE4FC", backgroundColor: question.expanded ? "white" : "#EEE4FC",
border: question.expanded ? "none" : "1px solid #9A9AAF", border: question.expanded ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7", boxShadow: "0px 10px 30px #e7e7e7",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
padding: isMobile ? "10px" : "20px 10px 20px 20px", padding: isMobile ? "10px" : "20px 10px 20px 20px",
flexDirection: isMobile ? "column" : null, flexDirection: isMobile ? "column" : null,
}} }}
> >
<FormControl <FormControl
variant="standard" variant="standard"
sx={{ sx={{
p: 0, p: 0,
maxWidth: isTablet ? "549px" : "640px", maxWidth: isTablet ? "549px" : "640px",
width: "100%", width: "100%",
marginRight: isMobile ? "0px" : "16.1px", marginRight: isMobile ? "0px" : "16.1px",
}} }}
>
<TextField
defaultValue={question.title}
placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value || " ")}
InputProps={{
startAdornment: (
<Box>
<InputAdornment
ref={anchorRef}
position="start"
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
> >
<TextField {IconAndrom(question.expanded, question.type)}
defaultValue={question.title} </InputAdornment>
placeholder={"Заголовок вопроса"} <ChooseAnswerModal
onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value || " ")} open={open}
InputProps={{ onClose={() => setOpen(false)}
startAdornment: ( anchorRef={anchorRef}
<Box> question={question}
<InputAdornment questionType={question.type}
ref={anchorRef}
position="start"
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(question.expanded, question.type)}
</InputAdornment>
<ChooseAnswerModal
open={open}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
question={question}
questionType={question.type}
/>
</Box>
),
}}
sx={{
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: question.expanded
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: !question.expanded ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: question.type === null ? 0 : "18px",
},
"data-cy": "quiz-question-title",
}}
/>
</FormControl>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => toggleExpandQuestion(question.id)}
>
{question.expanded ? (
<ArrowDownIcon
style={{
width: "18px",
color: "#4D4D4D",
}}
/>
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
{question.expanded ? (
<></>
) : (
<Box
sx={{
display: "flex",
height: "30px",
borderRight: "solid 1px #4D4D4D",
}}
>
<FormControlLabel
control={
<Checkbox
icon={
<HideIcon
style={{
boxSizing: "border-box",
color: "#7E2AEA",
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
padding: "3px",
}}
/>
}
checkedIcon={<CrossedEyeIcon />}
/>
}
label={""}
sx={{
color: theme.palette.grey2.main,
ml: "-9px",
mr: 0,
userSelect: "none",
}}
/>
<IconButton
sx={{ padding: "0" }}
onClick={() => copyQuestion(question.id, question.quizId)}
>
<CopyIcon
style={{ color: theme.palette.brightPurple.main }}
/>
</IconButton>
<IconButton
sx={{
cursor: "pointer",
borderRadius: "6px",
padding: "0",
margin: "0 5px 0 10px",
}}
onClick={() => {
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
});
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
const newRule = {};
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id);
} else {
console.log("удаляю безтипогово");
deleteQuestion(question.id);
}
};
deleteQuestionWithTimeout(question.id, deleteFn);
}}
data-cy="delete-question"
>
<DeleteIcon
style={{ color: theme.palette.brightPurple.main }}
/>
</IconButton>
</Box>
)}
{question.type !== null &&
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
>
{question.page + 1}
</Box>
}
<IconButton
disableRipple
sx={{
padding: isMobile ? "0" : "0 5px",
right: isMobile ? "0" : null,
bottom: isMobile ? "0" : null,
}}
{...draggableProps}
>
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
</IconButton>
</Box>
</Box>
{question.expanded && (
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: 0,
borderRadius: "12px",
}}
>
{question.type === null ? (
<TypeQuestions question={question} />
) : (
<SwitchQuestionsPage question={question} />
)}
</Box>
)}
</Paper>
<Box
onMouseEnter={() => setPlusVisible(true)}
onMouseLeave={() => setPlusVisible(false)}
sx={{
maxWidth: "825px",
display: "flex",
alignItems: "center",
height: "40px",
cursor: "pointer",
}}
>
<Box
onClick={() => createUntypedQuestion(question.quizId, question.id)}
sx={{
display: plusVisible && !isDragging ? "flex" : "none",
width: "100%",
alignItems: "center",
columnGap: "10px",
}}
data-cy="create-question"
>
<Box
sx={{
boxSizing: "border-box",
width: "100%",
height: "1px",
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage:
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/> />
<PlusIcon /> </Box>
</Box> ),
</Box> }}
</> sx={{
); margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: question.expanded ? theme.palette.background.default : "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: !question.expanded ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: question.type === null ? 0 : "18px",
},
"data-cy": "quiz-question-title",
}}
/>
</FormControl>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => toggleExpandQuestion(question.id)}
>
{question.expanded ? (
<ArrowDownIcon
style={{
width: "18px",
color: "#4D4D4D",
}}
/>
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
{question.expanded ? (
<></>
) : (
<Box
sx={{
display: "flex",
height: "30px",
borderRight: "solid 1px #4D4D4D",
}}
>
<FormControlLabel
control={
<Checkbox
icon={
<HideIcon
style={{
boxSizing: "border-box",
color: "#7E2AEA",
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
padding: "3px",
}}
/>
}
checkedIcon={<CrossedEyeIcon />}
/>
}
label={""}
sx={{
color: theme.palette.grey2.main,
ml: "-9px",
mr: 0,
userSelect: "none",
}}
/>
<IconButton sx={{ padding: "0" }} onClick={() => copyQuestion(question.id, question.quizId)}>
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
<IconButton
sx={{
cursor: "pointer",
borderRadius: "6px",
padding: "0",
margin: "0 5px 0 10px",
}}
onClick={() => {
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") {
//удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
questions.forEach((q) => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
} else if (question.content.rule.parentId.length > 0) {
//удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {
//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id))
clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, (question) => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
});
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
const newRule = {};
newRule.main = parentQuestion.content.rule.main.filter(
(data) => data.next !== question.content.id
); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default =
parentQuestion.content.rule.parentId === question.content.id
? ""
: parentQuestion.content.rule.parentId;
newRule.children = [...parentQuestion.content.rule.children].splice(
parentQuestion.content.rule.children.indexOf(question.content.id),
1
);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id);
} else {
console.log("удаляю безтипогово");
deleteQuestion(question.id);
}
};
deleteQuestionWithTimeout(question.id, deleteFn);
}}
data-cy="delete-question"
>
<DeleteIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
</Box>
)}
{question.type !== null && (
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded ? theme.palette.brightPurple.main : "#FFF",
background: question.expanded ? "#EEE4FC" : theme.palette.brightPurple.main,
}}
>
{question.page + 1}
</Box>
)}
<IconButton
disableRipple
sx={{
padding: isMobile ? "0" : "0 5px",
right: isMobile ? "0" : null,
bottom: isMobile ? "0" : null,
}}
{...draggableProps}
>
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
</IconButton>
</Box>
</Box>
{question.expanded && (
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: 0,
borderRadius: "12px",
}}
>
{question.type === null ? (
<TypeQuestions question={question} />
) : (
<SwitchQuestionsPage question={question} />
)}
</Box>
)}
</Paper>
<Box
onMouseEnter={() => setPlusVisible(true)}
onMouseLeave={() => setPlusVisible(false)}
sx={{
maxWidth: "825px",
display: "flex",
alignItems: "center",
height: "40px",
cursor: "pointer",
}}
>
<Box
onClick={() => createUntypedQuestion(question.quizId, question.id)}
sx={{
display: plusVisible && !isDragging ? "flex" : "none",
width: "100%",
alignItems: "center",
columnGap: "10px",
}}
data-cy="create-question"
>
<Box
sx={{
boxSizing: "border-box",
width: "100%",
height: "1px",
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage: "radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/>
<PlusIcon />
</Box>
</Box>
</>
);
} }
const IconAndrom = (isExpanded: boolean, questionType: QuestionType | null) => { const IconAndrom = (isExpanded: boolean, questionType: QuestionType | null) => {
switch (questionType) { switch (questionType) {
case "variant": case "variant":
return ( return <Answer color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
<Answer case "images":
color={isExpanded ? "#9A9AAF" : "#7E2AEA"} return <OptionsPict color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
sx={{ height: "22px", width: "20px" }} case "varimg":
/> return <OptionsAndPict color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
); case "emoji":
case "images": return <Emoji color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
return ( case "text":
<OptionsPict return <Input color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
color={isExpanded ? "#9A9AAF" : "#7E2AEA"} case "select":
sx={{ height: "22px", width: "20px" }} return <DropDown color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
/> case "date":
); return <Date color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
case "varimg": case "number":
return ( return <Slider color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
<OptionsAndPict case "file":
color={isExpanded ? "#9A9AAF" : "#7E2AEA"} return <Download color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
sx={{ height: "22px", width: "20px" }} case "page":
/> return <Page color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
); case "rating":
case "emoji": return <RatingIcon color={isExpanded ? "#9A9AAF" : "#7E2AEA"} sx={{ height: "22px", width: "20px" }} />;
return ( default:
<Emoji return <></>;
color={isExpanded ? "#9A9AAF" : "#7E2AEA"} }
sx={{ height: "22px", width: "20px" }}
/>
);
case "text":
return (
<Input
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "select":
return (
<DropDown
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "date":
return (
<Date
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "number":
return (
<Slider
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "file":
return (
<Download
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "page":
return (
<Page
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
case "rating":
return (
<RatingIcon
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
sx={{ height: "22px", width: "20px" }}
/>
);
default:
return <></>;
}
}; };

@ -5,80 +5,77 @@ import { Draggable } from "react-beautiful-dnd";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import QuestionsPageCard from "./QuestionPageCard"; import QuestionsPageCard from "./QuestionPageCard";
type FormDraggableListItemProps = { type FormDraggableListItemProps = {
question: AnyTypedQuizQuestion | UntypedQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
questionIndex: number; questionIndex: number;
}; };
export default memo( export default memo(({ question, questionIndex }: FormDraggableListItemProps) => {
({ question, questionIndex }: FormDraggableListItemProps) => { const theme = useTheme();
const theme = useTheme();
return ( return (
<Draggable draggableId={String(questionIndex)} index={questionIndex}> <Draggable draggableId={String(questionIndex)} index={questionIndex}>
{(provided) => ( {(provided) => (
<ListItem <ListItem
ref={provided.innerRef} ref={provided.innerRef}
{...(questionIndex !== 0 ? provided.draggableProps : {})} {...(questionIndex !== 0 ? provided.draggableProps : {})}
sx={{ userSelect: "none", padding: 0 }} sx={{ userSelect: "none", padding: 0 }}
> >
{question.deleted ? ( {question.deleted ? (
<Box <Box
{...provided.dragHandleProps} {...provided.dragHandleProps}
sx={{ sx={{
width: "100%", width: "100%",
maxWidth: "800px", maxWidth: "800px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
marginTop: "30px", marginTop: "30px",
gap: "5px", gap: "5px",
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
color: theme.palette.grey2.main, color: theme.palette.grey2.main,
}} }}
> >
Вопрос удалён. Вопрос удалён.
</Typography> </Typography>
<Typography <Typography
onClick={() => { onClick={() => {
updateQuestion(question.id, question => { updateQuestion(question.id, (question) => {
question.deleted = false; question.deleted = false;
}); });
}} }}
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
fontSize: "16px", fontSize: "16px",
textDecoration: "underline", textDecoration: "underline",
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main, textDecorationColor: theme.palette.brightPurple.main,
}} }}
> >
Восстановить? Восстановить?
</Typography> </Typography>
</Box> </Box>
) : ( ) : (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
position: "relative", position: "relative",
borderBottom: "1px solid rgba(0, 0, 0, 0.23)", borderBottom: "1px solid rgba(0, 0, 0, 0.23)",
}} }}
> >
<QuestionsPageCard <QuestionsPageCard
key={questionIndex} key={questionIndex}
question={question} question={question}
questionIndex={questionIndex} questionIndex={questionIndex}
draggableProps={provided.dragHandleProps} draggableProps={provided.dragHandleProps}
/> />
</Box> </Box>
)} )}
</ListItem> </ListItem>
)} )}
</Draggable> </Draggable>
); );
} });
);

@ -11,8 +11,8 @@ import OptionsPict from "@icons/questionsPage/options_pict";
import Page from "@icons/questionsPage/page"; import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating"; import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider"; import Slider from "@icons/questionsPage/slider";
import { Box, InputAdornment, Paper } from "@mui/material"; import { Box, FormControlLabel, IconButton, InputAdornment, Paper, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion, updateUntypedQuestion } from "@root/questions/actions"; import { toggleExpandQuestion, updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
@ -22,142 +22,206 @@ import SwitchQuestionsPage from "../../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal"; import { ChooseAnswerModal } from "./ChooseAnswerModal";
import FormTypeQuestions from "../FormTypeQuestions"; import FormTypeQuestions from "../FormTypeQuestions";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { HideIcon } from "@icons/questionsPage/hideIcon";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { NoLuggageOutlined, SignalCellularNullOutlined } from "@mui/icons-material";
interface Props { interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
questionIndex: number; questionIndex: number;
draggableProps: DraggableProvidedDragHandleProps | null | undefined; draggableProps: DraggableProvidedDragHandleProps | null | undefined;
} }
export default function QuestionsPageCard({ export default function QuestionsPageCard({ question, questionIndex, draggableProps }: Props) {
question, const [open, setOpen] = useState<boolean>(false);
questionIndex, const anchorRef = useRef(null);
draggableProps, const theme = useTheme();
}: Props) { const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [open, setOpen] = useState<boolean>(false); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion; const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
updateQuestionFn(question.id, question => { updateQuestionFn(question.id, (question) => {
question.title = title; question.title = title;
}); });
}, 200); }, 200);
return ( return (
<> <>
<Paper <Paper
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
maxWidth: "796px", maxWidth: "796px",
width: "100%", width: "100%",
backgroundColor: "white", backgroundColor: "white",
border: "none", border: "none",
boxShadow: "none", boxShadow: "none",
paddingBottom: "20px", paddingBottom: "20px",
borderRadius: "0", borderRadius: "0",
borderTopLeftRadius: "12px", borderTopLeftRadius: "12px",
borderTopRightRadius: "12px", borderTopRightRadius: "12px",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", p: 0,
padding: 0, flexDirection: "column",
}} }}
> >
<CustomTextField <Box
placeholder={`Заголовок ${questionIndex + 1} вопроса`} sx={{
text={question.title} display: "flex",
onChange={({ target }) => setTitle(target.value)} alignItems: "center",
sx={{ margin: "20px", width: "auto" }} margin: "20px",
InputProps={{ gap: "18px",
startAdornment: ( flexDirection: isMobile ? "column-reverse" : null,
<Box> }}
<InputAdornment >
ref={anchorRef} <CustomTextField
position="start" placeholder={`Заголовок ${questionIndex + 1} вопроса`}
sx={{ cursor: "pointer" }} text={question.title}
onClick={() => setOpen((isOpened) => !isOpened)} onChange={({ target }) => setTitle(target.value)}
> sx={{ width: "100%" }}
{IconAndrom(question.type)} InputProps={{
</InputAdornment> startAdornment: (
<ChooseAnswerModal <Box>
open={open} <InputAdornment
onClose={() => setOpen(false)} ref={anchorRef}
anchorRef={anchorRef} position="start"
question={question} sx={{ cursor: "pointer" }}
questionType={question.type} onClick={() => setOpen((isOpened) => !isOpened)}
/> >
</Box> {IconAndrom(question.type)}
), </InputAdornment>
endAdornment: ( <ChooseAnswerModal
<Box {...draggableProps}> open={open}
{questionIndex !== 0 && ( onClose={() => setOpen(false)}
<InputAdornment position="start"> anchorRef={anchorRef}
<PointsIcon question={question}
style={{ color: "#9A9AAF", fontSize: "30px" }} questionType={question.type}
/>
</InputAdornment>
)}
</Box>
),
}}
/> />
{question.type === null ? ( </Box>
<FormTypeQuestions question={question} /> ),
) : ( }}
<SwitchQuestionsPage question={question} /> />
)} <Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<Box
sx={{
flexDirection: isMobile ? "row-reverse" : null,
display: "flex",
alignItems: "center",
gap: "4px",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => toggleExpandQuestion(question.id)}
>
{question.expanded ? (
<ArrowDownIcon
style={{
width: "18px",
color: "#4D4D4D",
}}
/>
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded ? theme.palette.brightPurple.main : "#FFF",
background: question.expanded ? "#EEE4FC" : theme.palette.brightPurple.main,
}}
>
{questionIndex + 1}
</Box> </Box>
</Paper> </Box>
</>
); <IconButton
disableRipple
sx={{
padding: isMobile ? "0" : "0 5px",
right: isMobile ? "0" : null,
bottom: isMobile ? "0" : null,
}}
{...draggableProps}
>
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
</IconButton>
</Box>
</Box>
{question.type === null ? (
<FormTypeQuestions question={question} />
) : (
<SwitchQuestionsPage question={question} />
)}
</Box>
</Paper>
</>
);
} }
const IconAndrom = (questionType: QuestionType | null) => { const IconAndrom = (questionType: QuestionType | null) => {
switch (questionType) { switch (questionType) {
case "variant": case "variant":
return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "images": case "images":
return ( return <OptionsPict color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
<OptionsPict color="#9A9AAF" sx={{ height: "22px", width: "20px" }} /> case "varimg":
); return <OptionsAndPict color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "varimg": case "emoji":
return ( return <Emoji color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
<OptionsAndPict case "text":
color="#9A9AAF" return <Input color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
sx={{ height: "22px", width: "20px" }} case "select":
/> return <DropDown color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
); case "date":
case "emoji": return <Date color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return <Emoji color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; case "number":
case "text": return <Slider color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return <Input color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; case "file":
case "select": return <Download color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return ( case "page":
<DropDown color="#9A9AAF" sx={{ height: "22px", width: "20px" }} /> return <Page color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
); case "rating":
case "date": return <RatingIcon color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return <Date color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; default:
case "number": return <AnswerGroup color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return <Slider color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; }
case "file":
return (
<Download color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
case "page":
return <Page color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "rating":
return (
<RatingIcon color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
default:
return (
<AnswerGroup color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
);
}
}; };

@ -5,31 +5,25 @@ import { DragDropContext, Droppable } from "react-beautiful-dnd";
import FormDraggableListItem from "./FormDraggableListItem"; import FormDraggableListItem from "./FormDraggableListItem";
import { useQuestions } from "@root/questions/hooks"; import { useQuestions } from "@root/questions/hooks";
export const FormDraggableList = () => { export const FormDraggableList = () => {
const { questions } = useQuestions();
const { questions } = useQuestions() const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index);
};
const onDragEnd = ({ destination, source }: DropResult) => { return (
if (destination) reorderQuestions(source.index, destination.index); <DragDropContext onDragEnd={onDragEnd}>
}; <Droppable droppableId="droppable-list">
{(provided) => (
return ( <Box ref={provided.innerRef} {...provided.droppableProps}>
<DragDropContext onDragEnd={onDragEnd}> {questions?.map((question, index) => (
<Droppable droppableId="droppable-list"> <FormDraggableListItem key={question.id} question={question} questionIndex={index} />
{(provided) => ( ))}
<Box ref={provided.innerRef} {...provided.droppableProps}> {provided.placeholder}
{questions?.map((question, index) => ( </Box>
<FormDraggableListItem )}
key={question.id} </Droppable>
question={question} </DragDropContext>
questionIndex={index} );
/>
))}
{provided.placeholder}
</Box>
)}
</Droppable>
</DragDropContext>
);
}; };

@ -8,106 +8,103 @@ import { FormDraggableList } from "./FormDraggableList";
import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
export default function FormQuestionsPage() { export default function FormQuestionsPage() {
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
if (!quiz) return null; if (!quiz) return null;
return ( return (
<> <>
<Box <Box
sx={{ sx={{
maxWidth: "796px", maxWidth: "796px",
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
margin: "60px 0 40px 0", margin: "60px 0 40px 0",
}} }}
> >
<Typography variant={"h5"}>Заголовок анкеты</Typography> <Typography variant={"h5"}>Заголовок анкеты</Typography>
<Button <Button
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
lineHeight: "19px", lineHeight: "19px",
padding: 0, padding: 0,
textDecoration: "underline", textDecoration: "underline",
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main, textDecorationColor: theme.palette.brightPurple.main,
}} }}
onClick={collapseAllQuestions} onClick={collapseAllQuestions}
> >
Свернуть всё Свернуть всё
</Button> </Button>
</Box> </Box>
<Box <Box
sx={{ sx={{
maxWidth: "796px", maxWidth: "796px",
boxShadow: "0px 10px 30px #e7e7e7", boxShadow: "0px 10px 30px #e7e7e7",
borderRadius: "12px", borderRadius: "12px",
marginBottom: "30px", marginBottom: "30px",
borderTop: "1px solid transparent", borderTop: "1px solid transparent",
borderBottom: "1px solid transparent", borderBottom: "1px solid transparent",
background: "#FFFFFF", background: "#FFFFFF",
}} }}
> >
<FormDraggableList /> <FormDraggableList />
<Box <Box
sx={{ sx={{
cursor: "pointer", cursor: "pointer",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "15px", gap: "15px",
padding: "4px", padding: "4px",
margin: "15px", margin: "15px",
border: "1px solid transparent", border: "1px solid transparent",
borderRadius: "8px", borderRadius: "8px",
"&:hover": { "&:hover": {
border: "1px solid #9A9AAF", border: "1px solid #9A9AAF",
}, },
}} }}
onClick={() => { onClick={() => {
createUntypedQuestion(quiz.backendId); createUntypedQuestion(quiz.backendId);
}} }}
data-cy="create-question" data-cy="create-question"
> >
<AddAnswer color="#EEE4FC" /> <AddAnswer color="#EEE4FC" />
<Typography sx={{ color: "#9A9AAF" }}> <Typography sx={{ color: "#9A9AAF" }}>Добавить еще один вопрос</Typography>
Добавить еще один вопрос </Box>
</Typography> </Box>
</Box> <Box
</Box> sx={{
<Box display: "flex",
sx={{ justifyContent: "flex-end",
display: "flex", gap: "8px",
justifyContent: "flex-end", maxWidth: "796px",
gap: "8px", }}
maxWidth: "796px", >
}} <Button
> variant="outlined"
<Button sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
variant="outlined" onClick={decrementCurrentStep}
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }} >
onClick={decrementCurrentStep} <ArrowLeft />
> </Button>
<ArrowLeft /> <Button
</Button> variant="contained"
<Button sx={{
variant="contained" height: "44px",
sx={{ padding: "10px 20px",
height: "44px", borderRadius: "8px",
padding: "10px 20px", background: theme.palette.brightPurple.main,
borderRadius: "8px", fontSize: "18px",
background: theme.palette.brightPurple.main, }}
fontSize: "18px", onClick={incrementCurrentStep}
}} >
onClick={incrementCurrentStep} Следующий шаг
> </Button>
Следующий шаг </Box>
</Button> {createPortal(<QuizPreview />, document.body)}
</Box> </>
{createPortal(<QuizPreview />, document.body)} );
</>
);
} }

@ -12,80 +12,75 @@ import Slider from "../../../assets/icons/questionsPage/slider";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { createTypedQuestion } from "@root/questions/actions"; import { createTypedQuestion } from "@root/questions/actions";
import type { import type { UntypedQuizQuestion } from "../../../model/questionTypes/shared";
UntypedQuizQuestion
} from "../../../model/questionTypes/shared";
type ButtonTypeQuestion = { type ButtonTypeQuestion = {
icon: JSX.Element; icon: JSX.Element;
title: string; title: string;
value: QuestionType; value: QuestionType;
}; };
const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [ const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [
{ {
icon: <Answer color="#9A9AAF" />, icon: <Answer color="#9A9AAF" />,
title: "Варианты ответов", title: "Варианты ответов",
value: "variant", value: "variant",
}, },
{ {
icon: <Input color="#9A9AAF" />, icon: <Input color="#9A9AAF" />,
title: "Своё поле для ввода", title: "Своё поле для ввода",
value: "text", value: "text",
}, },
{ {
icon: <DropDown color="#9A9AAF" />, icon: <DropDown color="#9A9AAF" />,
title: "Выпадающий список", title: "Выпадающий список",
value: "select", value: "select",
}, },
{ {
icon: <Date color="#9A9AAF" />, icon: <Date color="#9A9AAF" />,
title: "Дата", title: "Дата",
value: "date", value: "date",
}, },
{ {
icon: <Slider color="#9A9AAF" />, icon: <Slider color="#9A9AAF" />,
title: "Ползунок", title: "Ползунок",
value: "number", value: "number",
}, },
{ {
icon: <Download color="#9A9AAF" />, icon: <Download color="#9A9AAF" />,
title: "Загрузка файла", title: "Загрузка файла",
value: "file", value: "file",
}, },
]; ];
interface Props { interface Props {
question: UntypedQuizQuestion; question: UntypedQuizQuestion;
} }
export default function FormTypeQuestions({ question }: Props) { export default function FormTypeQuestions({ question }: Props) {
return (
return ( <Box>
<Box> <Box
<Box sx={{
sx={{ display: "flex",
display: "flex", flexWrap: "wrap",
flexWrap: "wrap", gap: "20px",
gap: "20px", margin: "20px",
margin: "20px", }}
}} >
> {("page" in question && question.page === 0 ? BUTTON_TYPE_QUESTIONS : BUTTON_TYPE_SHORT_QUESTIONS).map(
{(("page" in question) && question.page === 0 ({ icon, title, value: questionType }) => (
? BUTTON_TYPE_QUESTIONS <QuestionsMiniButton
: BUTTON_TYPE_SHORT_QUESTIONS key={title}
).map(({ icon, title, value: questionType }) => ( onClick={() => {
<QuestionsMiniButton createTypedQuestion(question.id, questionType);
key={title} }}
onClick={() => { icon={icon}
createTypedQuestion(question.id, questionType); text={title}
}} />
icon={icon} )
text={title} )}
/> </Box>
))} </Box>
</Box> );
</Box>
);
} }

@ -151,181 +151,181 @@ export default function OptionsAndPicture({ question }: Props) {
onClose={closeCropModal} onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick} onSaveImageClick={handleCropModalSaveClick}
/> />
<Box {/*<Box*/}
sx={{ {/* sx={{*/}
width: "100%", {/* width: "100%",*/}
border: "1px solid #9A9AAF", {/* border: "1px solid #9A9AAF",*/}
borderRadius: "8px", {/* borderRadius: "8px",*/}
display: isTablet ? "block" : "none", {/* display: isTablet ? "block" : "none",*/}
}} {/* }}*/}
> {/*>*/}
<TextField {/* <TextField*/}
fullWidth {/* fullWidth*/}
focused={false} {/* focused={false}*/}
placeholder={"Добавьте ответ"} {/* placeholder={"Добавьте ответ"}*/}
multiline={question.content.largeCheck} {/* multiline={question.content.largeCheck}*/}
InputProps={{ {/* InputProps={{*/}
startAdornment: ( {/* startAdornment: (*/}
<> {/* <>*/}
<InputAdornment position="start"> {/* <InputAdornment position="start">*/}
<PointsIcon {/* <PointsIcon*/}
style={{ color: "#9A9AAF", fontSize: "30px" }} {/* style={{ color: "#9A9AAF", fontSize: "30px" }}*/}
/> {/* />*/}
</InputAdornment> {/* </InputAdornment>*/}
{!isMobile && ( {/* {!isMobile && (*/}
<Box {/* <Box*/}
sx={{ {/* sx={{*/}
width: "60px", {/* width: "60px",*/}
height: "40px", {/* height: "40px",*/}
background: "#EEE4FC", {/* background: "#EEE4FC",*/}
display: "flex", {/* display: "flex",*/}
justifyContent: "space-between", {/* justifyContent: "space-between",*/}
marginRight: "20px", {/* marginRight: "20px",*/}
marginLeft: "12px", {/* marginLeft: "12px",*/}
}} {/* }}*/}
> {/* >*/}
<Box {/* <Box*/}
sx={{ {/* sx={{*/}
display: "flex", {/* display: "flex",*/}
alignItems: "center", {/* alignItems: "center",*/}
justifyContent: "center", {/* justifyContent: "center",*/}
width: "100%", {/* width: "100%",*/}
}} {/* }}*/}
> {/* >*/}
<ImageAddIcons fontSize="22px" color="#7E2AEA" /> {/* <ImageAddIcons fontSize="22px" color="#7E2AEA" />*/}
</Box> {/* </Box>*/}
<span {/* <span*/}
style={{ {/* style={{*/}
display: "flex", {/* display: "flex",*/}
alignItems: "center", {/* alignItems: "center",*/}
justifyContent: "center", {/* justifyContent: "center",*/}
background: "#7E2AEA", {/* background: "#7E2AEA",*/}
height: "100%", {/* height: "100%",*/}
width: "25px", {/* width: "25px",*/}
color: "white", {/* color: "white",*/}
fontSize: "15px", {/* fontSize: "15px",*/}
}} {/* }}*/}
> {/* >*/}
+ {/* +*/}
</span> {/* </span>*/}
</Box> {/* </Box>*/}
)} {/* )}*/}
</> {/* </>*/}
), {/* ),*/}
endAdornment: ( {/* endAdornment: (*/}
<InputAdornment position="end"> {/* <InputAdornment position="end">*/}
<IconButton {/* <IconButton*/}
sx={{ padding: "0" }} {/* sx={{ padding: "0" }}*/}
aria-describedby="my-popover-id" {/* aria-describedby="my-popover-id"*/}
> {/* >*/}
<MessageIcon {/* <MessageIcon*/}
style={{ {/* style={{*/}
color: "#9A9AAF", {/* color: "#9A9AAF",*/}
fontSize: "30px", {/* fontSize: "30px",*/}
marginRight: "6.5px", {/* marginRight: "6.5px",*/}
}} {/* }}*/}
/> {/* />*/}
</IconButton> {/* </IconButton>*/}
<Popover {/* <Popover*/}
id="my-popover-id" {/* id="my-popover-id"*/}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }} {/* anchorOrigin={{ vertical: "bottom", horizontal: "left" }}*/}
open={false} {/* open={false}*/}
> {/* >*/}
<TextareaAutosize {/* <TextareaAutosize*/}
style={{ margin: "10px" }} {/* style={{ margin: "10px" }}*/}
placeholder="Подсказка для этого ответа" {/* placeholder="Подсказка для этого ответа"*/}
/> {/* />*/}
</Popover> {/* </Popover>*/}
<IconButton sx={{ padding: "0" }}> {/* <IconButton sx={{ padding: "0" }}>*/}
<DeleteIcon {/* <DeleteIcon*/}
style={{ {/* style={{*/}
color: theme.palette.grey2.main, {/* color: theme.palette.grey2.main,*/}
marginRight: "-1px", {/* marginRight: "-1px",*/}
}} {/* }}*/}
/> {/* />*/}
</IconButton> {/* </IconButton>*/}
</InputAdornment> {/* </InputAdornment>*/}
), {/* ),*/}
}} {/* }}*/}
sx={{ {/* sx={{*/}
"& .MuiInputBase-root": { {/* "& .MuiInputBase-root": {*/}
padding: "13.5px", {/* padding: "13.5px",*/}
borderRadius: "10px", {/* borderRadius: "10px",*/}
background: "#ffffff", {/* background: "#ffffff",*/}
height: "48px", {/* height: "48px",*/}
}, {/* },*/}
"& .MuiOutlinedInput-notchedOutline": { {/* "& .MuiOutlinedInput-notchedOutline": {*/}
border: "none", {/* border: "none",*/}
}, {/* },*/}
}} {/* }}*/}
inputProps={{ {/* inputProps={{*/}
sx: { fontSize: "18px", lineHeight: "21px", py: 0 }, {/* sx: { fontSize: "18px", lineHeight: "21px", py: 0 },*/}
}} {/* }}*/}
/> {/* />*/}
{isMobile && ( {/*{isMobile && (*/}
<Box {/* <Box*/}
sx={{ {/* sx={{*/}
display: "flex", {/* display: "flex",*/}
alignItems: "center", {/* alignItems: "center",*/}
m: "8px", {/* m: "8px",*/}
position: "relative", {/* position: "relative",*/}
}} {/* }}*/}
> {/* >*/}
<Box {/* <Box*/}
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }} {/* sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}*/}
/> {/* />*/}
<ImageAddIcons {/* <ImageAddIcons*/}
style={{ {/* style={{*/}
position: "absolute", {/* position: "absolute",*/}
color: "#7E2AEA", {/* color: "#7E2AEA",*/}
fontSize: "20px", {/* fontSize: "20px",*/}
left: "45%", {/* left: "45%",*/}
right: "55%", {/* right: "55%",*/}
}} {/* }}*/}
/> {/* />*/}
<Box {/* <Box*/}
sx={{ {/* sx={{*/}
display: "flex", {/* display: "flex",*/}
justifyContent: "center", {/* justifyContent: "center",*/}
alignItems: "center", {/* alignItems: "center",*/}
width: "20px", {/* width: "20px",*/}
background: "#EEE4FC", {/* background: "#EEE4FC",*/}
height: "40px", {/* height: "40px",*/}
color: "white", {/* color: "white",*/}
backgroundColor: "#7E2AEA", {/* backgroundColor: "#7E2AEA",*/}
}} {/* }}*/}
> {/* >*/}
<Box {/* <Box*/}
sx={{ width: "100%", background: "#EEE4FC", height: "40px" }} {/* sx={{ width: "100%", background: "#EEE4FC", height: "40px" }}*/}
/> {/* />*/}
<ImageAddIcons {/* <ImageAddIcons*/}
style={{ {/* style={{*/}
position: "absolute", {/* position: "absolute",*/}
color: "#7E2AEA", {/* color: "#7E2AEA",*/}
fontSize: "20px", {/* fontSize: "20px",*/}
left: "45%", {/* left: "45%",*/}
right: "55%", {/* right: "55%",*/}
}} {/* }}*/}
/> {/* />*/}
<Box {/* <Box*/}
sx={{ {/* sx={{*/}
display: "flex", {/* display: "flex",*/}
justifyContent: "center", {/* justifyContent: "center",*/}
alignItems: "center", {/* alignItems: "center",*/}
width: "20px", {/* width: "20px",*/}
background: "#EEE4FC", {/* background: "#EEE4FC",*/}
height: "40px", {/* height: "40px",*/}
color: "white", {/* color: "white",*/}
backgroundColor: "#7E2AEA", {/* backgroundColor: "#7E2AEA",*/}
}} {/* }}*/}
> {/* >*/}
+ {/* +*/}
</Box> {/* </Box>*/}
</Box> {/* </Box>*/}
</Box> {/* </Box>*/}
)} {/*)}*/}
</Box> {/*</Box>*/}
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",

@ -1,12 +1,5 @@
import { useState, useEffect, useLayoutEffect, useRef } from "react" import { useState, useEffect, useLayoutEffect, useRef } from "react";
import { import { Box, Button, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
Button,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions";
import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions"; import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
@ -14,105 +7,102 @@ import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import AddPlus from "../../assets/icons/questionsPage/addPlus"; import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal" import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal";
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool"; import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions"; import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store"; import { useUiTools } from "@root/uiTools/store";
export default function QuestionsPage() { export default function QuestionsPage() {
const theme = useTheme(); const theme = useTheme();
const { openedModalSettingsId, openBranchingPanel } = useUiTools(); const { openedModalSettingsId, openBranchingPanel } = useUiTools();
const isMobile = false//useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
useLayoutEffect(() => { useLayoutEffect(() => {
updateOpenBranchingPanel(false) updateOpenBranchingPanel(false);
updateEditSomeQuestion() updateEditSomeQuestion();
},[]) }, []);
const ref = useRef() const ref = useRef();
if (!quiz) return null; if (!quiz) return null;
return (
<>
<Box
ref={ref}
id="QuestionsPage"
sx={{
maxWidth: "796px",
width: "100%",
display: "flex",
justifyContent: "space-between",
margin: "60px 0 40px 0",
}}
>
<Typography variant={"h5"}>{quiz.name ? quiz.name : "Заголовок квиза"}</Typography>
<Button
sx={{
display: openBranchingPanel ? "none" : "flex",
fontSize: "16px",
lineHeight: "19px",
padding: 0,
textDecoration: "underline",
color: theme.palette.brightPurple.main,
textDecorationColor: theme.palette.brightPurple.main,
}}
onClick={collapseAllQuestions}
>
Свернуть всё
</Button>
</Box>
<QuestionSwitchWindowTool />
<Box
sx={{
display: "flex",
justifyContent: "space-between",
maxWidth: "796px",
}}
>
<IconButton
onClick={() => {
createUntypedQuestion(quiz.backendId);
}}
sx={{
position: "fixed",
left: isMobile ? "20px" : "250px",
bottom: isMobile ? "140px" : "20px",
}}
data-cy="create-question"
>
<AddPlus />
</IconButton>
return ( <Box sx={{ display: "flex", gap: "8px", marginLeft: "auto" }}>
<> <Button
<Box variant="outlined"
ref={ref} sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
id="QuestionsPage" data-cy="back-button"
sx={{ onClick={decrementCurrentStep}
maxWidth: "796px", >
width: "100%", <ArrowLeft />
display: "flex", </Button>
justifyContent: "space-between", <Button
margin: "60px 0 40px 0", variant="contained"
}} sx={{
> height: "44px",
<Typography variant={"h5"}>{ padding: "10px 20px",
quiz.name ? quiz.name : "Заголовок квиза" }</Typography> borderRadius: "8px",
<Button background: theme.palette.brightPurple.main,
sx={{ fontSize: "18px",
display: openBranchingPanel ? "none" : "flex", }}
fontSize: "16px", onClick={incrementCurrentStep}
lineHeight: "19px", >
padding: 0, Следующий шаг
textDecoration: "underline", </Button>
color: theme.palette.brightPurple.main, </Box>
textDecorationColor: theme.palette.brightPurple.main, </Box>
}} {createPortal(<QuizPreview />, document.body)}
onClick={collapseAllQuestions} {openedModalSettingsId !== null && <BranchingQuestions />}
> </>
Свернуть всё );
</Button>
</Box>
<QuestionSwitchWindowTool/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
maxWidth: "796px",
}}
>
<IconButton
onClick={() => {
createUntypedQuestion(quiz.backendId);
}}
sx={{
position: "fixed",
left: isMobile ? "20px" : "250px",
bottom: "20px",
}}
data-cy="create-question"
>
<AddPlus />
</IconButton>
<Box sx={{ display: "flex", gap: "8px", marginLeft: "auto" }}>
<Button
variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
data-cy="back-button"
onClick={decrementCurrentStep}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
sx={{
height: "44px",
padding: "10px 20px",
borderRadius: "8px",
background: theme.palette.brightPurple.main,
fontSize: "18px",
}}
onClick={incrementCurrentStep}
>
Следующий шаг
</Button>
</Box>
</Box>
{createPortal(<QuizPreview />, document.body)}
{openedModalSettingsId !== null && <BranchingQuestions/>}
</>
);
} }

@ -6,217 +6,216 @@ import SwitchSlider from "./switchSlider";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { updateQuestion } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
interface Props { interface Props {
question: QuizQuestionNumber; question: QuizQuestionNumber;
} }
export default function SliderOptions({ question }: Props) { export default function SliderOptions({ question }: Props) {
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980)); const isTablet = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [switchState, setSwitchState] = useState("setting"); const [switchState, setSwitchState] = useState("setting");
const [stepError, setStepError] = useState(""); const [stepError, setStepError] = useState("");
const SSHC = (data: string) => { const SSHC = (data: string) => {
setSwitchState(data); setSwitchState(data);
}; };
return ( return (
<> <>
<Box <Box
sx={{ sx={{
width: isTablet ? "auto" : "100%", width: isTablet ? "auto" : "100%",
maxWidth: "720.8px", maxWidth: "720.8px",
display: "flex", display: "flex",
pl: "20px", pl: "20px",
pr: isMobile ? "13px" : "20px", pr: isMobile ? "13px" : "20px",
pb: isMobile ? "30px" : "20px", pb: isMobile ? "30px" : "20px",
flexDirection: "column", flexDirection: "column",
gap: isMobile ? "25px" : "20px", gap: isMobile ? "25px" : "20px",
}} }}
>
<Box
sx={{
gap: isMobile ? "10px" : "14px",
mt: isMobile ? "25px" : "0px",
display: "flex",
flexDirection: "column",
marginRight: isMobile ? "10px" : "0px",
}}
>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D" }}>
Выбор значения из диапазона
</Typography>
<Box sx={{ width: "100%", display: "flex", alignItems: "center", gap: isMobile ? "9px" : "20px" }}>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"0"}
min={0}
max={99999999999}
value={question.content.range.split("—")[0]}
onChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.range = `${target.value}${question.content.range.split("—")[1]}`;
});
}}
onBlur={({ target }) => {
const start = question.content.start;
const min = Number(target.value);
const max = Number(question.content.range.split("—")[1]);
if (min >= max) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.range = `${max - 1 >= 0 ? max - 1 : 0}${question.content.range.split("—")[1]}`;
});
}
if (start < min) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.start = min;
});
}
}}
/>
<Typography></Typography>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"100"}
min={0}
max={100000000000}
value={question.content.range.split("—")[1]}
onChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.range = `${question.content.range.split("—")[0]}${target.value}`;
});
}}
onBlur={({ target }) => {
const start = question.content.start;
const step = question.content.step;
const min = Number(question.content.range.split("—")[0]);
const max = Number(target.value);
const range = max - min;
if (max <= min) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.range = `${min}${min + 1 >= 100 ? 100 : min + 1}`;
});
}
if (start > max) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.start = max;
});
}
if (step > max) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.step = min;
});
if (range % step) {
setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
} else {
setStepError("");
}
}
}}
/>
</Box>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexDirection: isMobile ? "column-reverse" : "",
gap: isMobile ? "15px" : "50px",
}}
>
<Box sx={{ width: "100%" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D", mb: isMobile ? "10px" : "14px" }}>
Начальное значение
</Typography>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"50"}
min={Number(question.content.range.split("—")[0])}
max={Number(question.content.range.split("—")[1])}
value={String(question.content.start)}
onChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.start = Number(target.value);
});
}}
/>
</Box>
<Box sx={{ width: "100%" }}>
<Typography
sx={{
fontWeight: "500",
fontSize: "18px",
color: "#4D4D4D",
mb: "10px",
}}
> >
<Box Шаг
sx={{ </Typography>
gap: isMobile ? "10px" : "14px", <CustomNumberField
mt: isMobile ? "25px" : "0px", sx={{ maxWidth: "310px", width: "100%" }}
display: "flex", min={0}
flexDirection: "column", max={100}
marginRight: isMobile ? "10px" : "0px", placeholder={"1"}
}} error={stepError}
> value={String(question.content.step)}
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D" }}> onChange={({ target }) => {
Выбор значения из диапазона updateQuestion(question.id, (question) => {
</Typography> if (question.type !== "number") return;
<Box sx={{ width: "100%", display: "flex", alignItems: "center", gap: isMobile ? "9px" : "20px" }}>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"0"}
min={0}
max={99}
value={question.content.range.split("—")[0]}
onChange={({ target }) => {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.range = `${target.value}${question.content.range.split("—")[1]}`; question.content.step = Number(target.value);
}); });
}} }}
onBlur={({ target }) => { onBlur={({ target }) => {
const start = question.content.start; const min = Number(question.content.range.split("—")[0]);
const min = Number(target.value); const max = Number(question.content.range.split("—")[1]);
const max = Number(question.content.range.split("—")[1]); const range = max - min;
const step = Number(target.value);
if (min >= max) { if (step > max) {
updateQuestion(question.id, question => { updateQuestion(question.id, (question) => {
if (question.type !== "number") return; if (question.type !== "number") return;
question.content.range = `${max - 1 >= 0 ? max - 1 : 0}${question.content.range.split("—")[1]}`; question.content.step = max;
}); });
} }
if (start < min) { if (range % step) {
updateQuestion(question.id, question => { setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
if (question.type !== "number") return; } else {
setStepError("");
question.content.start = min; }
}); }}
} />
}} </Box>
/> </Box>
<Typography></Typography> </Box>
<CustomNumberField <ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
sx={{ maxWidth: "310px", width: "100%" }} <SwitchSlider switchState={switchState} question={question} />
placeholder={"100"} </>
min={0} );
max={100}
value={question.content.range.split("—")[1]}
onChange={({ target }) => {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.range = `${question.content.range.split("—")[0]}${target.value}`;
});
}}
onBlur={({ target }) => {
const start = question.content.start;
const step = question.content.step;
const min = Number(question.content.range.split("—")[0]);
const max = Number(target.value);
const range = max - min;
if (max <= min) {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.range = `${min}${min + 1 >= 100 ? 100 : min + 1}`;
});
}
if (start > max) {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.start = max;
});
}
if (step > max) {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.step = min;
});
if (range % step) {
setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
} else {
setStepError("");
}
}
}}
/>
</Box>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexDirection: isMobile ? "column-reverse" : "",
gap: isMobile ? "15px" : "50px",
}}
>
<Box sx={{ width: "100%" }}>
<Typography sx={{ fontWeight: "500", fontSize: "18px", color: "#4D4D4D", mb: isMobile ? "10px" : "14px" }}>
Начальное значение
</Typography>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
placeholder={"50"}
min={Number(question.content.range.split("—")[0])}
max={Number(question.content.range.split("—")[1])}
value={String(question.content.start)}
onChange={({ target }) => {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.start = Number(target.value);
});
}}
/>
</Box>
<Box sx={{ width: "100%" }}>
<Typography
sx={{
fontWeight: "500",
fontSize: "18px",
color: "#4D4D4D",
mb: "10px",
}}
>
Шаг
</Typography>
<CustomNumberField
sx={{ maxWidth: "310px", width: "100%" }}
min={0}
max={100}
placeholder={"1"}
error={stepError}
value={String(question.content.step)}
onChange={({ target }) => {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.step = Number(target.value);
});
}}
onBlur={({ target }) => {
const min = Number(question.content.range.split("—")[0]);
const max = Number(question.content.range.split("—")[1]);
const range = max - min;
const step = Number(target.value);
if (step > max) {
updateQuestion(question.id, question => {
if (question.type !== "number") return;
question.content.step = max;
});
}
if (range % step) {
setStepError(`Шаг должен делить без остатка диапазон ${max} - ${min} = ${max - min}`);
} else {
setStepError("");
}
}}
/>
</Box>
</Box>
</Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchSlider switchState={switchState} question={question} />
</>
);
} }

@ -12,48 +12,46 @@ import UploadFile from "./UploadFile/UploadFile";
import AnswerOptions from "./answerOptions/AnswerOptions"; import AnswerOptions from "./answerOptions/AnswerOptions";
import { notReachable } from "../../utils/notReachable"; import { notReachable } from "../../utils/notReachable";
interface Props { interface Props {
question: AnyTypedQuizQuestion; question: AnyTypedQuizQuestion;
} }
export default function SwitchQuestionsPage({ question }: Props) { export default function SwitchQuestionsPage({ question }: Props) {
switch (question.type) {
case "variant":
return <AnswerOptions question={question} />;
switch (question.type) { case "images":
case "variant": return <OptionsPicture question={question} />;
return <AnswerOptions question={question} />;
case "images": case "varimg":
return <OptionsPicture question={question} />; return <OptionsAndPicture question={question} />;
case "varimg": case "emoji":
return <OptionsAndPicture question={question} />; return <Emoji question={question} />;
case "emoji": case "text":
return <Emoji question={question} />; return <OwnTextField question={question} />;
case "text": case "select":
return <OwnTextField question={question} />; return <DropDown question={question} />;
case "select": case "date":
return <DropDown question={question} />; return <DataOptions question={question} />;
case "date": case "number":
return <DataOptions question={question} />; return <SliderOptions question={question} />;
case "number": case "file":
return <SliderOptions question={question} />; return <UploadFile question={question} />;
case "file": case "page":
return <UploadFile question={question} />; return <PageOptions question={question} />;
case "page": case "rating":
return <PageOptions question={question} />; return <RatingOptions question={question} />;
case "rating": default:
return <RatingOptions question={question} />; notReachable(question);
}
default:
notReachable(question)
}
} }

@ -1,172 +1,168 @@
import { import { Typography, Box, useTheme, ButtonBase, Modal, TextField, InputAdornment } from "@mui/material";
Typography,
Box,
useTheme,
ButtonBase,
Modal,
TextField,
InputAdornment,
} from "@mui/material";
import UploadIcon from "../../../assets/icons/UploadIcon"; import UploadIcon from "../../../assets/icons/UploadIcon";
import SearchIcon from "../../../assets/icons/SearchIcon"; import SearchIcon from "../../../assets/icons/SearchIcon";
import UnsplashIcon from "../../../assets/icons/Unsplash.svg"; import UnsplashIcon from "../../../assets/icons/Unsplash.svg";
import { useRef, useState, type DragEvent } from "react"; import { useRef, useState, type DragEvent } from "react";
type ImageFormat = "jpg" | "jpeg" | "png" | "gif";
interface ModalkaProps { interface ModalkaProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
handleImageChange: (file: File) => void; handleImageChange: (file: File) => void;
description?: string;
accept?: ImageFormat[];
} }
export const UploadImageModal: React.FC<ModalkaProps> = ({ export const UploadImageModal: React.FC<ModalkaProps> = ({
handleImageChange, handleImageChange,
isOpen, isOpen,
onClose, onClose,
accept,
description,
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const dropZone = useRef<HTMLDivElement>(null); const dropZone = useRef<HTMLDivElement>(null);
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => { const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
setReady(true); setReady(true);
}; };
const handleDrop = (event: DragEvent<HTMLDivElement>) => { const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
if (!file) return; if (!file) return;
handleImageChange(file); handleImageChange(file);
}; };
return ( const acceptedFormats = accept ? accept.map((format) => "." + format).join(", ") : "";
<Modal
open={isOpen} console.log(acceptedFormats);
onClose={onClose}
aria-labelledby="modal-modal-title" return (
aria-describedby="modal-modal-description" <Modal
open={isOpen}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "690px",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: 0,
overflow: "hidden",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "20px",
background: theme.palette.background.default,
}}
> >
<Typography sx={{ marginBottom: "20px", fontWeight: "bold", color: "#4D4D4D" }}>
Добавьте изображение
</Typography>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])}
hidden
accept={acceptedFormats || ".jpg, .jpeg, .png , .gif"}
multiple
type="file"
data-cy="upload-image-input"
/>
<Box <Box
sx={{ onDragOver={(event: DragEvent<HTMLDivElement>) => event.preventDefault()}
position: "absolute", onDrop={handleDrop}
top: "50%", ref={dropZone}
left: "50%", sx={{
transform: "translate(-50%, -50%)", width: "580px",
maxWidth: "690px", padding: "33px 10px 33px 55px",
bgcolor: "background.paper", display: "flex",
borderRadius: "12px", alignItems: "center",
boxShadow: 24, backgroundColor: theme.palette.background.default,
p: 0, border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
overflow: "hidden", borderRadius: "8px",
}} gap: "55px",
}}
onDragEnter={handleDragEnter} // Применяем обработчик onDragEnter напрямую
> >
<Box <UploadIcon />
sx={{ <Box>
display: "flex", <Typography sx={{ color: "#9A9AAF", fontWeight: "bold" }}>
flexDirection: "column", Загрузите или перетяните сюда файл
padding: "20px", </Typography>
background: theme.palette.background.default, <Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
}} {description || "Принимает JPG, PNG, и GIF формат — максимум 5mb"}
> </Typography>
<Typography </Box>
sx={{ marginBottom: "20px", fontWeight: "bold", color: "#4D4D4D" }}
>
Добавьте изображение
</Typography>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])}
hidden
accept="image/*"
multiple
type="file"
data-cy="upload-image-input"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
onDrop={handleDrop}
ref={dropZone}
sx={{
width: "580px",
padding: "33px 10px 33px 55px",
display: "flex",
alignItems: "center",
backgroundColor: theme.palette.background.default,
border: `1px solid ${ready ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
gap: "55px",
}}
onDragEnter={handleDragEnter} // Применяем обработчик onDragEnter напрямую
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: "bold" }}>
Загрузите или перетяните сюда файл
</Typography>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>
Принимает JPG, PNG, и GIF формат максимум 5mb
</Typography>
</Box>
</Box>
</ButtonBase>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "20px 0",
}}
>
<Typography
sx={{
fontWeight: "bold",
color: "#4D4D4D",
}}
>
Или выберите на фотостоке
</Typography>
<img src={UnsplashIcon} alt="" />
</Box>
<TextField
id="search-in-unsplash"
placeholder="Ищите изображения на английском языка"
sx={{
"& .MuiInputBase-input": {
height: "48px",
padding: "0 10px 0 0",
},
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: "8px",
},
"& .Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "1px solid rgba(0, 0, 0, 0.23)",
},
"& .MuiInputBase-root.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline":
{
borderColor: "rgba(0, 0, 0, 0.23)",
},
}}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
sx={{
outline: "none",
"& svg > path": { stroke: "#9A9AAF" },
}}
>
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
</Box> </Box>
</Modal> </ButtonBase>
); <Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "20px 0",
}}
>
<Typography
sx={{
fontWeight: "bold",
color: "#4D4D4D",
}}
>
Или выберите на фотостоке
</Typography>
<img src={UnsplashIcon} alt="" />
</Box>
<TextField
id="search-in-unsplash"
placeholder="Ищите изображения на английском языка"
sx={{
"& .MuiInputBase-input": {
height: "48px",
padding: "0 10px 0 0",
},
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: "8px",
},
"& .Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "1px solid rgba(0, 0, 0, 0.23)",
},
"& .MuiInputBase-root.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(0, 0, 0, 0.23)",
},
}}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
sx={{
outline: "none",
"& svg > path": { stroke: "#9A9AAF" },
}}
>
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
</Box>
</Modal>
);
}; };

@ -1,5 +1,4 @@
import { ResultSettings } from "./ResultSettings";
import { ResultSettings } from "./ResultSettings"
import { createFrontResult } from "@root/questions/actions"; import { createFrontResult } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
@ -7,6 +6,8 @@ import { Box, Typography, useTheme, useMediaQuery, Button } from "@mui/material"
import image from "../../assets/Rectangle 110.png"; import image from "../../assets/Rectangle 110.png";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import { decrementCurrentStep } from "@root/quizes/actions";
export const FirstEntry = () => { export const FirstEntry = () => {
const theme = useTheme(); const theme = useTheme();
@ -17,16 +18,20 @@ export const FirstEntry = () => {
const create = () => { const create = () => {
if (quiz?.config.haveRoot) { if (quiz?.config.haveRoot) {
questions questions
.filter((question:AnyTypedQuizQuestion) => { .filter((question: AnyTypedQuizQuestion) => {
return question.type !== null && question.content.rule.parentId.length !== 0 && question.content.rule.children.length === 0 return (
}) question.type !== null &&
.forEach(question => { question.content.rule.parentId.length !== 0 &&
createFrontResult(quiz.id, question.content.id) question.content.rule.children.length === 0
}) );
})
.forEach((question) => {
createFrontResult(quiz.backendId, question.content.id);
});
} else { } else {
createFrontResult(quiz.id, "line") createFrontResult(quiz.backendId, "line");
} }
} };
return ( return (
<> <>
@ -53,7 +58,7 @@ export const FirstEntry = () => {
mr: !isSmallMonitor ? "104px" : 0, mr: !isSmallMonitor ? "104px" : 0,
marginBottom: isSmallMonitor ? "20px" : 0, marginBottom: isSmallMonitor ? "20px" : 0,
position: "relative", position: "relative",
height: "100%" height: "100%",
}} }}
> >
<Typography variant="h5" sx={{ marginBottom: "20px" }}> <Typography variant="h5" sx={{ marginBottom: "20px" }}>
@ -69,7 +74,10 @@ export const FirstEntry = () => {
}} }}
> >
<Typography sx={{ color: "#4D4D4D", width: "95%" }}> <Typography sx={{ color: "#4D4D4D", width: "95%" }}>
Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке. Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным
пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально
индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте
пользователя по нужной ссылке.
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
@ -91,21 +99,35 @@ export const FirstEntry = () => {
}} }}
/> />
</Box> </Box>
<Button
onClick={create} <Box sx={{ display: "flex", justifyContent: "flex-start", alignItems: "center", gap: "8px", mt: "30px" }}>
variant="contained" <Button
sx={{ variant="outlined"
backgroundColor: "#7E2AEA", sx={{
fontSize: "18px", padding: "10px 20px",
lineHeight: "18px", borderRadius: "8px",
width: "216px", height: "44px",
height: "44px", }}
mt: "30px", onClick={decrementCurrentStep}
p: "10px 20px" >
}} <ArrowLeft />
> </Button>
Создать результаты <Button
</Button> onClick={create}
variant="contained"
sx={{
backgroundColor: "#7E2AEA",
fontSize: "18px",
lineHeight: "18px",
width: "216px",
height: "44px",
p: "10px 20px",
}}
>
Создать результаты
</Button>
</Box>
</> </>
); );
} };

@ -0,0 +1,33 @@
import { Box, Typography, Button } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
type ContactFormProps = {
showResultForm: boolean;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
};
export const ContactForm = ({
showResultForm,
setShowContactForm,
setShowResultForm,
}: ContactFormProps) => {
const quiz = useCurrentQuiz();
const followNextForm = () => {
setShowContactForm(false);
setShowResultForm(true);
};
return (
<Box>
<Typography>Форма контактов</Typography>
{!showResultForm && quiz?.config.resultInfo.when === "after" && (
<Button variant="contained" onClick={followNextForm}>
Показать результат
</Button>
)}
</Box>
);
};

@ -2,6 +2,8 @@ import { useState, useEffect } from "react";
import { Box, Button, useTheme } from "@mui/material"; import { Box, Button, useTheme } from "@mui/material";
import { useQuizViewStore } from "@root/quizView"; import { useQuizViewStore } from "@root/quizView";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
import type { import type {
AnyTypedQuizQuestion, AnyTypedQuizQuestion,
@ -11,20 +13,26 @@ import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
type FooterProps = { type FooterProps = {
questions: AnyTypedQuizQuestion[];
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void; setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
question: AnyTypedQuizQuestion; question: AnyTypedQuizQuestion;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
setResultQuestion: (id: string) => void;
}; };
export const Footer = ({ export const Footer = ({
setCurrentQuestion, setCurrentQuestion,
questions,
question, question,
setShowContactForm,
setShowResultForm,
setResultQuestion,
}: FooterProps) => { }: FooterProps) => {
const [disablePreviousButton, setDisablePreviousButton] = const [disablePreviousButton, setDisablePreviousButton] =
useState<boolean>(false); useState<boolean>(false);
const [disableNextButton, setDisableNextButton] = useState<boolean>(false); const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const quiz = useCurrentQuiz();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const questions = useQuestionsStore().questions as AnyTypedQuizQuestion[];
const theme = useTheme(); const theme = useTheme();
const linear = !questions.find( const linear = !questions.find(
({ content }) => content.rule.parentId === "root" ({ content }) => content.rule.parentId === "root"
@ -72,32 +80,42 @@ export const Footer = ({
} }
if (linear) { if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id); return;
}
const nextQuestion = questions[questionIndex + 1]; const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
if (nextQuestion) { setDisableNextButton(false);
setDisableNextButton(false);
} else {
setDisableNextButton(true);
}
} else { } else {
const nextQuestionId = getNextQuestionId(); const nextQuestion = getQuestionByContentId(
if (nextQuestionId) { question.content.rule.default
);
if (nextQuestion?.type) {
setDisableNextButton(false); setDisableNextButton(false);
} else {
const nextQuestion = getQuestionByContentId(
question.content.rule.default
);
if (nextQuestion?.type) {
setDisableNextButton(false);
} else {
setDisableNextButton(true);
}
} }
} }
}, [question, answers]); }, [question, answers]);
const showResult = () => {
const resultQuestion = questions.find(
({ type, content }) =>
type === "result" && content.rule.parentId === question.content.id
);
if (resultQuestion) {
setResultQuestion(resultQuestion.id);
return;
}
if (quiz?.config.resultInfo.when === "after") {
setShowContactForm(true);
} else {
setShowResultForm(true);
}
};
const getNextQuestionId = () => { const getNextQuestionId = () => {
if (answers.length) { if (answers.length) {
const answer = answers.find( const answer = answers.find(
@ -171,11 +189,12 @@ export const Footer = ({
const followNextStep = () => { const followNextStep = () => {
if (linear) { if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id); const questionIndex = questions.findIndex(({ id }) => id === question.id);
const nextQuestion = questions[questionIndex + 1]; const nextQuestion = questions[questionIndex + 1];
if (nextQuestion) { if (nextQuestion && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion); setCurrentQuestion(nextQuestion);
} else {
showResult();
} }
return; return;
@ -186,7 +205,7 @@ export const Footer = ({
if (nextQuestionId) { if (nextQuestionId) {
const nextQuestion = getQuestionByContentId(nextQuestionId); const nextQuestion = getQuestionByContentId(nextQuestionId);
if (nextQuestion?.type) { if (nextQuestion?.type && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion); setCurrentQuestion(nextQuestion);
return; return;
} else { } else {
@ -196,10 +215,10 @@ export const Footer = ({
const nextQuestion = getQuestionByContentId( const nextQuestion = getQuestionByContentId(
question.content.rule.default question.content.rule.default
); );
if (nextQuestion?.type) { if (nextQuestion?.type && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion); setCurrentQuestion(nextQuestion);
} else { } else {
enqueueSnackbar("не могу получить последующий вопрос (дефолтный)"); showResult();
} }
} }
}; };

@ -1,5 +1,9 @@
import { useState, useEffect } from "react";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { getQuestionByContentId } from "@root/questions/actions";
import { Variant } from "./questions/Variant"; import { Variant } from "./questions/Variant";
import { Images } from "./questions/Images"; import { Images } from "./questions/Images";
import { Varimg } from "./questions/Varimg"; import { Varimg } from "./questions/Varimg";
@ -12,12 +16,12 @@ import { File } from "./questions/File";
import { Page } from "./questions/Page"; import { Page } from "./questions/Page";
import { Rating } from "./questions/Rating"; import { Rating } from "./questions/Rating";
import { Footer } from "./Footer"; import { Footer } from "./Footer";
import { ContactForm } from "./ContactForm";
import { ResultForm } from "./ResultForm";
import { ResultQuestion } from "./ResultQuestion";
import { useState, type FC, useEffect } from "react";
import type { QuestionType } from "../../model/question/question"; import type { QuestionType } from "../../model/question/question";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { getQuestionByContentId } from "@root/questions/actions";
type QuestionProps = { type QuestionProps = {
questions: AnyTypedQuizQuestion[]; questions: AnyTypedQuizQuestion[];
@ -41,6 +45,9 @@ export const Question = ({ questions }: QuestionProps) => {
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [currentQuestion, setCurrentQuestion] = const [currentQuestion, setCurrentQuestion] =
useState<AnyTypedQuizQuestion>(); useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false);
const [resultQuestion, setResultQuestion] = useState<string>("");
useEffect(() => { useEffect(() => {
const nextQuestion = getQuestionByContentId(quiz?.config.haveRoot || ""); const nextQuestion = getQuestionByContentId(quiz?.config.haveRoot || "");
@ -70,13 +77,41 @@ export const Question = ({ questions }: QuestionProps) => {
margin: "0 auto", margin: "0 auto",
}} }}
> >
<QuestionComponent currentQuestion={currentQuestion} /> {!showContactForm && !showResultForm && !resultQuestion && (
<QuestionComponent currentQuestion={currentQuestion} />
)}
{resultQuestion && (
<ResultQuestion
resultQuestion={resultQuestion}
setResultQuestion={setResultQuestion}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showContactForm && (
<ContactForm
showResultForm={showResultForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showResultForm && (
<ResultForm
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
</Box> </Box>
<Footer {!showContactForm && !showResultForm && !resultQuestion && (
questions={questions} <Footer
question={currentQuestion} question={currentQuestion}
setCurrentQuestion={setCurrentQuestion} setCurrentQuestion={setCurrentQuestion}
/> setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
setResultQuestion={setResultQuestion}
/>
)}
</Box> </Box>
); );
}; };

@ -0,0 +1,33 @@
import { Box, Typography, Button } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
type ResultFormProps = {
showContactForm: boolean;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
};
export const ResultForm = ({
showContactForm,
setShowContactForm,
setShowResultForm,
}: ResultFormProps) => {
const quiz = useCurrentQuiz();
const followNextForm = () => {
setShowResultForm(false);
setShowContactForm(true);
};
return (
<Box>
<Typography>Форма результатов</Typography>
{!showContactForm && quiz?.config.resultInfo.when !== "after" && (
<Button variant="contained" onClick={followNextForm}>
Показать форму контактов
</Button>
)}
</Box>
);
};

@ -0,0 +1,43 @@
import { Box, Typography, Button } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
type ResultQuestionProps = {
resultQuestion: string;
setResultQuestion: (id: string) => void;
setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void;
};
export const ResultQuestion = ({
resultQuestion,
setResultQuestion,
setShowContactForm,
setShowResultForm,
}: ResultQuestionProps) => {
const quiz = useCurrentQuiz();
const { questions } = useQuestionsStore();
const followNextForm = () => {
setResultQuestion("");
if (quiz?.config.resultInfo.when === "after") {
setShowContactForm(true);
} else {
setShowResultForm(true);
}
};
return (
<Box>
<Typography>Вопрос результат</Typography>
<Typography>
{JSON.stringify(questions.find(({ id }) => id === resultQuestion))}
</Typography>
<Button variant="contained" onClick={followNextForm}>
Далее
</Button>
</Box>
);
};

@ -25,18 +25,17 @@ export const ViewPage = () => {
useEffect(() => { useEffect(() => {
const getData = async () => { const getData = async () => {
const quizes = await quizApi.getList() const quizes = await quizApi.getList();
setQuizes(quizes) setQuizes(quizes);
const questions = await questionApi.getList({ quiz_id: editQuizId }) const questions = await questionApi.getList({ quiz_id: editQuizId });
setQuestions(questions) setQuestions(questions);
} };
getData() getData();
}, []) }, []);
useEffect(() => { useEffect(() => {
setVisualStartPage(quiz?.config.noStartPage) setVisualStartPage(quiz?.config.noStartPage);
}, [questions]) }, [questions]);
const [visualStartPage, setVisualStartPage] = useState<boolean>(); const [visualStartPage, setVisualStartPage] = useState<boolean>();
@ -52,8 +51,8 @@ export const ViewPage = () => {
questions.filter(({ type }) => type) as AnyTypedQuizQuestion[] questions.filter(({ type }) => type) as AnyTypedQuizQuestion[]
).sort((previousItem, item) => previousItem.page - item.page); ).sort((previousItem, item) => previousItem.page - item.page);
console.log("visualStartPage ", visualStartPage) console.log("visualStartPage ", visualStartPage);
if (visualStartPage === undefined) return <></> if (visualStartPage === undefined) return <></>;
return ( return (
<Box> <Box>
{!visualStartPage ? ( {!visualStartPage ? (

@ -15,32 +15,24 @@ type NumberProps = {
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const [minRange, setMinRange] = useState<string>("0"); const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100"); const [maxRange, setMaxRange] = useState<string>("100000000000");
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const updateMinRangeDebounced = useDebouncedCallback( const updateMinRangeDebounced = useDebouncedCallback((value, crowded = false) => {
(value, crowded = false) => { if (crowded) {
if (crowded) { setMinRange(maxRange);
setMinRange(maxRange); }
}
updateAnswer(currentQuestion.content.id, value); updateAnswer(currentQuestion.content.id, value);
}, }, 1000);
1000 const updateMaxRangeDebounced = useDebouncedCallback((value, crowded = false) => {
); if (crowded) {
const updateMaxRangeDebounced = useDebouncedCallback( setMaxRange(minRange);
(value, crowded = false) => { }
if (crowded) {
setMaxRange(minRange);
}
updateAnswer(currentQuestion.content.id, value); updateAnswer(currentQuestion.content.id, value);
}, }, 1000);
1000 const answer = answers.find(({ questionId }) => questionId === currentQuestion.content.id)?.answer as string;
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
)?.answer as string;
const min = window.Number(currentQuestion.content.range.split("—")[0]); const min = window.Number(currentQuestion.content.range.split("—")[0]);
const max = window.Number(currentQuestion.content.range.split("—")[1]); const max = window.Number(currentQuestion.content.range.split("—")[1]);

@ -1,12 +1,4 @@
import { import { Box, Button, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
Button,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { createQuiz } from "@root/quizes/actions"; import { createQuiz } from "@root/quizes/actions";
import { useQuizes } from "@root/quizes/hooks"; import { useQuizes } from "@root/quizes/hooks";
import SectionWrapper from "@ui_kit/SectionWrapper"; import SectionWrapper from "@ui_kit/SectionWrapper";
@ -16,72 +8,62 @@ import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz"; import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard"; import QuizCard from "./QuizCard";
interface Props { interface Props {
outerContainerSx?: SxProps<Theme>; outerContainerSx?: SxProps<Theme>;
children?: React.ReactNode; children?: React.ReactNode;
} }
export default function MyQuizzesFull({ export default function MyQuizzesFull({ outerContainerSx: sx, children }: Props) {
outerContainerSx: sx, const { quizes } = useQuizes();
children, const navigate = useNavigate();
}: Props) { const theme = useTheme();
const { quizes } = useQuizes(); const isMobile = useMediaQuery(theme.breakpoints.down(500));
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500));
return ( return (
<> <>
{quizes.length === 0 ? ( {quizes.length === 0 ? (
<FirstQuiz /> <FirstQuiz />
) : ( ) : (
<SectionWrapper maxWidth="lg"> <SectionWrapper maxWidth="lg">
<ComplexNavText text1="Кабинет квизов" /> <ComplexNavText text1="Кабинет квизов" />
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
mt: "20px", mt: "20px",
mb: "30px", mb: "30px",
}} }}
> >
<Typography variant="h4">Мои квизы</Typography> <Typography variant="h4">Мои квизы</Typography>
<Button <Button
variant="contained" variant="contained"
sx={{ sx={{
padding: isMobile ? "10px" : "10px 47px", padding: isMobile ? "10px" : "10px 47px",
minWidth: "44px", minWidth: "44px",
}} }}
onClick={() => createQuiz(navigate)} onClick={() => createQuiz(navigate)}
data-cy="create-quiz" data-cy="create-quiz"
> >
{isMobile ? "+" : "Создать +"} {isMobile ? "+" : "Создать +"}
</Button> </Button>
</Box> </Box>
<Box <Box
sx={{ sx={{
py: "10px", py: "10px",
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
gap: "40px", gap: "40px",
mb: "60px", mb: "60px",
}} }}
> >
{quizes.map(quiz => ( {quizes.map((quiz) => (
<QuizCard <QuizCard key={quiz.id} quiz={quiz} openCount={0} applicationCount={0} conversionPercent={0} />
key={quiz.id} ))}
quiz={quiz} </Box>
openCount={0} {children}
applicationCount={0} </SectionWrapper>
conversionPercent={0} )}
/> </>
))} );
</Box>
{children}
</SectionWrapper>
)}
</>
);
} }

@ -29,10 +29,9 @@ import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import useSWR from "swr"; import useSWR from "swr";
import { SidebarMobile } from "./Sidebar/SidebarMobile"; import { SidebarMobile } from "./Sidebar/SidebarMobile";
import { cleanQuestions } from "@root/questions/actions"; import { cleanQuestions, setQuestions } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions"; import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import { BranchingPanel } from "../Questions/BranchingPanel"; import { BranchingPanel } from "../Questions/BranchingPanel";
import { setQuestions } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { useQuizes } from "@root/quizes/hooks"; import { useQuizes } from "@root/quizes/hooks";
import { questionApi } from "@api/question"; import { questionApi } from "@api/question";
@ -52,6 +51,7 @@ export default function EditPage() {
const questions = await questionApi.getList({ quiz_id: editQuizId }) const questions = await questionApi.getList({ quiz_id: editQuizId })
setQuestions(questions) setQuestions(questions)
} }
getData() getData()
}, []) }, [])
@ -245,7 +245,7 @@ export default function EditPage() {
left: 0, left: 0,
bottom: 0, bottom: 0,
width: "100%", width: "100%",
padding: "20px 40px", padding: isMobile ? "20px 16px" : "20px 40px",
display: "flex", display: "flex",
justifyContent: "flex-end", justifyContent: "flex-end",
gap: "15px", gap: "15px",
@ -320,16 +320,20 @@ export default function EditPage() {
</Box> </Box>
</Box> </Box>
)} )}
<Button <a href={`/view`} target="_blank" rel="noreferrer" style={{ textDecoration: "none" }}>
variant="contained" <Button
sx={{ variant="contained"
fontSize: "14px", sx={{
lineHeight: "18px", fontSize: "14px",
height: "34px", lineHeight: "18px",
}} height: "34px",
> minWidth: "130px"
Опубликовать }}
</Button> >
Опубликовать
</Button>
</a>
</Box> </Box>
} }

@ -6,120 +6,126 @@ import { useState } from "react";
import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal"; import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../../utils/useDisclosure"; import { useDisclosure } from "../../utils/useDisclosure";
const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"]; const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
interface Props { interface Props {
imageUrl: string | null; imageUrl: string | null;
onImageUploadClick: (image: Blob) => void; onImageUploadClick: (image: Blob) => void;
onDeleteClick: () => void; onDeleteClick: () => void;
} }
export default function FaviconDropZone({ imageUrl, onImageUploadClick, onDeleteClick }: Props) { export default function FaviconDropZone({ imageUrl, onImageUploadClick, onDeleteClick }: Props) {
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [isDropReady, setIsDropReady] = useState<boolean>(false); const [isDropReady, setIsDropReady] = useState<boolean>(false);
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure(); const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
if (!quiz) return null; if (!quiz) return null;
async function handleImageUpload(file: File) { async function handleImageUpload(file: File) {
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик"); if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
if (!allowedFileTypes.includes(file.type)) return enqueueSnackbar("Допустимые форматы изображений: png, jpeg, gif"); if (!allowedFileTypes.includes(file.type)) return enqueueSnackbar("Допустимые форматы изображений: png, jpeg, gif");
onImageUploadClick(file); onImageUploadClick(file);
closeImageUploadModal(); closeImageUploadModal();
} }
const onDrop = (event: React.DragEvent<HTMLDivElement>) => { const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
setIsDropReady(false); setIsDropReady(false);
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
if (!file || imageUrl) return; if (!file || imageUrl) return;
handleImageUpload(file); handleImageUpload(file);
}; };
return ( return (
<Box sx={{ <Box
sx={{
display: "flex",
gap: "10px",
}}
>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
description="Принимает JPG, PNG — максимум 5mb"
accept={["jpeg", "jpg", "png"]}
/>
<Box
onDragEnter={() => !imageUrl && setIsDropReady(true)}
onDragExit={() => setIsDropReady(false)}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
sx={{
width: "48px",
height: "48px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${isDropReady ? "red" : theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
<ButtonBase
onClick={imageUrl ? undefined : openImageUploadModal}
sx={{
width: "100%",
height: "100%",
display: "flex", display: "flex",
gap: "10px", justifyContent: "center",
}}> alignItems: "center",
<UploadImageModal borderRadius: "8px",
isOpen={isImageUploadOpen} overflow: "hidden",
onClose={closeImageUploadModal} }}
handleImageChange={handleImageUpload} >
{imageUrl ? (
<img
src={imageUrl}
style={{
width: "100%",
height: "100%",
objectFit: "scale-down",
}}
/> />
<Box ) : (
onDragEnter={() => !imageUrl && setIsDropReady(true)} <UploadIcon />
onDragExit={() => setIsDropReady(false)} )}
onDragOver={e => e.preventDefault()} </ButtonBase>
onDrop={onDrop} </Box>
sx={{ <Box
width: "48px", sx={{
height: "48px", display: "flex",
backgroundColor: theme.palette.background.default, flexDirection: "column",
border: `1px solid ${isDropReady ? "red" : theme.palette.grey2.main}`, alignItems: "start",
borderRadius: "8px", }}
}}> >
<ButtonBase {imageUrl && (
onClick={imageUrl ? undefined : openImageUploadModal} <ButtonBase onClick={onDeleteClick}>
sx={{ <Typography
width: "100%", sx={{
height: "100%", color: theme.palette.orange.main,
display: "flex", fontSize: "16px",
justifyContent: "center", lineHeight: "19px",
alignItems: "center", textDecoration: "underline",
borderRadius: "8px", }}
overflow: "hidden", >
}} Удалить
> </Typography>
{imageUrl ? </ButtonBase>
<img )}
src={imageUrl} <Typography
style={{ sx={{
width: "100%", color: theme.palette.orange.main,
height: "100%", fontSize: "16px",
objectFit: "scale-down", lineHeight: "19px",
}} textDecoration: "underline",
/> mt: "auto",
: }}
<UploadIcon /> >
} 5 MB максимум
</ButtonBase> </Typography>
</Box> </Box>
<Box sx={{ </Box>
display: "flex", );
flexDirection: "column", }
alignItems: "start",
}}>
{imageUrl &&
<ButtonBase onClick={onDeleteClick}>
<Typography
sx={{
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
}}
>
Удалить
</Typography>
</ButtonBase>
}
<Typography
sx={{
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
mt: "auto",
}}
>
5 MB максимум
</Typography>
</Box>
</Box>
);
};

@ -2,6 +2,7 @@ import AlignCenterIcon from "@icons/AlignCenterIcon";
import AlignLeftIcon from "@icons/AlignLeftIcon"; import AlignLeftIcon from "@icons/AlignLeftIcon";
import AlignRightIcon from "@icons/AlignRightIcon"; import AlignRightIcon from "@icons/AlignRightIcon";
import ArrowDown from "@icons/ArrowDownIcon"; import ArrowDown from "@icons/ArrowDownIcon";
import ArrowLeft from "@icons/ArrowLeftSP";
import LayoutCenteredIcon from "@icons/LayoutCenteredIcon"; import LayoutCenteredIcon from "@icons/LayoutCenteredIcon";
import LayoutExpandedIcon from "@icons/LayoutExpandedIcon"; import LayoutExpandedIcon from "@icons/LayoutExpandedIcon";
import LayoutStandartIcon from "@icons/LayoutStandartIcon"; import LayoutStandartIcon from "@icons/LayoutStandartIcon";
@ -652,7 +653,7 @@ export default function StartPageSettings() {
<Button <Button
onClick={() => setFormState("design")} onClick={() => setFormState("design")}
sx={{ sx={{
display: "block", display: "flex",
marginTop: "20px", marginTop: "20px",
padding: "0", padding: "0",
fontWeight: "bold", fontWeight: "bold",
@ -662,14 +663,14 @@ export default function StartPageSettings() {
textUnderlineOffset: "2px", textUnderlineOffset: "2px",
}} }}
> >
🡰 Вернуться к дизайну <ArrowLeft right={false}/> Вернуться к дизайну
</Button> </Button>
)} )}
{formState === "design" && ( {formState === "design" && (
<Button <Button
onClick={() => setFormState("content")} onClick={() => setFormState("content")}
sx={{ sx={{
display: "block", display: "flex",
marginLeft: "auto", marginLeft: "auto",
marginTop: "20px", marginTop: "20px",
padding: "0", padding: "0",
@ -680,7 +681,7 @@ export default function StartPageSettings() {
textUnderlineOffset: "2px", textUnderlineOffset: "2px",
}} }}
> >
Далее заполнить контент 🡲 Далее заполнить контент <ArrowLeft right={true}/>
</Button> </Button>
)} )}
</Box> </Box>

@ -297,6 +297,7 @@ export const uploadQuestionImage = async (
if (!question || !quizQid) return; if (!question || !quizQid) return;
try { try {
console.log(question.quizId)
const response = await quizApi.addImages(question.quizId, blob); const response = await quizApi.addImages(question.quizId, blob);
const values = Object.values(response); const values = Object.values(response);
@ -553,3 +554,4 @@ export const createBackResult = async (
enqueueSnackbar("Не удалось создать вопрос"); enqueueSnackbar("Не удалось создать вопрос");
} }
}); });

@ -1,74 +1,77 @@
import {Slider, useTheme} from "@mui/material"; import { Slider, useTheme } from "@mui/material";
type CustomSliderProps = { type CustomSliderProps = {
defaultValue?: number; defaultValue?: number;
value?: number | number[]; value?: number | number[];
min?: number; min?: number;
max?: number; max?: number;
step?: number; step?: number;
onChange?: (_: Event, value: number | number[]) => void; onChange?: (_: Event, value: number | number[]) => void;
onChangeCommitted?: (_: React.SyntheticEvent | Event, value: number | number[]) => void; onChangeCommitted?: (_: React.SyntheticEvent | Event, value: number | number[]) => void;
}; };
export const CustomSlider = ({ export const CustomSlider = ({
defaultValue, defaultValue,
value, value,
min = 0, min = 0,
max = 100, max = 100,
step, step,
onChange, onChange,
onChangeCommitted onChangeCommitted,
}: CustomSliderProps) => { }: CustomSliderProps) => {
// const handleChange = ({ type }: Event, newValue: number | number[]) => { // const handleChange = ({ type }: Event, newValue: number | number[]) => {
// // Для корректной работы слайдера в FireFox // // Для корректной работы слайдера в FireFox
// if (type !== "change") { // if (type !== "change") {
// onChange?.(e, newValue); // onChange?.(e, newValue);
// } // }
// }; // };
const theme = useTheme(); const theme = useTheme();
return ( return (
<Slider <Slider
value={value} value={value}
defaultValue={defaultValue} defaultValue={defaultValue}
min={min} min={min}
max={max} max={max}
step={step} step={step}
onChange={onChange} onChange={onChange}
valueLabelDisplay="on" valueLabelDisplay="on"
onChangeCommitted={onChangeCommitted} onChangeCommitted={onChangeCommitted}
onMouseDown={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}
data-cy="slider" data-cy="slider"
sx={{ sx={{
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
padding: "0", padding: "0",
marginTop: "75px", marginTop: "75px",
"& .MuiSlider-valueLabel":{ "& .MuiSlider-valueLabel": {
background: theme.palette.brightPurple.main, background: theme.palette.brightPurple.main,
borderRadius: "8px", borderRadius: "8px",
width: "60px", minWidth: "60px",
height: "36px" width: "auto",
}, whiteSpace: "nowrap",
"& .MuiSlider-valueLabel::before": { textAlign: "center",
width: "6px", height: "36px",
height: "2px", },
transform: "translate(-50%, 50%) rotate(90deg)", "& .MuiSlider-valueLabel::before": {
bottom: "-5px" width: "6px",
}, height: "2px",
"& .MuiSlider-rail": { transform: "translate(-50%, 50%) rotate(90deg)",
backgroundColor: "#F2F3F7", bottom: "-5px",
border: `1px solid #9A9AAF`, },
height: "12px" "& .MuiSlider-rail": {
}, backgroundColor: "#F2F3F7",
"& .MuiSlider-thumb": { border: `1px solid #9A9AAF`,
border: "3px #f2f3f7 solid", height: "12px",
height: "23px", },
width: "23px" "& .MuiSlider-thumb": {
}, border: "3px #f2f3f7 solid",
"& .MuiSlider-track": { height: "23px",
height: "12px" width: "23px",
} },
}} "& .MuiSlider-track": {
/> height: "12px",
); },
}}
/>
);
}; };

@ -1,10 +1,4 @@
import { import { FormControl, TextField, useTheme, SxProps, Theme } from "@mui/material";
FormControl,
TextField,
useTheme,
SxProps,
Theme,
} from "@mui/material";
import type { ChangeEvent, KeyboardEvent, FocusEvent } from "react"; import type { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
import type { InputProps } from "@mui/material"; import type { InputProps } from "@mui/material";

@ -5,6 +5,7 @@ import { decrementCurrentStep } from "@root/quizes/actions";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
import CustomAvatar from "./Avatar"; import CustomAvatar from "./Avatar";
import NavMenuItem from "./NavMenuItem"; import NavMenuItem from "./NavMenuItem";
import { Link } from "react-router-dom";
export default function Header() { export default function Header() {
const theme = useTheme(); const theme = useTheme();
@ -25,7 +26,9 @@ export default function Header() {
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
}} }}
> >
<PenaLogo width={124} /> <Link to="/">
<PenaLogo width={124} />
</Link>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",

@ -1,11 +1,4 @@
import { import { Box, Container, IconButton, Typography, useTheme, useMediaQuery } from "@mui/material";
Box,
Container,
IconButton,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
import NavMenuItem from "./NavMenuItem"; import NavMenuItem from "./NavMenuItem";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
import WalletIcon from "@icons/WalletIcon"; import WalletIcon from "@icons/WalletIcon";
@ -13,7 +6,7 @@ import CustomAvatar from "./Avatar";
import { Burger } from "@icons/Burger"; import { Burger } from "@icons/Burger";
import { clearAuthToken } from "@frontend/kitui"; import { clearAuthToken } from "@frontend/kitui";
import { logout } from "@api/auth"; import { logout } from "@api/auth";
import { useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { clearUserData } from "@root/user"; import { clearUserData } from "@root/user";
import { LogoutButton } from "@ui_kit/LogoutButton"; import { LogoutButton } from "@ui_kit/LogoutButton";
@ -59,7 +52,9 @@ export default function HeaderFull() {
style={{ fontSize: "30px", color: "#000000", cursor: "pointer" }} style={{ fontSize: "30px", color: "#000000", cursor: "pointer" }}
/> />
)} )}
<PenaLogo width={124} /> <Link to="/">
<PenaLogo width={124} />
</Link>
{!isTablet && ( {!isTablet && (
<Box <Box
sx={{ sx={{
@ -93,10 +88,7 @@ export default function HeaderFull() {
> >
Мой баланс Мой баланс
</Typography> </Typography>
<Typography <Typography variant="body2" color={theme.palette.brightPurple.main}>
variant="body2"
color={theme.palette.brightPurple.main}
>
00.00 руб. 00.00 руб.
</Typography> </Typography>
</Box> </Box>
@ -112,12 +104,12 @@ export default function HeaderFull() {
width: "36px", width: "36px",
}} }}
/> />
<LogoutButton <LogoutButton
onClick={handleLogoutClick} onClick={handleLogoutClick}
sx={{ sx={{
ml: "20px", ml: "20px",
}} }}
/> />
</> </>
)} )}
</Box> </Box>

@ -1,330 +1,344 @@
import { CropIcon } from "@icons/CropIcon"; import { CropIcon } from "@icons/CropIcon";
import { ResetIcon } from "@icons/ResetIcon"; import { ResetIcon } from "@icons/ResetIcon";
import { import {
Box, Box,
Button, Button,
IconButton, IconButton,
Modal, Modal,
Slider, Slider,
SxProps, SxProps,
Theme, Theme,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { FC, useMemo, useRef, useState } from "react"; import { FC, useMemo, useRef, useState } from "react";
import ReactCrop, { Crop, PixelCrop } from "react-image-crop"; import ReactCrop, { Crop, PixelCrop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css"; import "react-image-crop/dist/ReactCrop.css";
import { canvasPreview } from "./utils/canvasPreview"; import { canvasPreview } from "./utils/canvasPreview";
const styleSlider: SxProps<Theme> = { const styleSlider: SxProps<Theme> = {
color: "#7E2AEA", color: "#7E2AEA",
height: "12px", height: "12px",
"& .MuiSlider-track": { "& .MuiSlider-track": {
border: "none", border: "none",
}, },
"& .MuiSlider-rail": { "& .MuiSlider-rail": {
backgroundColor: "#F2F3F7", backgroundColor: "#F2F3F7",
border: `1px solid #9A9AAF`, border: `1px solid #9A9AAF`,
}, },
"& .MuiSlider-thumb": { "& .MuiSlider-thumb": {
height: 26, height: 26,
width: 26, width: 26,
border: `6px solid #7E2AEA`, border: `6px solid #7E2AEA`,
backgroundColor: "white", backgroundColor: "white",
boxShadow: `0px 0px 0px 3px white, boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`, 0px 4px 4px 3px #C3C8DD`,
"&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": { "&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": {
boxShadow: `0px 0px 0px 3px white, boxShadow: `0px 0px 0px 3px white,
0px 4px 4px 3px #C3C8DD`, 0px 4px 4px 3px #C3C8DD`,
},
}, },
},
}; };
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
imageBlob: Blob | null; imageBlob: Blob | null;
originalImageUrl: string | null; originalImageUrl: string | null;
setCropModalImageBlob: (imageBlob: Blob) => void; setCropModalImageBlob: (imageBlob: Blob) => void;
onClose: () => void; onClose: () => void;
onSaveImageClick: (imageBlob: Blob) => void; onSaveImageClick: (imageBlob: Blob) => void;
} }
export const CropModal: FC<Props> = ({ isOpen, imageBlob, originalImageUrl, setCropModalImageBlob, onSaveImageClick, onClose }) => { export const CropModal: FC<Props> = ({
const theme = useTheme(); isOpen,
const [crop, setCrop] = useState<Crop>(); imageBlob,
const [completedCrop, setCompletedCrop] = useState<PixelCrop>(); originalImageUrl,
const [darken, setDarken] = useState(0); setCropModalImageBlob,
const [rotate, setRotate] = useState(0); onSaveImageClick,
const [width, setWidth] = useState<number>(240); onClose,
const cropImageElementRef = useRef<HTMLImageElement>(null); }) => {
const isMobile = useMediaQuery(theme.breakpoints.down(786)); const theme = useTheme();
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>(240);
const cropImageElementRef = useRef<HTMLImageElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down(786));
const imageUrl = useMemo(() => imageBlob && URL.createObjectURL(imageBlob), [imageBlob]); const imageUrl = useMemo(() => imageBlob && URL.createObjectURL(imageBlob), [imageBlob]);
const handleCropClick = async () => { const handleCropClick = async () => {
if (!completedCrop) throw new Error("No completed crop"); if (!completedCrop) throw new Error("No completed crop");
if (!cropImageElementRef.current) throw new Error("No image"); if (!cropImageElementRef.current) throw new Error("No image");
const canvasCopy = document.createElement("canvas"); const canvasCopy = document.createElement("canvas");
const ctx = canvasCopy.getContext("2d"); const ctx = canvasCopy.getContext("2d");
if (!ctx) throw new Error("No 2d context"); if (!ctx) throw new Error("No 2d context");
canvasCopy.width = completedCrop.width; canvasCopy.width = completedCrop.width;
canvasCopy.height = completedCrop.height; canvasCopy.height = completedCrop.height;
ctx.filter = `brightness(${100 - darken}%)`; ctx.filter = `brightness(${100 - darken}%)`;
await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate); await canvasPreview(cropImageElementRef.current, canvasCopy, completedCrop, rotate);
canvasCopy.toBlob((blob) => { canvasCopy.toBlob((blob) => {
if (!blob) throw new Error("Failed to create blob"); if (!blob) throw new Error("Failed to create blob");
setCropModalImageBlob(blob); setCropModalImageBlob(blob);
setCrop(undefined); setCrop(undefined);
setCompletedCrop(undefined); setCompletedCrop(undefined);
}); });
}; };
function handleSaveClick() { function handleSaveClick() {
if (imageBlob) onSaveImageClick?.(imageBlob); if (imageBlob) onSaveImageClick?.(imageBlob);
setCrop(undefined); setCrop(undefined);
setCompletedCrop(undefined); setCompletedCrop(undefined);
onClose(); onClose();
}
async function handleLoadOriginalImage() {
if (!originalImageUrl) return;
const response = await fetch(originalImageUrl);
const blob = await response.blob();
setCropModalImageBlob(blob);
setCrop(undefined);
setCompletedCrop(undefined);
}
const getImageSize = () => {
if (cropImageElementRef.current) {
const imageWidth = cropImageElementRef.current.naturalWidth;
const imageHeight = cropImageElementRef.current.naturalHeight;
const aspect = imageWidth / imageHeight;
if (aspect <= 1.333) {
setWidth(240);
}
if (aspect >= 1.5) {
setWidth(580);
}
if (aspect >= 1.778) {
setWidth(580);
}
} }
};
async function handleLoadOriginalImage() { return (
if (!originalImageUrl) return; <Modal
open={isOpen}
const response = await fetch(originalImageUrl); onClose={onClose}
const blob = await response.blob(); aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
setCropModalImageBlob(blob); >
setCrop(undefined); <Box
setCompletedCrop(undefined); sx={{
} position: "absolute",
top: "50%",
const getImageSize = () => { left: "50%",
if (cropImageElementRef.current) { transform: "translate(-50%, -50%)",
const imageWidth = cropImageElementRef.current.naturalWidth; bgcolor: "background.paper",
const imageHeight = cropImageElementRef.current.naturalHeight; boxShadow: 24,
padding: "20px",
const aspect = imageWidth / imageHeight; borderRadius: "8px",
width: isMobile ? "343px" : "620px",
if (aspect <= 1.333) { }}
setWidth(240); >
} <Box
if (aspect >= 1.5) { sx={{
setWidth(580); height: "320px",
} padding: "10px",
if (aspect >= 1.778) { backgroundSize: "cover",
setWidth(580); backgroundRepeat: "no-repeat",
} display: "flex",
} alignItems: "center",
}; justifyContent: "center",
}}
return (
<Modal
open={isOpen}
onClose={onClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
> >
<Box sx={{ {imageUrl && (
position: "absolute", <ReactCrop
top: "50%", crop={crop}
left: "50%", onChange={(_, percentCrop) => setCrop(percentCrop)}
transform: "translate(-50%, -50%)", onComplete={(c) => setCompletedCrop(c)}
bgcolor: "background.paper", maxWidth={500}
boxShadow: 24, minWidth={50}
padding: "20px", maxHeight={320}
borderRadius: "8px", minHeight={50}
width: isMobile ? "343px" : "620px", >
}}> <img
<Box onLoad={getImageSize}
sx={{ ref={cropImageElementRef}
height: "320px", alt="Crop me"
padding: "10px", src={imageUrl}
backgroundSize: "cover", style={{
backgroundRepeat: "no-repeat", filter: `brightness(${100 - darken}%)`,
display: "flex", transform: ` rotate(${rotate}deg)`,
alignItems: "center", maxWidth: "580px",
justifyContent: "center", maxHeight: "320px",
}} }}
> width={width}
{imageUrl && ( />
<ReactCrop </ReactCrop>
crop={crop} )}
onChange={(_, percentCrop) => setCrop(percentCrop)} </Box>
onComplete={(c) => setCompletedCrop(c)} <Box
maxWidth={500} sx={{
minWidth={50} color: "#7E2AEA",
maxHeight={320} display: "flex",
minHeight={50} alignItems: "center",
> justifyContent: "center",
<img fontSize: "16xp",
onLoad={getImageSize} fontWeight: "600",
ref={cropImageElementRef} marginBottom: "50px",
alt="Crop me" }}
src={imageUrl} >
style={{ <Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
filter: `brightness(${100 - darken}%)`, {crop?.width ? Math.round(crop.width) + "px" : ""}
transform: ` rotate(${rotate}deg)`, </Typography>
maxWidth: "580px", <Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
maxHeight: "320px", {crop?.height ? Math.round(crop.height) + "px" : ""}
}} </Typography>
width={width} </Box>
/>
</ReactCrop>
)}
</Box>
<Box
sx={{
color: "#7E2AEA",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "16xp",
fontWeight: "600",
marginBottom: "50px",
}}
>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.width ? Math.round(crop.width) + "px" : ""}
</Typography>
<Typography sx={{ color: "#7E2AEA", lineHeight: "0px" }}>
{crop?.height ? Math.round(crop.height) + "px" : ""}
</Typography>
</Box>
<Box <Box
sx={{ sx={{
display: isMobile ? "block" : "flex", display: isMobile ? "block" : "flex",
alignItems: "end", alignItems: "end",
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<IconButton onClick={() => setRotate(r => (r + 90) % 360)}> <IconButton onClick={() => setRotate((r) => (r + 90) % 360)}>
<ResetIcon /> <ResetIcon />
</IconButton> </IconButton>
<Box> <Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}> <Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>Размер</Typography>
Размер <Slider
</Typography> sx={[
<Slider styleSlider,
sx={[styleSlider, { {
width: isMobile ? "350px" : "250px", width: isMobile ? "350px" : "250px",
}]} },
value={width} ]}
min={50} value={width}
max={580} min={50}
step={1} max={580}
onChange={(_, newValue) => { step={1}
setWidth(newValue as number); onChange={(_, newValue) => {
}} setWidth(newValue as number);
/> }}
</Box> />
<Box> </Box>
<Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}> <Box>
Затемнение <Typography sx={{ color: "#9A9AAF", fontSize: "16px" }}>Затемнение</Typography>
</Typography> <Slider
<Slider sx={[
sx={[styleSlider, { styleSlider,
width: isMobile ? "350px" : "250px", {
}]} width: isMobile ? "350px" : "250px",
value={darken} },
min={0} ]}
max={100} value={darken}
step={1} min={0}
onChange={(_, newValue) => setDarken(newValue as number)} max={100}
/> step={1}
</Box> onChange={(_, newValue) => setDarken(newValue as number)}
</Box> />
<Box </Box>
sx={{ </Box>
marginTop: "40px", <Box
width: "100%", sx={{
display: "flex", marginTop: "40px",
}} width: "100%",
> display: "flex",
<Button gap: "10px",
onClick={handleSaveClick} }}
disableRipple >
data-cy="crop-modal-save-button" <Button
sx={{ onClick={handleCropClick}
height: "48px", disableRipple
color: "#7E2AEA", disabled={!completedCrop}
borderRadius: "8px", sx={{
border: "1px solid #7E2AEA", padding: "10px 20px",
marginRight: "10px", borderRadius: "8px",
px: "20px", background: theme.palette.brightPurple.main,
}} fontSize: "18px",
>Сохранить</Button> color: "#7E2AEA",
<Button border: `1px solid ${!completedCrop ? "rgba(0, 0, 0, 0.26)" : "#7E2AEA"}`,
onClick={handleLoadOriginalImage} backgroundColor: "transparent",
disableRipple }}
disabled={!originalImageUrl} >
sx={{ <CropIcon color={!completedCrop ? "rgba(0, 0, 0, 0.26)" : "#7E2AEA"} />
width: "215px", Обрезать
height: "48px", </Button>
color: "#7E2AEA", <Button
borderRadius: "8px", onClick={handleLoadOriginalImage}
border: "1px solid #7E2AEA", disableRipple
marginRight: "10px", disabled={!originalImageUrl}
ml: "auto", sx={{
}} width: "215px",
> height: "48px",
Загрузить оригинал color: "#7E2AEA",
</Button> borderRadius: "8px",
<Button border: "1px solid #7E2AEA",
onClick={handleCropClick} }}
disableRipple >
variant="contained" Загрузить оригинал
disabled={!completedCrop} </Button>
sx={{ <Button
padding: "10px 20px", onClick={handleSaveClick}
borderRadius: "8px", disableRipple
background: theme.palette.brightPurple.main, variant="contained"
fontSize: "18px", data-cy="crop-modal-save-button"
}} sx={{
> height: "48px",
<CropIcon /> borderRadius: "8px",
Обрезать border: "1px solid #7E2AEA",
</Button> marginRight: "10px",
</Box> px: "20px",
</Box> ml: "auto",
</Modal> }}
); >
Сохранить
</Button>
</Box>
</Box>
</Modal>
);
}; };
export function useCropModalState(initialOpenState = false) { export function useCropModalState(initialOpenState = false) {
const [isCropModalOpen, setOpened] = useState(initialOpenState); const [isCropModalOpen, setOpened] = useState(initialOpenState);
const [imageBlob, setCropModalImageBlob] = useState<Blob | null>(null); const [imageBlob, setCropModalImageBlob] = useState<Blob | null>(null);
const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null); const [originalImageUrl, setOriginalImageUrl] = useState<string | null>(null);
const closeCropModal = () => { const closeCropModal = () => {
setOpened(false); setOpened(false);
setCropModalImageBlob(null); setCropModalImageBlob(null);
setOriginalImageUrl(null); setOriginalImageUrl(null);
}; };
async function openCropModal(image: Blob | string, originalImageUrl: string | null | undefined = null) { async function openCropModal(image: Blob | string, originalImageUrl: string | null | undefined = null) {
if (typeof image === "string") { if (typeof image === "string") {
const response = await fetch(image); const response = await fetch(image);
image = await response.blob(); image = await response.blob();
}
setCropModalImageBlob(image);
setOriginalImageUrl(originalImageUrl);
setOpened(true);
} }
return { setCropModalImageBlob(image);
isCropModalOpen, setOriginalImageUrl(originalImageUrl);
openCropModal, setOpened(true);
closeCropModal, }
imageBlob,
setCropModalImageBlob, return {
originalImageUrl, isCropModalOpen,
} as const; openCropModal,
closeCropModal,
imageBlob,
setCropModalImageBlob,
originalImageUrl,
} as const;
} }

@ -32,7 +32,9 @@ export default function Number({ question }: Props) {
gap: 1, gap: 1,
}} }}
> >
<Typography variant="h6" data-cy="question-title">{question.title}</Typography> <Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<Box <Box
sx={{ sx={{
px: 2, px: 2,

@ -32,7 +32,7 @@ export default function SwitchStepPages({
} }
case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />; case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />;
case 2: return <ResultPage />; case 2: return <ResultPage />;
case 3: return <QuestionsMap />; case 3: return <ContactFormPage />;
case 4: return <ContactFormPage />; case 4: return <ContactFormPage />;
case 5: return <InstallQuiz />; case 5: return <InstallQuiz />;
case 6: return <>Реклама</>; case 6: return <>Реклама</>;

@ -4114,10 +4114,15 @@ csstype@^3.0.2, csstype@^3.1.2:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
cypress@^13.4.0: cypress-file-upload@^5.0.8:
version "13.4.0" version "5.0.8"
resolved "https://registry.npmjs.org/cypress/-/cypress-13.4.0.tgz" resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
integrity sha512-KeWNC9xSHG/ewZURVbaQsBQg2mOKw4XhjJZFKjWbEjgZCdxpPXLpJnfq5Jns1Gvnjp6AlnIfpZfWFlDgVKXdWQ== integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==
cypress@^13.6.1:
version "13.6.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.1.tgz#c5f714f08551666ed3ac1fa95718eabb23a416df"
integrity sha512-k1Wl5PQcA/4UoTffYKKaxA0FJKwg8yenYNYRzLt11CUR0Kln+h7Udne6mdU1cUIdXBDTVZWtmiUjzqGs7/pEpw==
dependencies: dependencies:
"@cypress/request" "^3.0.0" "@cypress/request" "^3.0.0"
"@cypress/xvfb" "^1.2.4" "@cypress/xvfb" "^1.2.4"