use backend somewhere & refactor

This commit is contained in:
nflnkr 2023-11-13 21:04:51 +03:00
parent f7f1ce13ab
commit 0c6198dd79
26 changed files with 2132 additions and 1131 deletions

@ -41,6 +41,7 @@
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"react-router-dom": "^6.6.2", "react-router-dom": "^6.6.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"swr": "^2.2.4",
"typescript": "^4.4.2", "typescript": "^4.4.2",
"use-debounce": "^9.0.4", "use-debounce": "^9.0.4",
"web-vitals": "^2.1.0", "web-vitals": "^2.1.0",

@ -5,40 +5,44 @@ import { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit"; import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
import { GetQuizRequest, GetQuizResponse } from "model/quiz/get"; import { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList"; import { GetQuizListRequest, GetQuizListResponse } from "model/quiz/getList";
import { BackendQuiz } from "model/quiz/quiz"; import { RawQuiz } from "model/quiz/quiz";
const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz"; const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz";
function createQuiz(body: CreateQuizRequest = defaultCreateQuizBody) { function createQuiz(body?: Partial<CreateQuizRequest>) {
return makeRequest<CreateQuizRequest, BackendQuiz>({ return makeRequest<CreateQuizRequest, RawQuiz>({
url: `${baseUrl}/quiz/create`, url: `${baseUrl}/quiz/create`,
body, body: { ...defaultCreateQuizBody, ...body },
method: "POST", method: "POST",
}); });
} }
function getQuizList(body: GetQuizListRequest = defaultGetQuizListBody) { async function getQuizList(body?: Partial<GetQuizListRequest>) {
return makeRequest<GetQuizListRequest, GetQuizListResponse>({ const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
url: `${baseUrl}/quiz/getList`, url: `${baseUrl}/quiz/getList`,
body, body: { ...defaultGetQuizListBody, ...body },
method: "GET", method: "POST",
}); });
return response.items;
} }
function getQuiz(body: GetQuizRequest = defaultGetQuizBody) { function getQuiz(body?: Partial<GetQuizRequest>) {
return makeRequest<GetQuizRequest, GetQuizResponse>({ return makeRequest<GetQuizRequest, GetQuizResponse>({
url: `${baseUrl}/quiz/get`, url: `${baseUrl}/quiz/get`,
body, body: { ...defaultGetQuizBody, ...body },
method: "GET", method: "GET",
}); });
} }
function editQuiz(body: EditQuizRequest = defaultEditQuizBody) { async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
// await new Promise((resolve) => setTimeout(resolve, 1000));
return makeRequest<EditQuizRequest, EditQuizResponse>({ return makeRequest<EditQuizRequest, EditQuizResponse>({
url: `${baseUrl}/quiz/edit`, url: `${baseUrl}/quiz/edit`,
body, body,
method: "PATCH", method: "PATCH",
signal,
}); });
} }
@ -86,18 +90,18 @@ const defaultCreateQuizBody: CreateQuizRequest = {
"fingerprinting": true, "fingerprinting": true,
"repeatable": true, "repeatable": true,
"note_prevented": true, "note_prevented": true,
"mail_notifications": true, "mail_notifications": false,
"unique_answers": true, "unique_answers": true,
"name": "string", "name": "string",
"description": "string", "description": "string",
"config": "string", "config": "string",
"status": "string", "status": "stop",
"limit": 0, "limit": 0,
"due_to": 0, "due_to": 0,
"time_of_passing": 0, "time_of_passing": 0,
"pausable": true, "pausable": false,
"question_cnt": 0, "question_cnt": 0,
"super": true, "super": false,
"group_id": 0, "group_id": 0,
}; };
@ -129,14 +133,6 @@ const defaultGetQuizBody: GetQuizRequest = {
}; };
const defaultGetQuizListBody: GetQuizListRequest = { const defaultGetQuizListBody: GetQuizListRequest = {
"limit": 0, "limit": 100,
"offset": 0, "offset": 0,
"from": 0,
"to": 0,
"search": "string",
"status": "string",
"deleted": true,
"archived": true,
"super": true,
"group_id": 0,
}; };

@ -11,6 +11,7 @@ import { HTML5Backend } from "react-dnd-html5-backend";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import lightTheme from "./utils/themes/light"; import lightTheme from "./utils/themes/light";
import { SWRConfig } from "swr";
dayjs.locale("ru"); dayjs.locale("ru");
@ -20,14 +21,16 @@ const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeTe
const root = createRoot(document.getElementById("root")!); const root = createRoot(document.getElementById("root")!);
root.render( root.render(
<DndProvider backend={HTML5Backend}> <SWRConfig value={{ revalidateOnFocus: false, shouldRetryOnError: false }}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}> <DndProvider backend={HTML5Backend}>
<ThemeProvider theme={lightTheme}> <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<SnackbarProvider> <ThemeProvider theme={lightTheme}>
<CssBaseline /> <SnackbarProvider>
<App /> <CssBaseline />
</SnackbarProvider> <App />
</ThemeProvider> </SnackbarProvider>
</LocalizationProvider> </ThemeProvider>
</DndProvider> </LocalizationProvider>
</DndProvider>
</SWRConfig>
); );

@ -1,18 +1,34 @@
export interface CreateQuizRequest { export interface CreateQuizRequest {
fingerprinting: boolean; /** set true for save deviceId */
repeatable: boolean; fingerprinting: boolean;
note_prevented: boolean; /** set true for allow user to repeat quiz */
repeatable: boolean;
/** set true for save statistic of incomplete quiz passing */
note_prevented: boolean;
/** set true for mail notification for each quiz passing */
mail_notifications: boolean; mail_notifications: boolean;
unique_answers: boolean; /** set true for save statistics only for unique quiz passing */
name: string; unique_answers: boolean;
description: string; /** name of quiz. max 280 length */
config: string; name: string;
status: string; /** description of quiz */
limit: number; description: string;
due_to: number; /** config of quiz. serialized json for rules of quiz flow */
time_of_passing: number; config: string;
pausable: boolean; /** status of quiz. allow only '', 'draft', 'template', 'stop', 'start' */
question_cnt: number; status: "draft" | "template" | "stop" | "start";
super: boolean; /** limit is count of max quiz passing */
group_id: number; limit: number;
/** last time when quiz is valid. timestamp in seconds */
due_to: number;
/** seconds to pass quiz */
time_of_passing: number;
/** true if it is allowed for pause quiz */
pausable: boolean;
/** count of questions */
question_cnt: number;
/** set true if squiz realize group functionality */
super: boolean;
/** group of new quiz */
group_id: number;
} }

@ -1,23 +1,66 @@
import { Quiz } from "./quiz";
export interface EditQuizRequest { export interface EditQuizRequest {
/** id of question for update */
id: number; id: number;
/** set true for storing fingerprints */
fp: boolean; fp: boolean;
/** set true for allow to repeat quiz after passing */
rep: boolean; rep: boolean;
/** set true for store unfinished passing */
note_prevented: boolean; note_prevented: boolean;
/** set true if we should send passing result on every passing */
mailing: boolean; mailing: boolean;
/** set true if we allow only one user quiz passing */
uniq: boolean; uniq: boolean;
name: string; /** new name of the quiz */
desc: string; name?: string;
conf: string; /** new descriptions of the quiz */
status: string; desc?: string;
/** new config of the quiz */
conf?: string;
/** new status. only draft,template,stop,start allowed */
status?: string;
/** max amount of quiz passing */
limit: number; limit: number;
/** max time of quiz passing */
due_to: number; due_to: number;
/** max time to pass quiz */
time_of_passing: number; time_of_passing: number;
/** allow to pause quiz to user */
pausable: boolean; pausable: boolean;
question_cnt: number; /** count of questions */
super: boolean; question_cnt?: number;
group_id: number; /** set true if squiz realize group functionality */
super?: boolean;
/** group of new quiz */
group_id?: number;
} }
export interface EditQuizResponse { export interface EditQuizResponse {
/** id of new version of question */
updated: number; updated: number;
} }
export function quizToEditQuizRequest(quiz: Quiz): EditQuizRequest {
return {
id: quiz.id,
fp: quiz.fingerprinting,
rep: quiz.repeatable,
note_prevented: quiz.note_prevented,
mailing: quiz.mail_notifications,
uniq: quiz.unique_answers,
name: quiz.name,
desc: quiz.description,
conf: JSON.stringify(quiz.config),
status: quiz.status,
limit: quiz.limit,
due_to: quiz.due_to,
time_of_passing: quiz.time_of_passing,
pausable: quiz.pausable,
question_cnt: quiz.question_cnt,
super: quiz.super,
group_id: quiz.group_id,
};
}

@ -1,17 +1,29 @@
import { RawQuiz } from "./quiz";
export interface GetQuizListRequest { export interface GetQuizListRequest {
limit: number; /** max items on page */
offset: number; limit?: number;
from: number; /** page number */
to: number; offset?: number;
search: string; /** start time of time period. timestamp in seconds */
status: string; from?: number;
deleted: boolean; /** end time of time period. timestamp in seconds */
archived: boolean; to?: number;
super: boolean; /** string for fulltext search in titles of quizes */
group_id: number; search?: string;
/** allow only - draft, template, timeout, stop, start, offlimit */
status?: "" | "draft" | "template" | "timeout" | "stop" | "start" | "offlimit";
/** get deleted quizes */
deleted?: boolean;
/** get archived quizes */
archived?: boolean;
/** set true if squiz realize group functionality */
super?: boolean;
/** group of new quiz */
group_id?: number;
} }
export interface GetQuizListResponse { export interface GetQuizListResponse {
count: number; count: number;
items: unknown[]; // TODO items: RawQuiz[];
} }

@ -1,29 +1,134 @@
export interface BackendQuiz { import { QuizConfig, defaultQuizConfig } from "@model/quizSettings";
export interface Quiz {
/** Id of created quiz */
id: number; id: number;
/** string id for customers */
qid: string; qid: string;
/** true if quiz deleted */
deleted: boolean; deleted: boolean;
/** true if quiz archived */
archived: boolean; archived: boolean;
/** set true for save deviceId */
fingerprinting: boolean; fingerprinting: boolean;
/** set true for allow user to repeat quiz */
repeatable: boolean; repeatable: boolean;
/** set true for save statistic of incomplete quiz passing */
note_prevented: boolean; note_prevented: boolean;
/** set true for mail notification for each quiz passing */
mail_notifications: boolean; mail_notifications: boolean;
/** set true for save statistics only for unique quiz passing */
unique_answers: boolean; unique_answers: boolean;
/** name of quiz. max 280 length */
name: string; name: string;
/** description of quiz */
description: string; description: string;
config: string; /** quiz config*/
config: QuizConfig;
/** status of quiz. allow only '', 'draft', 'template', 'stop', 'start' */
status: string; status: string;
/** limit is count of max quiz passing */
limit: number; limit: number;
/** last time when quiz is valid. timestamp in seconds */
due_to: number; due_to: number;
/** seconds to pass quiz */
time_of_passing: number; time_of_passing: number;
/** true if it is allowed for pause quiz */
pausable: boolean; pausable: boolean;
/** version of quiz */
version: number; version: number;
/** version comment to version of quiz */
version_comment: string; version_comment: string;
/** array of previous versions of quiz */
parent_ids: number[]; parent_ids: number[];
created_at: string; created_at: string;
updated_at: string; updated_at: string;
/** count of questions */
question_cnt: number; question_cnt: number;
/** count passings */
passed_count: number; passed_count: number;
/** average time of passing */
average_time: number; average_time: number;
/** set true if squiz realize group functionality */
super: boolean; super: boolean;
/** group of new quiz */
group_id: number; group_id: number;
} }
export interface RawQuiz {
/** Id of created quiz */
id: number;
/** string id for customers */
qid: string;
/** true if quiz deleted */
deleted: boolean;
/** true if quiz archived */
archived: boolean;
/** set true for save deviceId */
fingerprinting: boolean;
/** set true for allow user to repeat quiz */
repeatable: boolean;
/** set true for save statistic of incomplete quiz passing */
note_prevented: boolean;
/** set true for mail notification for each quiz passing */
mail_notifications: boolean;
/** set true for save statistics only for unique quiz passing */
unique_answers: boolean;
/** name of quiz. max 280 length */
name: string;
/** description of quiz */
description: string;
/** config of quiz. serialized json for rules of quiz flow */
config: string;
/** status of quiz. allow only '', 'draft', 'template', 'stop', 'start' */
status: string;
/** limit is count of max quiz passing */
limit: number;
/** last time when quiz is valid. timestamp in seconds */
due_to: number;
/** seconds to pass quiz */
time_of_passing: number;
/** true if it is allowed for pause quiz */
pausable: boolean;
/** version of quiz */
version: number;
/** version comment to version of quiz */
version_comment: string;
/** array of previous versions of quiz */
parent_ids: number[];
created_at: string;
updated_at: string;
/** count of questions */
question_cnt: number;
/** count passings */
passed_count: number;
/** average time of passing */
average_time: number;
/** set true if squiz realize group functionality */
super: boolean;
/** group of new quiz */
group_id: number;
}
export function quizToRawQuiz(quiz: Quiz): RawQuiz {
return {
...quiz,
config: JSON.stringify(quiz.config),
};
}
export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz {
let config = defaultQuizConfig;
try {
config = JSON.parse(rawQuiz.config);
} catch (error) {
console.warn("Cannot parse quiz config from string, using default config", error);
}
return {
...rawQuiz,
config,
};
}

72
src/model/quizSettings.ts Normal file

@ -0,0 +1,72 @@
export const quizSetupSteps = {
1: { displayStep: 1, text: "Настройка стартовой страницы" },
2: { displayStep: 1, text: "Настройка стартовой страницы" },
3: { displayStep: 1, text: "Настройка стартовой страницы" },
4: { displayStep: 2, text: "Задайте вопросы" },
5: { displayStep: 3, text: "Настройте авторезультаты" },
6: { displayStep: 3, text: "Настройте авторезультаты" },
7: { displayStep: 4, text: "Оценка графа карты вопросов" },
8: { displayStep: 5, text: "Настройте форму контактов" },
9: { displayStep: 6, text: "Установите квиз" },
10: { displayStep: 7, text: "Запустите рекламу" },
} as const;
export const maxQuizSetupSteps = Math.max(...Object.keys(quizSetupSteps).map(parseInt));
export const maxDisplayQuizSetupSteps = Math.max(...Object.values(quizSetupSteps).map(v => v.displayStep));
export type QuizSetupStep = keyof typeof quizSetupSteps;
export interface QuizConfig {
type: "quiz" | "form";
logo: string;
noStartPage: boolean;
startpageType: "standard" | "expanded" | "centered";
startpage: {
description: string;
button: string;
position: string;
background: {
type: string;
desktop: string;
mobile: string;
video: string;
cycle: boolean;
};
};
info: {
phonenumber: string;
clickable: boolean;
orgname: string;
site: string;
law?: string;
};
meta: string;
}
export const defaultQuizConfig: QuizConfig = {
type: "quiz",
logo: "",
noStartPage: false,
startpageType: "standard",
startpage: {
description: "",
button: "",
position: "left",
background: {
type: "none",
desktop: "",
mobile: "",
video: "",
cycle: false,
},
},
info: {
phonenumber: "",
clickable: false,
orgname: "",
site: "",
law: "",
},
meta: "",
};

@ -1,8 +1,8 @@
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import { createQuiz } from "@root/quizesV2";
import SectionWrapper from "@ui_kit/SectionWrapper"; import SectionWrapper from "@ui_kit/SectionWrapper";
import ComplexNavText from "./ComplexNavText"; import ComplexNavText from "./ComplexNavText";
import { useNavigate } from "react-router"; import { createQuiz } from "@root/quizes/actions";
import { useNavigate } from "react-router-dom";
export default function FirstQuiz() { export default function FirstQuiz() {

@ -1,88 +1,100 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import { import {
Typography, Box,
Box, Button,
Button, SxProps,
SxProps, Theme,
Theme, Typography,
useTheme, useMediaQuery,
useMediaQuery, useTheme,
} from "@mui/material"; } from "@mui/material";
import ComplexNavText from "./ComplexNavText";
import QuizCard from "./QuizCard";
import SectionWrapper from "@ui_kit/SectionWrapper"; import SectionWrapper from "@ui_kit/SectionWrapper";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import React from "react"; import React from "react";
import { quizStore } from "@root/quizes";
import FirstQuiz from "./FirstQuiz";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { createQuiz } from "@root/quizesV2"; import useSWR from "swr";
import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard";
import { setQuizes, createQuiz } from "@root/quizes/actions";
import { useQuizArray } from "@root/quizes/hooks";
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, outerContainerSx: sx,
children, children,
}: Props) { }: Props) {
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore(); useSWR("quizes", () => quizApi.getList(), {
const navigate = useNavigate(); onSuccess: setQuizes,
const theme = useTheme(); onError: error => {
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
return ( devlog("Error creating quiz", error);
<> enqueueSnackbar(`Не удалось получить квизы. ${message}`);
{Object.keys(listQuizes).length === 0 ? ( },
<FirstQuiz /> });
) : ( const quizArray = useQuizArray();
<SectionWrapper maxWidth="lg"> const navigate = useNavigate();
<ComplexNavText text1="Кабинет квизов" /> const theme = useTheme();
<Box const isMobile = useMediaQuery(theme.breakpoints.down(500));
sx={{
display: "flex", return (
justifyContent: "space-between", <>
alignItems: "center", {quizArray.length === 0 ? (
mt: "20px", <FirstQuiz />
mb: "30px", ) : (
}} <SectionWrapper maxWidth="lg">
> <ComplexNavText text1="Кабинет квизов" />
<Typography variant="h4">Мои квизы</Typography> <Box
<Button sx={{
variant="contained" display: "flex",
sx={{ justifyContent: "space-between",
padding: isMobile ? "10px" : "10px 47px", alignItems: "center",
minWidth: "44px", mt: "20px",
}} mb: "30px",
onClick={() => createQuiz(navigate)} }}
> >
{isMobile ? "+" : "Создать +"} <Typography variant="h4">Мои квизы</Typography>
</Button> <Button
</Box> variant="contained"
<Box sx={{
sx={{ padding: isMobile ? "10px" : "10px 47px",
py: "10px", minWidth: "44px",
display: "flex", }}
flexWrap: "wrap", onClick={() => createQuiz(navigate)}
gap: "40px", >
mb: "60px", {isMobile ? "+" : "Создать +"}
}} </Button>
> </Box>
{Object.values(listQuizes).map(({ id, name }) => ( <Box
<QuizCard sx={{
key={id} py: "10px",
name={name} display: "flex",
openCount={0} flexWrap: "wrap",
applicationCount={0} gap: "40px",
conversionPercent={0} mb: "60px",
onClickDelete={() => { }}
removeQuiz(id); >
}} {quizArray.map(quiz => (
onClickEdit={() => navigate(`/setting/${id}`)} <QuizCard
/> key={quiz.id}
))} quiz={quiz}
</Box> openCount={0}
{children} applicationCount={0}
</SectionWrapper> conversionPercent={0}
)} />
</> ))}
); </Box>
{children}
</SectionWrapper>
)}
</>
);
} }

@ -1,142 +1,147 @@
import {
Box,
Button,
IconButton,
Typography,
useTheme,
useMediaQuery,
} from "@mui/material";
import ChartIcon from "@icons/ChartIcon"; import ChartIcon from "@icons/ChartIcon";
import LinkIcon from "@icons/LinkIcon"; import LinkIcon from "@icons/LinkIcon";
import PencilIcon from "@icons/PencilIcon"; import PencilIcon from "@icons/PencilIcon";
import { Quiz } from "@model/quiz/quiz";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import {
Box,
Button,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { deleteQuiz } from "@root/quizes/actions";
import { useNavigate } from "react-router-dom";
interface Props { interface Props {
name: string; quiz: Quiz;
openCount?: number; openCount?: number;
applicationCount?: number; applicationCount?: number;
conversionPercent?: number; conversionPercent?: number;
onClickDelete?: () => void;
onClickEdit?: () => void;
} }
export default function QuizCard({ export default function QuizCard({
name, quiz,
openCount = 0, openCount = 0,
applicationCount = 0, applicationCount = 0,
conversionPercent = 0, conversionPercent = 0,
onClickDelete,
onClickEdit,
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const navigate = useNavigate();
return ( function handleEditClick() {
<Box navigate(`/setting/${quiz.id}`);
sx={{ }
display: "flex",
flexDirection: "column", return (
backgroundColor: "white", <Box
width: "560px", sx={{
height: "280px", display: "flex",
p: "20px", flexDirection: "column",
borderRadius: "12px", backgroundColor: "white",
boxSizing: "border-box", width: "560px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24), height: "280px",
p: "20px",
borderRadius: "12px",
boxSizing: "border-box",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525), 0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066), 0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12), 0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343), 0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`, 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}} }}
>
<Typography variant="h5">{name}</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: "10px",
gap: "10px",
}}
>
<LinkIcon bgcolor="#EEE4FC" color={theme.palette.brightPurple.main} />
<Typography color={theme.palette.grey3.main}>
быстрая ссылка ...
</Typography>
</Box>
<Box
sx={{
display: "flex",
mt: "32px",
mr: "22px",
}}
>
<Box sx={{ flex: "1 1 0" }}>
<Typography variant="h5">{openCount}</Typography>
<Typography color={theme.palette.grey3.main}>Открытий</Typography>
</Box>
<Box sx={{ flex: "1 1 0" }}>
<Typography variant="h5">{openCount}</Typography>
<Typography color={theme.palette.grey3.main}>Заявок</Typography>
</Box>
<Box sx={{ flex: "1 1 0" }}>
<Typography variant="h5">{openCount} %</Typography>
<Typography color={theme.palette.grey3.main}>Конверсия</Typography>
</Box>
</Box>
<Box
sx={{
mt: "auto",
display: "flex",
gap: isMobile ? "10px" : "20px",
}}
>
<Button
variant="contained"
sx={{
padding: "10px 39px",
}}
> >
Заявки <Typography variant="h5">{quiz.name}</Typography>
</Button> <Box
<Button sx={{
variant="outlined" display: "flex",
startIcon={<PencilIcon />} alignItems: "center",
onClick={onClickEdit} mt: "10px",
sx={{ gap: "10px",
padding: isMobile ? "10px" : "10px 20px", }}
minWidth: "unset", >
color: theme.palette.brightPurple.main, <LinkIcon bgcolor="#EEE4FC" color={theme.palette.brightPurple.main} />
"& .MuiButton-startIcon": { <Typography color={theme.palette.grey3.main}>
marginRight: isMobile ? 0 : "4px", быстрая ссылка ...
marginLeft: 0, </Typography>
}, </Box>
}} <Box
> sx={{
{isMobile ? "" : "Редактировать"} display: "flex",
</Button> mt: "32px",
<Button mr: "22px",
variant="outlined" }}
startIcon={<ChartIcon />} >
sx={{ <Box sx={{ flex: "1 1 0" }}>
minWidth: "46px", <Typography variant="h5">{openCount}</Typography>
padding: "10px 10px", <Typography color={theme.palette.grey3.main}>Открытий</Typography>
"& .MuiButton-startIcon": { </Box>
mr: 0, <Box sx={{ flex: "1 1 0" }}>
ml: 0, <Typography variant="h5">{applicationCount}</Typography>
}, <Typography color={theme.palette.grey3.main}>Заявок</Typography>
}} </Box>
/> <Box sx={{ flex: "1 1 0" }}>
<IconButton <Typography variant="h5">{conversionPercent} %</Typography>
sx={{ <Typography color={theme.palette.grey3.main}>Конверсия</Typography>
color: theme.palette.brightPurple.main, </Box>
ml: "auto", </Box>
}} <Box
onClick={onClickDelete} sx={{
> mt: "auto",
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} /> display: "flex",
</IconButton> gap: isMobile ? "10px" : "20px",
</Box> }}
</Box> >
); <Button
variant="contained"
sx={{
padding: "10px 39px",
}}
>
Заявки
</Button>
<Button
variant="outlined"
startIcon={<PencilIcon />}
onClick={handleEditClick}
sx={{
padding: isMobile ? "10px" : "10px 20px",
minWidth: "unset",
color: theme.palette.brightPurple.main,
"& .MuiButton-startIcon": {
marginRight: isMobile ? 0 : "4px",
marginLeft: 0,
},
}}
>
{isMobile ? "" : "Редактировать"}
</Button>
<Button
variant="outlined"
startIcon={<ChartIcon />}
sx={{
minWidth: "46px",
padding: "10px 10px",
"& .MuiButton-startIcon": {
mr: 0,
ml: 0,
},
}}
/>
<IconButton
sx={{
color: theme.palette.brightPurple.main,
ml: "auto",
}}
onClick={() => deleteQuiz(quiz.id)}
>
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
</IconButton>
</Box>
</Box>
);
} }

@ -1,270 +1,270 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import BackArrowIcon from "@icons/BackArrowIcon";
import { Burger } from "@icons/Burger";
import EyeIcon from "@icons/EyeIcon";
import { PenaLogoIcon } from "@icons/PenaLogoIcon";
import {
Box,
Button,
Container,
FormControl,
IconButton,
TextField,
useMediaQuery,
useTheme,
} from "@mui/material";
import { decrementCurrentStep, setQuizes } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuizStore } from "@root/quizes/store";
import CustomAvatar from "@ui_kit/Header/Avatar";
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
import PenaLogo from "@ui_kit/PenaLogo";
import Sidebar from "@ui_kit/Sidebar";
import Stepper from "@ui_kit/Stepper"; import Stepper from "@ui_kit/Stepper";
import SwitchStepPages from "@ui_kit/switchStepPages"; import SwitchStepPages from "@ui_kit/switchStepPages";
import React, { useState } from "react"; import { isAxiosError } from "axios";
import PenaLogo from "@ui_kit/PenaLogo"; import { enqueueSnackbar } from "notistack";
import { import { useState } from "react";
Box,
Button,
Container,
FormControl,
IconButton,
TextField,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import BackArrowIcon from "@icons/BackArrowIcon"; import useSWR from "swr";
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
import EyeIcon from "@icons/EyeIcon";
import CustomAvatar from "@ui_kit/Header/Avatar";
import Sidebar from "@ui_kit/Sidebar";
import { quizStore } from "@root/quizes";
import { useParams } from "react-router-dom";
import { Burger } from "@icons/Burger";
import { PenaLogoIcon } from "@icons/PenaLogoIcon";
import { SidebarMobile } from "./Sidebar/SidebarMobile"; import { SidebarMobile } from "./Sidebar/SidebarMobile";
const DESCRIPTIONS = [
"Настройка стартовой страницы",
"Задайте вопросы",
"Настройте авторезультаты",
"Оценка графа карты вопросов",
"Настройте форму контактов",
"Установите квиз",
"Запустите рекламу",
] as const;
export default function StartPage() { export default function StartPage() {
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore(); useSWR("quizes", () => quizApi.getList(), {
const params = Number(useParams().quizId); onSuccess: setQuizes,
const activeStep = listQuizes[params].step; onError: error => {
const theme = useTheme(); const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false); devlog("Error creating quiz", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const theme = useTheme();
const quiz = useCurrentQuiz();
const currentStep = useQuizStore(state => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const handleBack = () => { const quizConfig = quiz?.config;
let result = listQuizes[params].step - 1;
updateQuizesList(params, { step: result ? result : 1 });
};
return ( return (
<> <>
{/*хедер*/} {/*хедер*/}
<Container <Container
component="nav" component="nav"
maxWidth={false} maxWidth={false}
disableGutters disableGutters
sx={{
px: "16px",
display: "flex",
height: isMobile ? "51px" : "80px",
alignItems: "center",
bgcolor: isMobile ? "#333647" : "white",
borderBottom: "1px solid #E3E3E3",
zIndex: theme.zIndex.drawer + 1,
}}
>
<Link to="/" style={{ display: "flex" }}>
{isMobile ? (
<PenaLogoIcon style={{ fontSize: "39px", color: "white" }} />
) : (
<PenaLogo width={124} />
)}
</Link>
<Box
sx={{
display: isMobile ? "none" : "flex",
alignItems: "center",
ml: "37px",
}}
>
<IconButton sx={{ p: "6px" }} onClick={() => handleBack()}>
<BackArrowIcon />
</IconButton>
<FormControl fullWidth variant="standard">
<TextField
fullWidth
id="project-name"
placeholder="Название проекта окно"
sx={{
width: "270px",
"& .MuiInputBase-root": {
height: "34px",
borderRadius: "8px",
p: 0,
},
}}
inputProps={{
sx: {
height: "20px",
borderRadius: "8px",
fontSize: "16px",
lineHeight: "20px",
p: "7px",
color: "black",
"&::placeholder": {
opacity: 1,
},
},
}}
/>
</FormControl>
</Box>
{isTablet ? (
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
{isMobile ? (
<Burger
onClick={() => setMobileSidebar(!mobileSidebar)}
style={{ fontSize: "30px", color: "white", cursor: "pointer" }}
/>
) : (
<CustomAvatar
sx={{ sx={{
ml: "11px", px: "16px",
backgroundColor: theme.palette.orange.main, display: "flex",
height: "36px", height: isMobile ? "51px" : "80px",
width: "36px", alignItems: "center",
bgcolor: isMobile ? "#333647" : "white",
borderBottom: "1px solid #E3E3E3",
zIndex: theme.zIndex.drawer + 1,
}} }}
/> >
)} <Link to="/" style={{ display: "flex" }}>
</Box> {isMobile ? (
) : ( <PenaLogoIcon style={{ fontSize: "39px", color: "white" }} />
<> ) : (
<PenaLogo width={124} />
)}
</Link>
<Box
sx={{
display: isMobile ? "none" : "flex",
alignItems: "center",
ml: "37px",
}}
>
<IconButton sx={{ p: "6px" }} onClick={decrementCurrentStep}>
<BackArrowIcon />
</IconButton>
<FormControl fullWidth variant="standard">
<TextField
fullWidth
id="project-name"
placeholder="Название проекта окно"
sx={{
width: "270px",
"& .MuiInputBase-root": {
height: "34px",
borderRadius: "8px",
p: 0,
},
}}
inputProps={{
sx: {
height: "20px",
borderRadius: "8px",
fontSize: "16px",
lineHeight: "20px",
p: "7px",
color: "black",
"&::placeholder": {
opacity: 1,
},
},
}}
/>
</FormControl>
</Box>
{isTablet ? (
<Box
sx={{
display: "flex",
ml: "auto",
}}
>
{isMobile ? (
<Burger
onClick={() => setMobileSidebar(!mobileSidebar)}
style={{ fontSize: "30px", color: "white", cursor: "pointer" }}
/>
) : (
<CustomAvatar
sx={{
ml: "11px",
backgroundColor: theme.palette.orange.main,
height: "36px",
width: "36px",
}}
/>
)}
</Box>
) : (
<>
<Box
sx={{
display: "flex",
gap: "30px",
overflow: "hidden",
ml: "20px",
}}
>
<NavMenuItem text="Редактировать" isActive />
<NavMenuItem text="Заявки" />
<NavMenuItem text="Аналитика" />
<NavMenuItem text="История" />
<NavMenuItem text="Помощь" />
</Box>
<Box
sx={{
display: "flex",
ml: "auto",
gap: "15px",
}}
>
<Button
variant="outlined"
startIcon={<EyeIcon />}
sx={{
color: theme.palette.brightPurple.main,
fontSize: "14px",
lineHeight: "18px",
height: "34px",
"& .MuiButton-startIcon": {
mr: "3px",
},
}}
>
Предпросмотр
</Button>
<Button
variant="contained"
sx={{
fontSize: "14px",
lineHeight: "18px",
height: "34px",
}}
>
Опубликовать
</Button>
<CustomAvatar
sx={{
ml: "11px",
backgroundColor: theme.palette.orange.main,
height: "36px",
width: "36px",
}}
/>
</Box>
</>
)}
</Container>
<Box <Box
sx={{ sx={{
display: "flex", display: isMobile ? "block" : "flex",
gap: "30px", }}
overflow: "hidden",
ml: "20px",
}}
> >
<NavMenuItem text="Редактировать" isActive /> {isMobile ? <SidebarMobile open={mobileSidebar} /> : <Sidebar />}
<NavMenuItem text="Заявки" /> <Box
<NavMenuItem text="Аналитика" /> sx={{
<NavMenuItem text="История" /> background: theme.palette.background.default,
<NavMenuItem text="Помощь" /> width: "100%",
padding: isMobile ? "16px" : "25px",
height: "calc(100vh - 80px)",
overflow: "auto",
boxSizing: "border-box",
}}
>
{quizConfig &&
<>
<Stepper activeStep={currentStep} />
<SwitchStepPages
activeStep={currentStep}
quizType={quizConfig.type}
/>
</>
}
</Box>
{isTablet && [1, 2, 3].includes(currentStep) && (
<Box
sx={{
position: "absolute",
left: isMobile ? 0 : "230px",
bottom: 0,
width: isMobile ? "100%" : "calc(100% - 230px)",
padding: "20px 40px",
display: "flex",
justifyContent: "flex-end",
gap: "15px",
background: "#FFF",
}}
>
<Button
variant="outlined"
startIcon={<EyeIcon />}
sx={{
color: theme.palette.brightPurple.main,
fontSize: "14px",
lineHeight: "18px",
height: "34px",
"& .MuiButton-startIcon": {
mr: "3px",
},
}}
>
Предпросмотр
</Button>
<Button
variant="contained"
sx={{
fontSize: "14px",
lineHeight: "18px",
height: "34px",
}}
>
Опубликовать
</Button>
</Box>
)}
</Box> </Box>
<Box </>
sx={{ );
display: "flex",
ml: "auto",
gap: "15px",
}}
>
<Button
variant="outlined"
startIcon={<EyeIcon />}
sx={{
color: theme.palette.brightPurple.main,
fontSize: "14px",
lineHeight: "18px",
height: "34px",
"& .MuiButton-startIcon": {
mr: "3px",
},
}}
>
Предпросмотр
</Button>
<Button
variant="contained"
sx={{
fontSize: "14px",
lineHeight: "18px",
height: "34px",
}}
>
Опубликовать
</Button>
<CustomAvatar
sx={{
ml: "11px",
backgroundColor: theme.palette.orange.main,
height: "36px",
width: "36px",
}}
/>
</Box>
</>
)}
</Container>
<Box
sx={{
display: isMobile ? "block" : "flex",
}}
>
{isMobile ? <SidebarMobile open={mobileSidebar} /> : <Sidebar />}
<Box
sx={{
background: theme.palette.background.default,
width: "100%",
padding: isMobile ? "16px" : "25px",
height: "calc(100vh - 80px)",
overflow: "auto",
boxSizing: "border-box",
}}
>
<Stepper
activeStep={activeStep}
desc={DESCRIPTIONS[activeStep - 1]}
/>
<SwitchStepPages
activeStep={activeStep}
quizType={listQuizes[params].config.type}
startpage={listQuizes[params].startpage}
createResult={listQuizes[params].createResult}
/>
</Box>
{isTablet && activeStep === 1 && (
<Box
sx={{
position: "absolute",
left: isMobile ? 0 : "230px",
bottom: 0,
width: isMobile ? "100%" : "calc(100% - 230px)",
padding: "20px 40px",
display: "flex",
justifyContent: "flex-end",
gap: "15px",
background: "#FFF",
}}
>
<Button
variant="outlined"
startIcon={<EyeIcon />}
sx={{
color: theme.palette.brightPurple.main,
fontSize: "14px",
lineHeight: "18px",
height: "34px",
"& .MuiButton-startIcon": {
mr: "3px",
},
}}
>
Предпросмотр
</Button>
<Button
variant="contained"
sx={{
fontSize: "14px",
lineHeight: "18px",
height: "34px",
}}
>
Опубликовать
</Button>
</Box>
)}
</Box>
</>
);
} }

@ -1,71 +1,71 @@
import { Box, Button, useTheme } from "@mui/material"; import { Box, Button } from "@mui/material";
import CreationCard from "@ui_kit/CreationCard"; import CreationCard from "@ui_kit/CreationCard";
import { useNavigate } from "react-router-dom";
import quizCreationImage1 from "../../assets/quiz-creation-1.png"; import quizCreationImage1 from "../../assets/quiz-creation-1.png";
import quizCreationImage2 from "../../assets/quiz-creation-2.png"; import quizCreationImage2 from "../../assets/quiz-creation-2.png";
import { useParams } from "react-router-dom"; import { setQuizType } from "@root/quizes/actions";
import { quizStore } from "@root/quizes"; import { useCurrentQuiz } from "@root/quizes/hooks";
export default function StepOne() { export default function StepOne() {
const params = Number(useParams().quizId); const navigate = useNavigate();
const theme = useTheme(); const quiz = useCurrentQuiz();
const config = quiz?.config;
const { listQuizes, updateQuizesList } = quizStore(); if (!config) return null;
return (
<Box return (
sx={{ <Box
overflowX: "scroll", sx={{
padding: "0 5px 15px", overflowX: "scroll",
"&::-webkit-scrollbar": { width: 0 }, padding: "0 5px 15px",
}} "&::-webkit-scrollbar": { width: 0 },
> }}
<Box
sx={{
minWidth: "720px",
display: "flex",
gap: "20px",
mt: "60px",
}}
>
<Button
variant="text"
data-cy="create-quiz-card"
onClick={() => {
let SPageClone = listQuizes[params].config;
SPageClone.type = "quize";
updateQuizesList(params, { config: SPageClone });
}}
> >
<CreationCard <Box
header="Создание квиз-опроса" sx={{
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас" minWidth: "720px",
image={quizCreationImage1} display: "flex",
border={ gap: "20px",
listQuizes[params].config.type === "quize" mt: "60px",
? "1px solid #7E2AEA" }}
: "none" >
} <Button
/> variant="text"
</Button> data-cy="create-quiz-card"
<Button onClick={() => {
variant="text" setQuizType(quiz.id, "quiz", navigate);
onClick={() => { }}
let SPageClone = listQuizes[params].config; >
SPageClone.type = "form"; <CreationCard
updateQuizesList(params, { config: SPageClone }); header="Создание квиз-опроса"
}} text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
> image={quizCreationImage1}
<CreationCard border={
header="Создание анкеты" config.type === "quiz"
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас" ? "1px solid #7E2AEA"
image={quizCreationImage2} : "none"
border={ }
listQuizes[params].config.type === "form" />
? "1px solid #7E2AEA" </Button>
: "none" <Button
} variant="text"
/> onClick={() => {
</Button> setQuizType(quiz.id,"form", navigate);
</Box> }}
</Box> >
); <CreationCard
header="Создание анкеты"
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
image={quizCreationImage2}
border={
config.type === "form"
? "1px solid #7E2AEA"
: "none"
}
/>
</Button>
</Box>
</Box>
);
} }

@ -1,93 +1,99 @@
import { import {
Box, Box,
Button, Button,
Typography, Typography,
useTheme, useMediaQuery,
useMediaQuery, useTheme,
} from "@mui/material"; } from "@mui/material";
import CardWithImage from "./CardWithImage"; import { setQuizStartpageType } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useNavigate } from "react-router-dom";
import cardImage1 from "../../assets/card-1.png"; import cardImage1 from "../../assets/card-1.png";
import cardImage2 from "../../assets/card-2.png"; import cardImage2 from "../../assets/card-2.png";
import cardImage3 from "../../assets/card-3.png"; import cardImage3 from "../../assets/card-3.png";
import { quizStore } from "@root/quizes"; import CardWithImage from "./CardWithImage";
import { useParams } from "react-router-dom";
export default function Steptwo() { export default function Steptwo() {
const params = Number(useParams().quizId); const navigate = useNavigate();
const { listQuizes, updateQuizesList } = quizStore(); const theme = useTheme();
const theme = useTheme(); const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300)); const quiz = useCurrentQuiz();
return ( const config = quiz?.config;
<Box sx={{ mt: "60px" }}>
<Typography variant="h5">Стартовая страница</Typography> if (!config) return null;
<Box
sx={{ return (
overflowX: "scroll", <Box sx={{ mt: "60px" }}>
paddingBottom: "15px", <Typography variant="h5">Стартовая страница</Typography>
"&::-webkit-scrollbar": { width: 0 }, <Box
}} sx={{
> overflowX: "scroll",
<Box paddingBottom: "15px",
sx={{ "&::-webkit-scrollbar": { width: 0 },
minWidth: "950px", }}
display: "flex", >
gap: "20px", <Box
mt: "40px", sx={{
padding: isSmallMonitor ? "0 15px 15px" : 0, minWidth: "950px",
}} display: "flex",
> gap: "20px",
<Button mt: "40px",
variant="text" padding: isSmallMonitor ? "0 15px 15px" : 0,
data-cy="select-quiz-layout-standard" }}
onClick={() => { >
updateQuizesList(params, { startpage: "standard" }); <Button
}} variant="text"
> data-cy="select-quiz-layout-standard"
<CardWithImage onClick={() => {
image={cardImage1} setQuizStartpageType(quiz.id, "standard", navigate);
text="Standard" }}
border={ >
listQuizes[params].startpage === "standard" <CardWithImage
? "1px solid #7E2AEA" image={cardImage1}
: "none" text="Standard"
} border={
/> config.startpageType === "standard"
</Button> ? "1px solid #7E2AEA"
<Button : "none"
variant="text" }
onClick={() => { />
updateQuizesList(params, { startpage: "expanded" }); </Button>
}} <Button
> variant="text"
<CardWithImage onClick={() => {
image={cardImage2} setQuizStartpageType(quiz.id, "expanded", navigate);
text="Expanded" }}
border={ >
listQuizes[params].startpage === "expanded" <CardWithImage
? "1px solid #7E2AEA" image={cardImage2}
: "none" text="Expanded"
} border={
/> config.startpageType === "expanded"
</Button> ? "1px solid #7E2AEA"
<Button : "none"
variant="text" }
onClick={() => { />
updateQuizesList(params, { startpage: "centered" }); </Button>
}} <Button
> variant="text"
<CardWithImage onClick={() => {
image={cardImage3} setQuizStartpageType(quiz.id, "centered", navigate);
text="Centered" }}
border={ >
listQuizes[params].startpage === "centered" <CardWithImage
? "1px solid #7E2AEA" image={cardImage3}
: "none" text="Centered"
} border={
/> config.startpageType === "centered"
</Button> ? "1px solid #7E2AEA"
: "none"
}
/>
</Button>
</Box>
</Box>
</Box> </Box>
</Box> );
</Box>
);
} }

@ -80,7 +80,7 @@ export const setQuestionFieldOptimistic = async <T extends keyof Question>(
} catch (error) { } catch (error) {
if (isAxiosCanceledError(error)) return; if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, question: question, currentUpdatedQuestion }); devlog("Error editing question", { error, question, currentUpdatedQuestion });
enqueueSnackbar("Не удалось сохранить вопрос"); enqueueSnackbar("Не удалось сохранить вопрос");
if (!savedOriginalQuestion) { if (!savedOriginalQuestion) {
devlog("Cannot rollback question"); devlog("Cannot rollback question");

@ -0,0 +1,195 @@
import { quizApi } from "@api/quiz";
import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { quizToEditQuizRequest } from "@model/quiz/edit";
import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz";
import { QuizConfig, QuizSetupStep, maxQuizSetupSteps } from "@model/quizSettings";
import { produce } from "immer";
import { enqueueSnackbar } from "notistack";
import { NavigateFunction } from "react-router-dom";
import { QuizStore, useQuizStore } from "./store";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
state.quizById = {};
if (quizes === null) return;
quizes.forEach(quiz => state.quizById[quiz.id] = rawQuizToQuiz(quiz));
}, {
type: "setQuizes",
quizes,
});
export const setQuiz = (quiz: Quiz) => setProducedState(state => {
state.quizById[quiz.id] = quiz;
}, {
type: "setQuiz",
quiz,
});
export const removeQuiz = (quizId: number) => setProducedState(state => {
delete state.quizById[quizId];
}, {
type: "removeQuiz",
quizId,
});
export const setQuizField = <T extends keyof Quiz>(
quizId: number,
field: T,
value: Quiz[T],
) => setProducedState(state => {
const quiz = state.quizById[quizId];
if (!quiz) return;
quiz[field] = value;
}, {
type: "setQuizField",
quizId,
field,
value,
});
export const updateQuiz = (
quizId: number,
updateFn: (quiz: Quiz) => void,
) => setProducedState(state => {
const quiz = state.quizById[quizId];
if (!quiz) return;
updateFn(quiz);
}, {
type: "updateQuiz",
quizId,
updateFn: updateFn.toString(),
});
export const incrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.min(
maxQuizSetupSteps, state.currentStep + 1
) as QuizSetupStep;
});
export const decrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.max(
1, state.currentStep - 1
) as QuizSetupStep;
});
export const setCurrentStep = (step: number) => setProducedState(state => {
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps, step)) as QuizSetupStep;
});
export const createQuiz = async (navigate: NavigateFunction) => {
try {
const quiz = await quizApi.create({
name: "Quiz name",
description: "Quiz description",
});
setQuiz(rawQuizToQuiz(quiz));
navigate(`/setting/${quiz.id}`);
} catch (error) {
devlog("Error creating quiz", error);
const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось создать квиз. ${message}`);
}
};
export const deleteQuiz = async (quizId: number) => {
try {
await quizApi.delete(quizId);
removeQuiz(quizId);
} catch (error) {
devlog("Error deleting quiz", error);
const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
}
};
export const setQuizType = (
quizId: number,
quizType: QuizConfig["type"],
navigate: NavigateFunction,
) => {
updateQuizWithFnOptimistic(
quizId,
quiz => {
quiz.config.type = quizType;
},
navigate,
);
incrementCurrentStep();
};
export const setQuizStartpageType = (
quizId: number,
startpageType: QuizConfig["startpageType"],
navigate: NavigateFunction,
) => {
updateQuizWithFnOptimistic(
quizId,
quiz => {
quiz.config.startpageType = startpageType;
},
navigate,
);
incrementCurrentStep();
};
let savedOriginalQuiz: Quiz | null = null;
let controller: AbortController | null = null;
export const updateQuizWithFnOptimistic = async (
quizId: number,
updateFn: (quiz: Quiz) => void,
navigate: NavigateFunction,
rollbackOnError = true,
) => {
const quiz = useQuizStore.getState().quizById[quizId] ?? null;
if (!quiz) return;
const currentUpdatedQuiz = produce(quiz, updateFn);
controller?.abort();
controller = new AbortController();
savedOriginalQuiz ??= quiz;
setQuiz(currentUpdatedQuiz);
try {
const { updated } = await quizApi.edit(quizToEditQuizRequest(currentUpdatedQuiz), controller.signal);
// await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected")));
setQuizField(quiz.id, "version", updated);
navigate(`/setting/${quizId}`, { replace: true });
controller = null;
savedOriginalQuiz = null;
} catch (error) {
if (isAxiosCanceledError(error)) return;
devlog("Error editing quiz", { error, quiz, currentUpdatedQuiz });
enqueueSnackbar("Не удалось сохранить настройки квиза");
if (rollbackOnError) {
if (!savedOriginalQuiz) {
devlog("Cannot rollback quiz");
throw new Error("Cannot rollback quiz");
}
setQuiz(savedOriginalQuiz);
}
controller = null;
savedOriginalQuiz = null;
}
};
function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuizStore) => void,
action?: A,
) {
useQuizStore.setState(state => produce(state, recipe), false, action);
}

@ -0,0 +1,17 @@
import { Quiz } from "@model/quiz/quiz";
import { useParams } from "react-router-dom";
import { useQuizStore } from "./store";
export function useQuizArray(): Quiz[] {
const quizes = useQuizStore(state => state.quizById);
return Object.values(quizes).flatMap(quiz => quiz ? [quiz] : []);
}
export function useCurrentQuiz() {
const quizId = parseInt(useParams().quizId ?? "");
const quiz = useQuizStore(state => state.quizById[quizId]);
return quiz;
}

@ -0,0 +1,25 @@
import { Quiz } from "@model/quiz/quiz";
import { QuizSetupStep } from "@model/quizSettings";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
export type QuizStore = {
quizById: Record<number, Quiz | undefined>;
currentStep: QuizSetupStep;
};
const initialState: QuizStore = {
quizById: {},
currentStep: 1,
};
export const useQuizStore = create<QuizStore>()(
devtools(
() => initialState,
{
name: "QuizStore",
enabled: process.env.NODE_ENV === "development",
}
)
);

@ -1,103 +0,0 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import { produce } from "immer";
import { BackendQuiz } from "model/quiz/quiz";
import { enqueueSnackbar } from "notistack";
import { NavigateFunction } from "react-router";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
type QuizStore = {
quizes: Record<number, BackendQuiz | undefined>;
};
const initialState: QuizStore = {
quizes: {},
};
export const useQuizStore = create<QuizStore>()(
devtools(
() => initialState,
{
name: "QuizStore",
enabled: process.env.NODE_ENV === "development",
}
)
);
export const setQuizes = (quizes: QuizStore["quizes"]) => useQuizStore.setState({ quizes });
export const setQuiz = (quiz: BackendQuiz) => setProducedState(state => {
state.quizes[quiz.id] = quiz;
}, {
type: "setQuiz",
quiz,
});
export const removeQuiz = (quizId: number) => setProducedState(state => {
delete state.quizes[quizId];
}, {
type: "removeQuiz",
quizId,
});
export const setQuizField = <T extends keyof BackendQuiz>(
quizId: number,
field: T,
value: BackendQuiz[T],
) => setProducedState(state => {
const quiz = state.quizes[quizId];
if (!quiz) return;
quiz[field] = value;
}, {
type: "setQuizField",
quizId,
field,
value,
});
export const updateQuiz = (
quizId: number,
updateFn: (quiz: BackendQuiz) => void,
) => setProducedState(state => {
const quiz = state.quizes[quizId];
if (!quiz) return;
updateFn(quiz);
}, {
type: "updateQuiz",
quizId,
updateFn: updateFn.toString(),
});
export const createQuiz = async (navigate: NavigateFunction) => {
try {
const quiz = await quizApi.create();
setQuiz(quiz);
navigate(`/settings/${quiz.id}`);
} catch (error) {
devlog("Error creating quiz", error);
enqueueSnackbar("Не удалось создать квиз");
}
};
export const deleteQuiz = async (quizId: number) => {
try {
await quizApi.delete(quizId);
removeQuiz(quizId);
} catch (error) {
devlog("Error deleting quiz", error);
enqueueSnackbar("Не удалось удалить квиз");
}
};
function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuizStore) => void,
action?: A,
) {
useQuizStore.setState(state => produce(state, recipe), false, action);
}

@ -1,12 +1,12 @@
import { Box, Button, LinearProgress, Paper, Typography } from "@mui/material"; import { Box, Button, LinearProgress, Paper, Typography } from "@mui/material";
import { questionStore } from "@root/questions"; import { questionStore } from "@root/questions";
import { import {
decrementCurrentQuestionIndex, decrementCurrentQuestionIndex,
incrementCurrentQuestionIndex, incrementCurrentQuestionIndex,
useQuizPreviewStore, useQuizPreviewStore,
} from "@root/quizPreview"; } from "@root/quizPreview";
import { DefiniteQuestionType } from "model/questionTypes/shared"; import { AnyQuizQuestion } from "model/questionTypes/shared";
import { FC, useEffect } from "react"; import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Date from "./QuizPreviewQuestionTypes/Date"; import Date from "./QuizPreviewQuestionTypes/Date";
@ -21,144 +21,140 @@ import Text from "./QuizPreviewQuestionTypes/Text";
import Variant from "./QuizPreviewQuestionTypes/Variant"; import Variant from "./QuizPreviewQuestionTypes/Variant";
import Varimg from "./QuizPreviewQuestionTypes/Varimg"; import Varimg from "./QuizPreviewQuestionTypes/Varimg";
const QuestionPreviewComponentByType: Record<DefiniteQuestionType, FC<any>> = {
variant: Variant,
images: Images,
varimg: Varimg,
emoji: Emoji,
text: Text,
select: Select,
date: Date,
number: Number,
file: File,
page: Page,
rating: Rating,
};
export default function QuizPreviewLayout() { export default function QuizPreviewLayout() {
const quizId = useParams().quizId ?? 0; const quizId = useParams().quizId ?? 0;
const listQuestions = questionStore((state) => state.listQuestions); const listQuestions = questionStore((state) => state.listQuestions);
const currentQuizStep = useQuizPreviewStore( const currentQuizStep = useQuizPreviewStore(
(state) => state.currentQuestionIndex (state) => state.currentQuestionIndex
); );
const quizQuestions = listQuestions[quizId] ?? []; const quizQuestions = listQuestions[quizId] ?? [];
const nonDeletedQuizQuestions = quizQuestions.filter( const nonDeletedQuizQuestions = quizQuestions.filter(
(question) => !question.deleted (question) => !question.deleted
); );
const maxCurrentQuizStep = const maxCurrentQuizStep =
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0; nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor( const currentProgress = Math.floor(
(currentQuizStep / maxCurrentQuizStep) * 100 (currentQuizStep / maxCurrentQuizStep) * 100
); );
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep]; const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
const QuestionComponent = currentQuestion
? QuestionPreviewComponentByType[
currentQuestion.type as DefiniteQuestionType
]
: null;
const questionElement = QuestionComponent ? ( useEffect(
<QuestionComponent key={currentQuestion.id} question={currentQuestion} /> function resetCurrentQuizStep() {
) : null; if (currentQuizStep > maxCurrentQuizStep) {
decrementCurrentQuestionIndex();
}
},
[currentQuizStep, maxCurrentQuizStep]
);
useEffect( return (
function resetCurrentQuizStep() { <Paper
if (currentQuizStep > maxCurrentQuizStep) { className="quiz-preview-draghandle"
decrementCurrentQuestionIndex(); data-cy="quiz-preview-layout"
} sx={{
}, height: "100%",
[currentQuizStep, maxCurrentQuizStep] display: "flex",
); flexDirection: "column",
flexGrow: 1,
return ( borderRadius: "12px",
<Paper pointerEvents: "auto",
className="quiz-preview-draghandle" }}
data-cy="quiz-preview-layout"
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
}}
>
<Box
sx={{
p: "16px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
"&::-webkit-scrollbar": { width: 0 },
}}
>
{questionElement}
</Box>
<Box
sx={{
mt: "auto",
p: "16px",
display: "flex",
borderTop: "1px solid #E3E3E3",
alignItems: "center",
}}
>
<Box
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 1,
}}
> >
<Typography> <Box
{nonDeletedQuizQuestions.length > 0 sx={{
? `Вопрос ${currentQuizStep + 1} из ${ p: "16px",
nonDeletedQuizQuestions.length whiteSpace: "break-spaces",
}` overflowY: "auto",
: "Нет вопросов"} flexGrow: 1,
</Typography> "&::-webkit-scrollbar": { width: 0 },
{nonDeletedQuizQuestions.length > 0 && ( }}
<LinearProgress >
variant="determinate" <QuestionPreviewComponent question={currentQuestion} />
value={currentProgress} </Box>
sx={{ <Box
"&.MuiLinearProgress-colorPrimary": { sx={{
backgroundColor: "fadePurple.main", mt: "auto",
}, p: "16px",
"& .MuiLinearProgress-barColorPrimary": { display: "flex",
backgroundColor: "brightPurple.main", borderTop: "1px solid #E3E3E3",
}, alignItems: "center",
}} }}
/> >
)} <Box
</Box> sx={{
<Box flexGrow: 1,
sx={{ display: "flex",
ml: 2, flexDirection: "column",
display: "flex", gap: 1,
gap: 1, }}
}} >
> <Typography>
<Button {nonDeletedQuizQuestions.length > 0
variant="outlined" ? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length
onClick={decrementCurrentQuestionIndex} }`
disabled={currentQuizStep === 0} : "Нет вопросов"}
sx={{ px: 1, minWidth: 0 }} </Typography>
> {nonDeletedQuizQuestions.length > 0 && (
<ArrowLeft /> <LinearProgress
</Button> variant="determinate"
<Button value={currentProgress}
variant="contained" sx={{
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)} "&.MuiLinearProgress-colorPrimary": {
disabled={currentQuizStep >= maxCurrentQuizStep} backgroundColor: "fadePurple.main",
> },
Далее "& .MuiLinearProgress-barColorPrimary": {
</Button> backgroundColor: "brightPurple.main",
</Box> },
</Box> }}
</Paper> />
); )}
</Box>
<Box
sx={{
ml: 2,
display: "flex",
gap: 1,
}}
>
<Button
variant="outlined"
onClick={decrementCurrentQuestionIndex}
disabled={currentQuizStep === 0}
sx={{ px: 1, minWidth: 0 }}
>
<ArrowLeft />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)}
disabled={currentQuizStep >= maxCurrentQuizStep}
>
Далее
</Button>
</Box>
</Box>
</Paper>
);
}
function QuestionPreviewComponent({ question }: {
question: AnyQuizQuestion;
}) {
switch (question.type) {
case "variant": return <Variant question={question} />;
case "images": return <Images question={question} />;
case "varimg": return <Varimg question={question} />;
case "emoji": return <Emoji question={question} />;
case "text": return <Text question={question} />;
case "select": return <Select question={question} />;
case "date": return <Date question={question} />;
case "number": return <Number question={question} />;
case "file": return <File question={question} />;
case "page": return <Page question={question} />;
case "rating": return <Rating question={question} />;
default: throw new Error("Unknown question type");
}
} }

@ -1,12 +1,12 @@
import { useState } from "react"; import { useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { import {
Container, Container,
Box, Box,
useTheme, useTheme,
List, List,
Typography, Typography,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { quizStore } from "@root/quizes"; import { quizStore } from "@root/quizes";
@ -24,156 +24,157 @@ import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
import GearIcon from "@icons/GearIcon"; import GearIcon from "@icons/GearIcon";
import LayoutIcon from "@icons/LayoutIcon"; import LayoutIcon from "@icons/LayoutIcon";
import MenuItem from "./MenuItem"; import MenuItem from "./MenuItem";
import { quizSetupSteps } from "@model/quizSettings";
import { useQuizStore } from "@root/quizes/store";
import { setCurrentStep } from "@root/quizes/actions";
const createQuizMenuItems = [ const createQuizMenuItems = [
[LayoutIcon, "Стартовая страница"], [LayoutIcon, "Стартовая страница"],
[QuestionIcon, "Вопросы"], [QuestionIcon, "Вопросы"],
[ChartPieIcon, "Результаты"], [ChartPieIcon, "Результаты"],
[QuestionsMapIcon, "Карта вопросов"], [QuestionsMapIcon, "Карта вопросов"],
[ContactBookIcon, "Форма контактов"], [ContactBookIcon, "Форма контактов"],
[FlowArrowIcon, "Установка квиза"], [FlowArrowIcon, "Установка квиза"],
[MegaphoneIcon, "Запуск рекламы"], [MegaphoneIcon, "Запуск рекламы"],
] as const; ] as const;
const quizSettingsMenuItems = [ const quizSettingsMenuItems = [
[TagIcon, "Дополнения"], [TagIcon, "Дополнения"],
[PencilCircleIcon, "Дизайн"], [PencilCircleIcon, "Дизайн"],
[PuzzlePieceIcon, "Интеграции"], [PuzzlePieceIcon, "Интеграции"],
[GearIcon, "Настройки"], [GearIcon, "Настройки"],
] as const; ] as const;
export default function Sidebar() { export default function Sidebar() {
const theme = useTheme(); const theme = useTheme();
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false); const [isMenuCollapsed, setIsMenuCollapsed] = useState(false);
const [progress, setProgress] = useState<number>(1 / 7); const [progress, setProgress] = useState<number>(1 / 7);
const quizId = Number(useParams().quizId); const currentStep = useQuizStore(state => state.currentStep);
const { listQuizes, updateQuizesList } = quizStore();
const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev); const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev);
return (
<Box return (
sx={{ <Box
backgroundColor: theme.palette.lightPurple.main,
minWidth: isMenuCollapsed ? "80px" : "230px",
width: isMenuCollapsed ? "80px" : "230px",
height: "calc(100vh - 80px)",
display: "flex",
flexDirection: "column",
py: "19px",
transitionProperty: "width, min-width",
transitionDuration: "200ms",
overflow: "hidden",
whiteSpace: "nowrap",
boxSizing: "border-box",
}}
>
<Box
sx={{
display: "flex",
pl: isMenuCollapsed ? undefined : "16px",
pr: isMenuCollapsed ? undefined : "8px",
mb: isMenuCollapsed ? "5px" : undefined,
alignItems: "center",
justifyContent: isMenuCollapsed ? "center" : undefined,
}}
>
{!isMenuCollapsed && (
<Typography
sx={{ sx={{
fontSize: "14px", backgroundColor: theme.palette.lightPurple.main,
lineHeight: "20px", minWidth: isMenuCollapsed ? "80px" : "230px",
fontWeight: 500, width: isMenuCollapsed ? "80px" : "230px",
color: theme.palette.grey2.main, height: "calc(100vh - 80px)",
display: "flex",
flexDirection: "column",
py: "19px",
transitionProperty: "width, min-width",
transitionDuration: "200ms",
overflow: "hidden",
whiteSpace: "nowrap",
boxSizing: "border-box",
}} }}
>
Создание квиза
</Typography>
)}
<IconButton
onClick={handleMenuCollapseToggle}
sx={{ ml: isMenuCollapsed ? undefined : "auto" }}
> >
<CollapseMenuIcon <Box
height="16px" sx={{
width="16px" display: "flex",
color={theme.palette.grey2.main} pl: isMenuCollapsed ? undefined : "16px",
transform={isMenuCollapsed ? "rotate(180deg)" : ""} pr: isMenuCollapsed ? undefined : "8px",
/> mb: isMenuCollapsed ? "5px" : undefined,
</IconButton> alignItems: "center",
</Box> justifyContent: isMenuCollapsed ? "center" : undefined,
<List disablePadding> }}
{createQuizMenuItems.map((menuItem, index) => { >
const Icon = menuItem[0]; {!isMenuCollapsed && (
return ( <Typography
<MenuItem sx={{
onClick={() => { fontSize: "14px",
updateQuizesList(quizId, { step: index + 1 }); lineHeight: "20px",
}} fontWeight: 500,
key={menuItem[1]} color: theme.palette.grey2.main,
text={menuItem[1]} }}
isCollapsed={isMenuCollapsed} >
isActive={listQuizes[quizId].step === index + 1} Создание квиза
icon={ </Typography>
<Icon )}
color={ <IconButton
listQuizes[quizId].step === index + 1 onClick={handleMenuCollapseToggle}
? theme.palette.brightPurple.main sx={{ ml: isMenuCollapsed ? undefined : "auto" }}
: isMenuCollapsed >
? "white" <CollapseMenuIcon
: theme.palette.grey2.main height="16px"
} width="16px"
height={isMenuCollapsed ? "35px" : "24px"} color={theme.palette.grey2.main}
width={isMenuCollapsed ? "35px" : "24px"} transform={isMenuCollapsed ? "rotate(180deg)" : ""}
/> />
} </IconButton>
/> </Box>
); <List disablePadding>
})} {createQuizMenuItems.map((menuItem, index) => {
</List> const Icon = menuItem[0];
{!isMenuCollapsed && ( return (
<Typography <MenuItem
sx={{ onClick={() => setCurrentStep(index + 1)}
px: "16px", key={menuItem[1]}
mt: "16px", text={menuItem[1]}
mb: "11px", isCollapsed={isMenuCollapsed}
fontSize: "14px", isActive={quizSetupSteps[currentStep].displayStep === index + 1}
lineHeight: "20px", icon={
fontWeight: 500, <Icon
color: theme.palette.grey2.main, color={
}} quizSetupSteps[currentStep].displayStep === index + 1
> ? theme.palette.brightPurple.main
Настройки квиза : isMenuCollapsed
</Typography> ? "white"
)} : theme.palette.grey2.main
<List disablePadding> }
{quizSettingsMenuItems.map((menuItem, index) => { height={isMenuCollapsed ? "35px" : "24px"}
const Icon = menuItem[0]; width={isMenuCollapsed ? "35px" : "24px"}
const totalIndex = index + createQuizMenuItems.length; />
const isActive = listQuizes[quizId].step === totalIndex + 1; }
return ( />
<MenuItem );
onClick={() => updateQuizesList(quizId, { step: totalIndex + 1 })} })}
key={menuItem[1]} </List>
text={menuItem[1]} {!isMenuCollapsed && (
isActive={isActive} <Typography
isCollapsed={isMenuCollapsed} sx={{
icon={ px: "16px",
<Icon mt: "16px",
color={ mb: "11px",
isActive fontSize: "14px",
? theme.palette.brightPurple.main lineHeight: "20px",
: isMenuCollapsed fontWeight: 500,
? "white" color: theme.palette.grey2.main,
: theme.palette.grey2.main }}
} >
height={isMenuCollapsed ? "35px" : "24px"} Настройки квиза
width={isMenuCollapsed ? "35px" : "24px"} </Typography>
/> )}
} {/* <List disablePadding> // TODO
/> {quizSettingsMenuItems.map((menuItem, index) => {
); const Icon = menuItem[0];
})} const totalIndex = index + createQuizMenuItems.length;
</List> const isActive = listQuizes[quizId].step === totalIndex + 1;
</Box> return (
); <MenuItem
onClick={() => updateQuizesList(quizId, { step: totalIndex + 1 })}
key={menuItem[1]}
text={menuItem[1]}
isActive={isActive}
isCollapsed={isMenuCollapsed}
icon={
<Icon
color={
isActive
? theme.palette.brightPurple.main
: isMenuCollapsed
? "white"
: theme.palette.grey2.main
}
height={isMenuCollapsed ? "35px" : "24px"}
width={isMenuCollapsed ? "35px" : "24px"}
/>
}
/>
);
})}
</List> */}
</Box>
);
} }

@ -1,16 +1,13 @@
import * as React from "react";
import MobileStepper from "@mui/material/MobileStepper";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import MobileStepper from "@mui/material/MobileStepper";
import { QuizSetupStep, maxDisplayQuizSetupSteps, quizSetupSteps } from "@model/quizSettings";
interface Props { interface Props {
desc?: string; activeStep: QuizSetupStep;
activeStep?: number;
steps?: number;
} }
export default function ProgressMobileStepper({ export default function ProgressMobileStepper({
desc, activeStep,
activeStep = 1,
steps = 8,
}: Props) { }: Props) {
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
@ -31,9 +28,9 @@ export default function ProgressMobileStepper({
> >
<MobileStepper <MobileStepper
variant="progress" variant="progress"
steps={steps} steps={maxDisplayQuizSetupSteps}
position="static" position="static"
activeStep={activeStep} activeStep={activeStep - 1}
sx={{ sx={{
width: "100%", width: "100%",
flexGrow: 1, flexGrow: 1,
@ -55,9 +52,9 @@ export default function ProgressMobileStepper({
sx={{ fontWeight: 400, fontSize: "12px", lineHeight: "14.22px" }} sx={{ fontWeight: 400, fontSize: "12px", lineHeight: "14.22px" }}
> >
{" "} {" "}
Шаг {activeStep} из {steps - 1} Шаг {quizSetupSteps[activeStep].displayStep} из {maxDisplayQuizSetupSteps}
</Typography> </Typography>
<Typography>{desc}</Typography> <Typography>{quizSetupSteps[activeStep].text}</Typography>
</Box> </Box>
</Box> </Box>
); );

@ -1,49 +1,37 @@
import * as React from "react"; import { QuizSetupStep } from "@model/quizSettings";
import StepOne from "../pages/startPage/stepOne"; import { notReachable } from "../utils/notReachable";
import Steptwo from "../pages/startPage/steptwo";
import StartPageSettings from "../pages/startPage/StartPageSettings";
import QuestionsPage from "../pages/Questions/QuestionsPage";
import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage";
import ContactFormPage from "../pages/ContactFormPage/ContactFormPage"; import ContactFormPage from "../pages/ContactFormPage/ContactFormPage";
import InstallQuiz from "../pages/InstallQuiz/InstallQuiz"; import InstallQuiz from "../pages/InstallQuiz/InstallQuiz";
import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage";
import QuestionsPage from "../pages/Questions/QuestionsPage";
import { QuestionsMap } from "../pages/QuestionsMap";
import { Result } from "../pages/Result/Result"; import { Result } from "../pages/Result/Result";
import { Setting } from "../pages/Result/Setting"; import { Setting } from "../pages/Result/Setting";
import { QuestionsMap } from "../pages/QuestionsMap"; import StartPageSettings from "../pages/startPage/StartPageSettings";
import StepOne from "../pages/startPage/stepOne";
import Steptwo from "../pages/startPage/steptwo";
interface Props { interface Props {
activeStep: number; activeStep: QuizSetupStep;
quizType: string; quizType: string;
startpage: string;
createResult: boolean;
} }
export default function SwitchStepPages({ export default function SwitchStepPages({
activeStep = 1, activeStep = 1,
quizType, quizType,
startpage,
createResult,
}: Props) { }: Props) {
switch (activeStep) { switch (activeStep) {
case 1: case 1: return <StepOne />;
if (!quizType) return <StepOne />; case 2: return <Steptwo />;
if (!startpage) return <Steptwo />; case 3: return <StartPageSettings />;
return <StartPageSettings />; case 4: return quizType === "form" ? <QuestionsPage /> : <FormQuestionsPage />;
case 2: case 5: return <Result />;
if (quizType === "form") return <FormQuestionsPage />; case 6: return <Setting />;
return <QuestionsPage />; case 7: return <QuestionsMap />;
case 3: case 8: return <ContactFormPage />;
if (!createResult) return <Result />; case 9: return <InstallQuiz />;
return <Setting />; case 10: return <>Реклама</>;
case 4: default: return notReachable(activeStep);
return <QuestionsMap />; }
case 5:
return <ContactFormPage />;
case 6:
return <InstallQuiz />;
case 7:
return <>Реклама</>;
default:
return <></>;
}
} }

@ -0,0 +1,3 @@
export function notReachable(_: never): never {
throw new Error(`Shouldn't reach here: ${_}`);
}

@ -13,6 +13,9 @@
], ],
"@api/*": [ "@api/*": [
"./api/*" "./api/*"
],
"@model/*": [
"./model/*"
] ]
} }
} }

652
yarn.lock

File diff suppressed because it is too large Load Diff