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,6 +21,7 @@ const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeTe
const root = createRoot(document.getElementById("root")!); const root = createRoot(document.getElementById("root")!);
root.render( root.render(
<SWRConfig value={{ revalidateOnFocus: false, shouldRetryOnError: false }}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}> <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
@ -30,4 +32,5 @@ root.render(
</ThemeProvider> </ThemeProvider>
</LocalizationProvider> </LocalizationProvider>
</DndProvider> </DndProvider>
</SWRConfig>
); );

@ -1,18 +1,34 @@
export interface CreateQuizRequest { export interface CreateQuizRequest {
/** 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 of quiz. serialized json for rules of quiz flow */
config: string; config: string;
status: string; /** status of quiz. allow only '', 'draft', 'template', 'stop', 'start' */
status: "draft" | "template" | "stop" | "start";
/** 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;
/** count of questions */
question_cnt: number; question_cnt: number;
/** set true if squiz realize group functionality */
super: boolean; super: boolean;
/** group of new quiz */
group_id: number; 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,20 +1,27 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import { import {
Typography,
Box, Box,
Button, Button,
SxProps, SxProps,
Theme, Theme,
useTheme, Typography,
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;
@ -24,14 +31,23 @@ export default function MyQuizzesFull({
outerContainerSx: sx, outerContainerSx: sx,
children, children,
}: Props) { }: Props) {
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore(); useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error creating quiz", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const quizArray = useQuizArray();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
return ( return (
<> <>
{Object.keys(listQuizes).length === 0 ? ( {quizArray.length === 0 ? (
<FirstQuiz /> <FirstQuiz />
) : ( ) : (
<SectionWrapper maxWidth="lg"> <SectionWrapper maxWidth="lg">
@ -66,17 +82,13 @@ export default function MyQuizzesFull({
mb: "60px", mb: "60px",
}} }}
> >
{Object.values(listQuizes).map(({ id, name }) => ( {quizArray.map(quiz => (
<QuizCard <QuizCard
key={id} key={quiz.id}
name={name} quiz={quiz}
openCount={0} openCount={0}
applicationCount={0} applicationCount={0}
conversionPercent={0} conversionPercent={0}
onClickDelete={() => {
removeQuiz(id);
}}
onClickEdit={() => navigate(`/setting/${id}`)}
/> />
))} ))}
</Box> </Box>

@ -1,35 +1,40 @@
import ChartIcon from "@icons/ChartIcon";
import LinkIcon from "@icons/LinkIcon";
import PencilIcon from "@icons/PencilIcon";
import { Quiz } from "@model/quiz/quiz";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import { import {
Box, Box,
Button, Button,
IconButton, IconButton,
Typography, Typography,
useTheme,
useMediaQuery, useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import ChartIcon from "@icons/ChartIcon"; import { deleteQuiz } from "@root/quizes/actions";
import LinkIcon from "@icons/LinkIcon"; import { useNavigate } from "react-router-dom";
import PencilIcon from "@icons/PencilIcon";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
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();
function handleEditClick() {
navigate(`/setting/${quiz.id}`);
}
return ( return (
<Box <Box
@ -50,7 +55,7 @@ export default function QuizCard({
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> <Typography variant="h5">{quiz.name}</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
@ -76,11 +81,11 @@ export default function QuizCard({
<Typography color={theme.palette.grey3.main}>Открытий</Typography> <Typography color={theme.palette.grey3.main}>Открытий</Typography>
</Box> </Box>
<Box sx={{ flex: "1 1 0" }}> <Box sx={{ flex: "1 1 0" }}>
<Typography variant="h5">{openCount}</Typography> <Typography variant="h5">{applicationCount}</Typography>
<Typography color={theme.palette.grey3.main}>Заявок</Typography> <Typography color={theme.palette.grey3.main}>Заявок</Typography>
</Box> </Box>
<Box sx={{ flex: "1 1 0" }}> <Box sx={{ flex: "1 1 0" }}>
<Typography variant="h5">{openCount} %</Typography> <Typography variant="h5">{conversionPercent} %</Typography>
<Typography color={theme.palette.grey3.main}>Конверсия</Typography> <Typography color={theme.palette.grey3.main}>Конверсия</Typography>
</Box> </Box>
</Box> </Box>
@ -102,7 +107,7 @@ export default function QuizCard({
<Button <Button
variant="outlined" variant="outlined"
startIcon={<PencilIcon />} startIcon={<PencilIcon />}
onClick={onClickEdit} onClick={handleEditClick}
sx={{ sx={{
padding: isMobile ? "10px" : "10px 20px", padding: isMobile ? "10px" : "10px 20px",
minWidth: "unset", minWidth: "unset",
@ -132,7 +137,7 @@ export default function QuizCard({
color: theme.palette.brightPurple.main, color: theme.palette.brightPurple.main,
ml: "auto", ml: "auto",
}} }}
onClick={onClickDelete} onClick={() => deleteQuiz(quiz.id)}
> >
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} /> <MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
</IconButton> </IconButton>

@ -1,7 +1,9 @@
import Stepper from "@ui_kit/Stepper"; import { quizApi } from "@api/quiz";
import SwitchStepPages from "@ui_kit/switchStepPages"; import { devlog } from "@frontend/kitui";
import React, { useState } from "react"; import BackArrowIcon from "@icons/BackArrowIcon";
import PenaLogo from "@ui_kit/PenaLogo"; import { Burger } from "@icons/Burger";
import EyeIcon from "@icons/EyeIcon";
import { PenaLogoIcon } from "@icons/PenaLogoIcon";
import { import {
Box, Box,
Button, Button,
@ -12,42 +14,41 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { Link } from "react-router-dom"; import { decrementCurrentStep, setQuizes } from "@root/quizes/actions";
import BackArrowIcon from "@icons/BackArrowIcon"; import { useCurrentQuiz } from "@root/quizes/hooks";
import NavMenuItem from "@ui_kit/Header/NavMenuItem"; import { useQuizStore } from "@root/quizes/store";
import EyeIcon from "@icons/EyeIcon";
import CustomAvatar from "@ui_kit/Header/Avatar"; 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 Sidebar from "@ui_kit/Sidebar";
import { quizStore } from "@root/quizes"; import Stepper from "@ui_kit/Stepper";
import { useParams } from "react-router-dom"; import SwitchStepPages from "@ui_kit/switchStepPages";
import { Burger } from "@icons/Burger"; import { isAxiosError } from "axios";
import { PenaLogoIcon } from "@icons/PenaLogoIcon"; import { enqueueSnackbar } from "notistack";
import { useState } from "react";
import { Link } from "react-router-dom";
import useSWR from "swr";
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 message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error creating quiz", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const theme = useTheme(); const theme = useTheme();
const quiz = useCurrentQuiz();
const currentStep = useQuizStore(state => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false); 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 (
<> <>
@ -80,7 +81,7 @@ export default function StartPage() {
ml: "37px", ml: "37px",
}} }}
> >
<IconButton sx={{ p: "6px" }} onClick={() => handleBack()}> <IconButton sx={{ p: "6px" }} onClick={decrementCurrentStep}>
<BackArrowIcon /> <BackArrowIcon />
</IconButton> </IconButton>
<FormControl fullWidth variant="standard"> <FormControl fullWidth variant="standard">
@ -212,18 +213,17 @@ export default function StartPage() {
boxSizing: "border-box", boxSizing: "border-box",
}} }}
> >
<Stepper {quizConfig &&
activeStep={activeStep} <>
desc={DESCRIPTIONS[activeStep - 1]} <Stepper activeStep={currentStep} />
/>
<SwitchStepPages <SwitchStepPages
activeStep={activeStep} activeStep={currentStep}
quizType={listQuizes[params].config.type} quizType={quizConfig.type}
startpage={listQuizes[params].startpage}
createResult={listQuizes[params].createResult}
/> />
</>
}
</Box> </Box>
{isTablet && activeStep === 1 && ( {isTablet && [1, 2, 3].includes(currentStep) && (
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",

@ -1,15 +1,19 @@
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;
if (!config) return null;
const { listQuizes, updateQuizesList } = quizStore();
return ( return (
<Box <Box
sx={{ sx={{
@ -30,9 +34,7 @@ export default function StepOne() {
variant="text" variant="text"
data-cy="create-quiz-card" data-cy="create-quiz-card"
onClick={() => { onClick={() => {
let SPageClone = listQuizes[params].config; setQuizType(quiz.id, "quiz", navigate);
SPageClone.type = "quize";
updateQuizesList(params, { config: SPageClone });
}} }}
> >
<CreationCard <CreationCard
@ -40,7 +42,7 @@ export default function StepOne() {
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас" text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
image={quizCreationImage1} image={quizCreationImage1}
border={ border={
listQuizes[params].config.type === "quize" config.type === "quiz"
? "1px solid #7E2AEA" ? "1px solid #7E2AEA"
: "none" : "none"
} }
@ -49,9 +51,7 @@ export default function StepOne() {
<Button <Button
variant="text" variant="text"
onClick={() => { onClick={() => {
let SPageClone = listQuizes[params].config; setQuizType(quiz.id,"form", navigate);
SPageClone.type = "form";
updateQuizesList(params, { config: SPageClone });
}} }}
> >
<CreationCard <CreationCard
@ -59,7 +59,7 @@ export default function StepOne() {
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас" text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
image={quizCreationImage2} image={quizCreationImage2}
border={ border={
listQuizes[params].config.type === "form" config.type === "form"
? "1px solid #7E2AEA" ? "1px solid #7E2AEA"
: "none" : "none"
} }

@ -2,21 +2,27 @@ 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();
const config = quiz?.config;
if (!config) return null;
return ( return (
<Box sx={{ mt: "60px" }}> <Box sx={{ mt: "60px" }}>
@ -41,14 +47,14 @@ export default function Steptwo() {
variant="text" variant="text"
data-cy="select-quiz-layout-standard" data-cy="select-quiz-layout-standard"
onClick={() => { onClick={() => {
updateQuizesList(params, { startpage: "standard" }); setQuizStartpageType(quiz.id, "standard", navigate);
}} }}
> >
<CardWithImage <CardWithImage
image={cardImage1} image={cardImage1}
text="Standard" text="Standard"
border={ border={
listQuizes[params].startpage === "standard" config.startpageType === "standard"
? "1px solid #7E2AEA" ? "1px solid #7E2AEA"
: "none" : "none"
} }
@ -57,14 +63,14 @@ export default function Steptwo() {
<Button <Button
variant="text" variant="text"
onClick={() => { onClick={() => {
updateQuizesList(params, { startpage: "expanded" }); setQuizStartpageType(quiz.id, "expanded", navigate);
}} }}
> >
<CardWithImage <CardWithImage
image={cardImage2} image={cardImage2}
text="Expanded" text="Expanded"
border={ border={
listQuizes[params].startpage === "expanded" config.startpageType === "expanded"
? "1px solid #7E2AEA" ? "1px solid #7E2AEA"
: "none" : "none"
} }
@ -73,14 +79,14 @@ export default function Steptwo() {
<Button <Button
variant="text" variant="text"
onClick={() => { onClick={() => {
updateQuizesList(params, { startpage: "centered" }); setQuizStartpageType(quiz.id, "centered", navigate);
}} }}
> >
<CardWithImage <CardWithImage
image={cardImage3} image={cardImage3}
text="Centered" text="Centered"
border={ border={
listQuizes[params].startpage === "centered" config.startpageType === "centered"
? "1px solid #7E2AEA" ? "1px solid #7E2AEA"
: "none" : "none"
} }

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

@ -5,8 +5,8 @@ import {
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,19 +21,6 @@ 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;
@ -53,15 +40,6 @@ export default function QuizPreviewLayout() {
); );
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep]; const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
const QuestionComponent = currentQuestion
? QuestionPreviewComponentByType[
currentQuestion.type as DefiniteQuestionType
]
: null;
const questionElement = QuestionComponent ? (
<QuestionComponent key={currentQuestion.id} question={currentQuestion} />
) : null;
useEffect( useEffect(
function resetCurrentQuizStep() { function resetCurrentQuizStep() {
@ -94,7 +72,7 @@ export default function QuizPreviewLayout() {
"&::-webkit-scrollbar": { width: 0 }, "&::-webkit-scrollbar": { width: 0 },
}} }}
> >
{questionElement} <QuestionPreviewComponent question={currentQuestion} />
</Box> </Box>
<Box <Box
sx={{ sx={{
@ -115,8 +93,7 @@ export default function QuizPreviewLayout() {
> >
<Typography> <Typography>
{nonDeletedQuizQuestions.length > 0 {nonDeletedQuizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${ ? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length
nonDeletedQuizQuestions.length
}` }`
: "Нет вопросов"} : "Нет вопросов"}
</Typography> </Typography>
@ -162,3 +139,22 @@ export default function QuizPreviewLayout() {
</Paper> </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");
}
}

@ -24,6 +24,9 @@ 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, "Стартовая страница"],
@ -46,10 +49,10 @@ 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 ( return (
<Box <Box
sx={{ sx={{
@ -106,17 +109,15 @@ export default function Sidebar() {
const Icon = menuItem[0]; const Icon = menuItem[0];
return ( return (
<MenuItem <MenuItem
onClick={() => { onClick={() => setCurrentStep(index + 1)}
updateQuizesList(quizId, { step: index + 1 });
}}
key={menuItem[1]} key={menuItem[1]}
text={menuItem[1]} text={menuItem[1]}
isCollapsed={isMenuCollapsed} isCollapsed={isMenuCollapsed}
isActive={listQuizes[quizId].step === index + 1} isActive={quizSetupSteps[currentStep].displayStep === index + 1}
icon={ icon={
<Icon <Icon
color={ color={
listQuizes[quizId].step === index + 1 quizSetupSteps[currentStep].displayStep === index + 1
? theme.palette.brightPurple.main ? theme.palette.brightPurple.main
: isMenuCollapsed : isMenuCollapsed
? "white" ? "white"
@ -145,7 +146,7 @@ export default function Sidebar() {
Настройки квиза Настройки квиза
</Typography> </Typography>
)} )}
<List disablePadding> {/* <List disablePadding> // TODO
{quizSettingsMenuItems.map((menuItem, index) => { {quizSettingsMenuItems.map((menuItem, index) => {
const Icon = menuItem[0]; const Icon = menuItem[0];
const totalIndex = index + createQuizMenuItems.length; const totalIndex = index + createQuizMenuItems.length;
@ -173,7 +174,7 @@ export default function Sidebar() {
/> />
); );
})} })}
</List> </List> */}
</Box> </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