Merge branch 'dev' of penahub.gitlab.yandexcloud.net:frontend/squiz into dev

This commit is contained in:
Nastya 2023-12-16 19:00:22 +03:00
commit ca686e397a
15 changed files with 1229 additions and 1080 deletions

@ -5,7 +5,7 @@ import "dayjs/locale/ru";
import SigninDialog from "./pages/auth/Signin"; import SigninDialog from "./pages/auth/Signin";
import SignupDialog from "./pages/auth/Signup"; import SignupDialog from "./pages/auth/Signup";
import { ViewPage } from "./pages/ViewPublicationPage"; import { ViewPage } from "./pages/ViewPublicationPage";
import { BrowserRouter, Route, Routes, useLocation, useNavigate, Navigate } from "react-router-dom"; import { Route, Routes, useLocation, useNavigate, Navigate } from "react-router-dom";
import "./index.css"; import "./index.css";
import ContactFormPage from "./pages/ContactFormPage/ContactFormPage"; import ContactFormPage from "./pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "./pages/InstallQuiz/InstallQuiz"; import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
@ -21,61 +21,65 @@ import { clearUserData, setUser, useUserStore } from "@root/user";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import PrivateRoute from "@ui_kit/PrivateRoute"; import PrivateRoute from "@ui_kit/PrivateRoute";
import { Restore } from "./pages/startPage/Restore";
dayjs.locale("ru"); dayjs.locale("ru");
const routeslink = [ const routeslink = [
{ path: "/list", page: <MyQuizzesFull />, header: false, sidebar: false }, { path: "/list", page: <MyQuizzesFull />, header: false, sidebar: false },
{ path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true, }, { path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true },
{ path: "/contacts", page: <ContactFormPage />, header: true, sidebar: true }, { path: "/contacts", page: <ContactFormPage />, header: true, sidebar: true },
{ path: "/result", page: <Result />, header: true, sidebar: true }, { path: "/result", page: <Result />, header: true, sidebar: true },
{ path: "/settings", page: <ResultSettings />, header: true, sidebar: true }, { path: "/settings", page: <ResultSettings />, header: true, sidebar: true },
{ path: "/install", page: <InstallQuiz />, header: true, sidebar: true }, { path: "/install", page: <InstallQuiz />, header: true, sidebar: true },
] as const; ] as const;
export default function App() { export default function App() {
const userId = useUserStore((state) => state.userId); const userId = useUserStore((state) => state.userId);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
useUserFetcher({ useUserFetcher({
url: `https://hub.pena.digital/user/${userId}`, url: `https://hub.pena.digital/user/${userId}`,
userId, userId,
onNewUser: setUser, onNewUser: setUser,
onError: (error) => { onError: (error) => {
const errorMessage = getMessageFromFetchError(error); const errorMessage = getMessageFromFetchError(error);
if (errorMessage) { if (errorMessage) {
enqueueSnackbar(errorMessage); enqueueSnackbar(errorMessage);
clearUserData(); clearUserData();
clearAuthToken(); clearAuthToken();
} }
}, },
}); });
if (location.state?.redirectTo) if (location.state?.redirectTo)
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />; return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
return ( return (
<> <>
<ContactFormModal /> <ContactFormModal />
{location.state?.backgroundLocation && ( {location.state?.backgroundLocation && (
<Routes> <Routes>
<Route path="/signin" element={<SigninDialog />} /> <Route path="/signin" element={<SigninDialog />} />
<Route path="/signup" element={<SignupDialog />} /> <Route path="/signup" element={<SignupDialog />} />
</Routes> <Route path="/restore" element={<Restore />} />
)} </Routes>
<Routes location={location.state?.backgroundLocation || location}> )}
<Route path="/" element={<Landing />} /> <Routes location={location.state?.backgroundLocation || location}>
<Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} /> <Route path="/" element={<Landing />} />
<Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} /> <Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} />
<Route element={<PrivateRoute />}> <Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} />
{routeslink.map((e, i) => ( <Route path="/restore" element={<Navigate to="/" replace state={{ redirectTo: "/restore" }} />} />
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} /> <Route element={<PrivateRoute />}>
))} {routeslink.map((e, i) => (
<Route path="edit" element={<EditPage />} /> <Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
<Route path="crop" element={<ImageCrop />} /> ))}
<Route path="/view" element={<ViewPage />} />
</Route> <Route path="edit" element={<EditPage />} />
</Routes> <Route path="crop" element={<ImageCrop />} />
</> <Route path="/view" element={<ViewPage />} />
); </Route>
} </Routes>
</>
);
}

@ -51,7 +51,6 @@ type Popper = {
type NodeSingularWithPopper = NodeSingular & { type NodeSingularWithPopper = NodeSingular & {
popper: (config: PopperConfig) => Popper; popper: (config: PopperConfig) => Popper;
}; };
let counter = 0;
const stylesheet: Stylesheet[] = [ const stylesheet: Stylesheet[] = [
{ {
@ -161,7 +160,6 @@ function CsComponent({
}, [modalQuestionTargetContentId]) }, [modalQuestionTargetContentId])
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => { const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
console.log('AN', counter++, parentNodeContentId,targetNodeContentId)
//запрещаем работу родителя-ребенка если это один и тот же вопрос //запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return if (parentNodeContentId === targetNodeContentId) return
@ -171,7 +169,6 @@ console.log('AN', counter++, parentNodeContentId,targetNodeContentId)
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
console.log('AN1', targetQuestion, parentNodeContentId,parentNodeChildren)
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) { if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren }) clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed', true) cy?.data('changed', true)
@ -199,13 +196,13 @@ console.log('AN1', targetQuestion, parentNodeContentId,parentNodeChildren)
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => { const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
console.log('AN2', parentNodeContentId,parentNodeChildren,targetQuestion)
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
//смотрим не добавлен ли родителю 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);
} }
}) })

@ -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) => { };
if (destination) reorderQuestions(source.index, destination.index);
};
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable-list"> <Droppable droppableId="droppable-list">
{(provided) => ( {(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{questions?.map((question, index) => ( {questions?.map((question, index) => (
<FormDraggableListItem <FormDraggableListItem key={question.id} question={question} questionIndex={index} />
key={question.id} ))}
question={question} {provided.placeholder}
questionIndex={index} </Box>
/> )}
))} </Droppable>
{provided.placeholder} </DragDropContext>
</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>
);
} }

@ -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 = 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: isMobile ? "140px" : "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/>}
</>
);
} }

@ -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)
}
} }

@ -4,14 +4,23 @@ 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; description?: string;
accept?: ImageFormat[];
} }
export const UploadImageModal: React.FC<ModalkaProps> = ({ handleImageChange, isOpen, onClose, description }) => { export const UploadImageModal: React.FC<ModalkaProps> = ({
handleImageChange,
isOpen,
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);
@ -31,6 +40,10 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({ handleImageChange, is
handleImageChange(file); handleImageChange(file);
}; };
const acceptedFormats = accept ? accept.map((format) => "." + format).join(", ") : "";
console.log(acceptedFormats);
return ( return (
<Modal <Modal
open={isOpen} open={isOpen}
@ -67,7 +80,7 @@ export const UploadImageModal: React.FC<ModalkaProps> = ({ handleImageChange, is
<input <input
onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])} onChange={(event) => event.target.files?.[0] && handleImageChange(event.target.files[0])}
hidden hidden
accept=".jpg, .jpeg, .png" accept={acceptedFormats || ".jpg, .jpeg, .png , .gif"}
multiple multiple
type="file" type="file"
data-cy="upload-image-input" data-cy="upload-image-input"

@ -1,18 +1,9 @@
import { login } from "@api/auth"; import { login } from "@api/auth";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { import { Box, Button, Dialog, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setUserId, useUserStore } from "@root/user"; import { setUserId, useUserStore } from "@root/user";
import InputTextfield from "@ui_kit/InputTextfield"; import InputTextfield from "@ui_kit/InputTextfield";
import PenaLogo2 from "@ui_kit/PenaLogo2"; import Logotip from "../../pages/Landing/images/icons/QuizLogo";
import PasswordInput from "@ui_kit/passwordInput"; import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
@ -31,9 +22,7 @@ const initialValues: Values = {
}; };
const validationSchema = object({ const validationSchema = object({
email: string() email: string().required("Поле обязательно").email("Введите корректный email"),
.required("Поле обязательно")
.email("Введите корректный email"),
password: string().required("Поле обязательно").min(8, "Минимум 8 символов"), password: string().required("Поле обязательно").min(8, "Минимум 8 символов"),
}); });
@ -49,10 +38,7 @@ export default function SigninDialog() {
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values, formikHelpers) => { onSubmit: async (values, formikHelpers) => {
const [loginResponse, loginError] = await login( const [loginResponse, loginError] = await login(values.email.trim(), values.password.trim());
values.email.trim(),
values.password.trim()
);
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
@ -111,8 +97,7 @@ export default function SigninDialog() {
gap: "15px", gap: "15px",
borderRadius: "12px", borderRadius: "12px",
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)", boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled": "& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled": {
{
position: "absolute", position: "absolute",
top: "46px", top: "46px",
margin: "0", margin: "0",
@ -130,7 +115,7 @@ export default function SigninDialog() {
<CloseIcon sx={{ transform: "scale(1.5)" }} /> <CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton> </IconButton>
<Box> <Box>
<PenaLogo2 width={upMd ? 233 : 196} color="black" /> <Logotip width={upMd ? 233 : 196} />
</Box> </Box>
<Typography <Typography
sx={{ sx={{
@ -212,16 +197,8 @@ export default function SigninDialog() {
mt: "auto", mt: "auto",
}} }}
> >
<Typography <Typography sx={{ color: "#7E2AEA", textAlign: "center" }}>Вы еще не присоединились?</Typography>
sx={{ color: "#7E2AEA", textAlign: "center" }} <Link component={RouterLink} to="/signup" sx={{ color: "#7E2AEA" }}>
>
Вы еще не присоединились?
</Typography>
<Link
component={RouterLink}
to="/signup"
sx={{ color: "#7E2AEA" }}
>
Регистрация Регистрация
</Link> </Link>
</Box> </Box>

@ -1,23 +1,14 @@
import { register } from "@api/auth"; import { register } from "@api/auth";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import { import { Box, Button, Dialog, IconButton, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setUserId, useUserStore } from "@root/user"; import { setUserId, useUserStore } from "@root/user";
import InputTextfield from "@ui_kit/InputTextfield"; import InputTextfield from "@ui_kit/InputTextfield";
import PenaLogo2 from "@ui_kit/PenaLogo2"; import Logotip from "../../pages/Landing/images/icons/QuizLogo";
import PasswordInput from "@ui_kit/passwordInput"; import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import {Link as RouterLink, useLocation, useNavigate} from "react-router-dom"; import { Link as RouterLink, useLocation, useNavigate } from "react-router-dom";
import { object, ref, string } from "yup"; import { object, ref, string } from "yup";
interface Values { interface Values {
@ -33,9 +24,7 @@ const initialValues: Values = {
}; };
const validationSchema = object({ const validationSchema = object({
email: string() email: string().required("Поле обязательно").email("Введите корректный email"),
.required("Поле обязательно")
.email("Введите корректный email"),
password: string() password: string()
.min(8, "Минимум 8 символов") .min(8, "Минимум 8 символов")
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы") .matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы")
@ -50,18 +39,14 @@ export default function SignupDialog() {
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const location = useLocation() const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const formik = useFormik<Values>({ const formik = useFormik<Values>({
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values, formikHelpers) => { onSubmit: async (values, formikHelpers) => {
const [registerResponse, registerError] = await register( const [registerResponse, registerError] = await register(values.email.trim(), values.password.trim(), "+7");
values.email.trim(),
values.password.trim(),
"+7"
);
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
@ -120,12 +105,11 @@ export default function SignupDialog() {
gap: "15px", gap: "15px",
borderRadius: "12px", borderRadius: "12px",
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)", boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled": "& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled": {
{ position: "absolute",
position: "absolute", top: "46px",
top: "46px", margin: "0",
margin: "0", },
},
}} }}
> >
<IconButton <IconButton
@ -139,7 +123,7 @@ export default function SignupDialog() {
<CloseIcon sx={{ transform: "scale(1.5)" }} /> <CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton> </IconButton>
<Box sx={{ mt: upMd ? undefined : "62px" }}> <Box sx={{ mt: upMd ? undefined : "62px" }}>
<PenaLogo2 width={upMd ? 233 : 196} color="black" /> <Logotip width={upMd ? 233 : 196} />
</Box> </Box>
<Typography <Typography
sx={{ sx={{
@ -186,11 +170,8 @@ export default function SignupDialog() {
value: formik.values.repeatPassword, value: formik.values.repeatPassword,
placeholder: "Не менее 8 символов", placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur, onBlur: formik.handleBlur,
error: error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
formik.touched.repeatPassword && helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
Boolean(formik.errors.repeatPassword),
helperText:
formik.touched.repeatPassword && formik.errors.repeatPassword,
autoComplete: "new-password", autoComplete: "new-password",
"data-cy": "repeat-password", "data-cy": "repeat-password",
}} }}

@ -52,6 +52,7 @@ export default function FaviconDropZone({ imageUrl, onImageUploadClick, onDelete
onClose={closeImageUploadModal} onClose={closeImageUploadModal}
handleImageChange={handleImageUpload} handleImageChange={handleImageUpload}
description="Принимает JPG, PNG — максимум 5mb" description="Принимает JPG, PNG — максимум 5mb"
accept={["jpeg", "jpg", "png"]}
/> />
<Box <Box
onDragEnter={() => !imageUrl && setIsDropReady(true)} onDragEnter={() => !imageUrl && setIsDropReady(true)}

@ -0,0 +1,189 @@
import { FC, useState } from "react";
import CloseIcon from "@mui/icons-material/Close";
import { Box, Button, Dialog, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import InputTextfield from "@ui_kit/InputTextfield";
import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { object, ref, string } from "yup";
import Logotip from "../Landing/images/icons/QuizLogo";
import { useNavigate } from "react-router-dom";
interface Values {
email: string;
password: string;
repeatPassword: string;
}
const initialValues: Values = {
email: "",
password: "",
repeatPassword: "",
};
const validationSchema = object({
email: string().required("Поле обязательно").email("Введите корректный email"),
password: string()
.min(8, "Минимум 8 символов")
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы")
.required("Поле обязательно"),
repeatPassword: string()
.oneOf([ref("password"), undefined], "Пароли не совпадают")
.required("Повторите пароль"),
});
export const Restore: FC = () => {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const navigate = useNavigate();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const formik = useFormik<Values>({
initialValues,
validationSchema,
onSubmit: (values) => {
console.log(values);
},
});
function handleClose() {
setIsDialogOpen(false);
setTimeout(() => navigate("/"), theme.transitions.duration.leavingScreen);
}
return (
<Box>
<Button sx={{ width: "200px", height: "200px", background: "red" }} onClick={() => setIsDialogOpen(true)}>
open
</Button>
<Dialog
open={isDialogOpen}
onClose={handleClose}
PaperProps={{
sx: {
width: "600px",
maxWidth: "600px",
},
}}
slotProps={{
backdrop: {
style: {
backgroundColor: "rgb(0 0 0 / 0.7)",
},
},
}}
>
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
sx={{
position: "relative",
backgroundColor: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
p: upMd ? "50px" : "18px",
pb: upMd ? "40px" : "30px",
gap: "15px",
borderRadius: "12px",
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled": {
position: "absolute",
top: "46px",
margin: "0",
},
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
right: "7px",
top: "7px",
}}
>
<CloseIcon sx={{ transform: "scale(1.5)" }} />
</IconButton>
<Box sx={{ mt: upMd ? undefined : "62px" }}>
<Logotip width={upMd ? 233 : 196} />
</Box>
<Typography
sx={{
color: "#4D4D4D",
mt: "5px",
mb: upMd ? "30px" : "33px",
}}
>
Восстановление пароля
</Typography>
<InputTextfield
TextfieldProps={{
value: formik.values.email,
placeholder: "username",
onBlur: formik.handleBlur,
error: formik.touched.email && Boolean(formik.errors.email),
helperText: formik.touched.email && formik.errors.email,
"data-cy": "username",
}}
onChange={formik.handleChange}
color="#F2F3F7"
id="email"
label="Email"
gap={upMd ? "10px" : "10px"}
/>
<PasswordInput
TextfieldProps={{
value: formik.values.password,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.password && Boolean(formik.errors.password),
helperText: formik.touched.password && formik.errors.password,
autoComplete: "new-password",
"data-cy": "password",
}}
onChange={formik.handleChange}
color="#F2F3F7"
id="password"
label="Пароль"
gap={upMd ? "10px" : "10px"}
/>
<PasswordInput
TextfieldProps={{
value: formik.values.repeatPassword,
placeholder: "Не менее 8 символов",
onBlur: formik.handleBlur,
error: formik.touched.repeatPassword && Boolean(formik.errors.repeatPassword),
helperText: formik.touched.repeatPassword && formik.errors.repeatPassword,
autoComplete: "new-password",
"data-cy": "repeat-password",
}}
onChange={formik.handleChange}
color="#F2F3F7"
id="repeatPassword"
label="Повторить пароль"
gap={upMd ? "10px" : "10px"}
/>
<Button
variant="contained"
fullWidth
type="submit"
disabled={formik.isSubmitting}
sx={{
py: "12px",
"&:hover": {
backgroundColor: "#581CA7",
},
"&:active": {
color: "white",
backgroundColor: "black",
},
}}
data-cy="signup"
>
Восстановить
</Button>
</Box>
</Dialog>
</Box>
);
};