use backend somewhere & refactor
This commit is contained in:
parent
f7f1ce13ab
commit
0c6198dd79
@ -41,6 +41,7 @@
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-router-dom": "^6.6.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"swr": "^2.2.4",
|
||||
"typescript": "^4.4.2",
|
||||
"use-debounce": "^9.0.4",
|
||||
"web-vitals": "^2.1.0",
|
||||
|
@ -5,40 +5,44 @@ import { DeleteQuizRequest, DeleteQuizResponse } from "model/quiz/delete";
|
||||
import { EditQuizRequest, EditQuizResponse } from "model/quiz/edit";
|
||||
import { GetQuizRequest, GetQuizResponse } from "model/quiz/get";
|
||||
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";
|
||||
|
||||
function createQuiz(body: CreateQuizRequest = defaultCreateQuizBody) {
|
||||
return makeRequest<CreateQuizRequest, BackendQuiz>({
|
||||
function createQuiz(body?: Partial<CreateQuizRequest>) {
|
||||
return makeRequest<CreateQuizRequest, RawQuiz>({
|
||||
url: `${baseUrl}/quiz/create`,
|
||||
body,
|
||||
body: { ...defaultCreateQuizBody, ...body },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
function getQuizList(body: GetQuizListRequest = defaultGetQuizListBody) {
|
||||
return makeRequest<GetQuizListRequest, GetQuizListResponse>({
|
||||
async function getQuizList(body?: Partial<GetQuizListRequest>) {
|
||||
const response = await makeRequest<GetQuizListRequest, GetQuizListResponse>({
|
||||
url: `${baseUrl}/quiz/getList`,
|
||||
body,
|
||||
method: "GET",
|
||||
body: { ...defaultGetQuizListBody, ...body },
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
return response.items;
|
||||
}
|
||||
|
||||
function getQuiz(body: GetQuizRequest = defaultGetQuizBody) {
|
||||
function getQuiz(body?: Partial<GetQuizRequest>) {
|
||||
return makeRequest<GetQuizRequest, GetQuizResponse>({
|
||||
url: `${baseUrl}/quiz/get`,
|
||||
body,
|
||||
body: { ...defaultGetQuizBody, ...body },
|
||||
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>({
|
||||
url: `${baseUrl}/quiz/edit`,
|
||||
body,
|
||||
method: "PATCH",
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
@ -86,18 +90,18 @@ const defaultCreateQuizBody: CreateQuizRequest = {
|
||||
"fingerprinting": true,
|
||||
"repeatable": true,
|
||||
"note_prevented": true,
|
||||
"mail_notifications": true,
|
||||
"mail_notifications": false,
|
||||
"unique_answers": true,
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"config": "string",
|
||||
"status": "string",
|
||||
"status": "stop",
|
||||
"limit": 0,
|
||||
"due_to": 0,
|
||||
"time_of_passing": 0,
|
||||
"pausable": true,
|
||||
"pausable": false,
|
||||
"question_cnt": 0,
|
||||
"super": true,
|
||||
"super": false,
|
||||
"group_id": 0,
|
||||
};
|
||||
|
||||
@ -129,14 +133,6 @@ const defaultGetQuizBody: GetQuizRequest = {
|
||||
};
|
||||
|
||||
const defaultGetQuizListBody: GetQuizListRequest = {
|
||||
"limit": 0,
|
||||
"limit": 100,
|
||||
"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 "./index.css";
|
||||
import lightTheme from "./utils/themes/light";
|
||||
import { SWRConfig } from "swr";
|
||||
|
||||
|
||||
dayjs.locale("ru");
|
||||
@ -20,14 +21,16 @@ const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeTe
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
|
||||
root.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<SnackbarProvider>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</LocalizationProvider>
|
||||
</DndProvider>
|
||||
<SWRConfig value={{ revalidateOnFocus: false, shouldRetryOnError: false }}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<SnackbarProvider>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</LocalizationProvider>
|
||||
</DndProvider>
|
||||
</SWRConfig>
|
||||
);
|
||||
|
@ -1,18 +1,34 @@
|
||||
export interface CreateQuizRequest {
|
||||
fingerprinting: boolean;
|
||||
repeatable: boolean;
|
||||
note_prevented: 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;
|
||||
unique_answers: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
config: string;
|
||||
status: string;
|
||||
limit: number;
|
||||
due_to: number;
|
||||
time_of_passing: number;
|
||||
pausable: boolean;
|
||||
question_cnt: number;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
/** 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: "draft" | "template" | "stop" | "start";
|
||||
/** 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;
|
||||
/** 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 {
|
||||
/** id of question for update */
|
||||
id: number;
|
||||
/** set true for storing fingerprints */
|
||||
fp: boolean;
|
||||
/** set true for allow to repeat quiz after passing */
|
||||
rep: boolean;
|
||||
/** set true for store unfinished passing */
|
||||
note_prevented: boolean;
|
||||
/** set true if we should send passing result on every passing */
|
||||
mailing: boolean;
|
||||
/** set true if we allow only one user quiz passing */
|
||||
uniq: boolean;
|
||||
name: string;
|
||||
desc: string;
|
||||
conf: string;
|
||||
status: string;
|
||||
/** new name of the quiz */
|
||||
name?: string;
|
||||
/** new descriptions of the quiz */
|
||||
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;
|
||||
/** max time of quiz passing */
|
||||
due_to: number;
|
||||
/** max time to pass quiz */
|
||||
time_of_passing: number;
|
||||
/** allow to pause quiz to user */
|
||||
pausable: boolean;
|
||||
question_cnt: number;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
/** count of questions */
|
||||
question_cnt?: number;
|
||||
/** set true if squiz realize group functionality */
|
||||
super?: boolean;
|
||||
/** group of new quiz */
|
||||
group_id?: number;
|
||||
}
|
||||
|
||||
export interface EditQuizResponse {
|
||||
/** id of new version of question */
|
||||
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 {
|
||||
limit: number;
|
||||
offset: number;
|
||||
from: number;
|
||||
to: number;
|
||||
search: string;
|
||||
status: string;
|
||||
deleted: boolean;
|
||||
archived: boolean;
|
||||
super: boolean;
|
||||
group_id: number;
|
||||
/** max items on page */
|
||||
limit?: number;
|
||||
/** page number */
|
||||
offset?: number;
|
||||
/** start time of time period. timestamp in seconds */
|
||||
from?: number;
|
||||
/** end time of time period. timestamp in seconds */
|
||||
to?: number;
|
||||
/** string for fulltext search in titles of quizes */
|
||||
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 {
|
||||
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;
|
||||
/** 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: string;
|
||||
/** quiz config*/
|
||||
config: QuizConfig;
|
||||
/** 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 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
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 { createQuiz } from "@root/quizesV2";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
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() {
|
||||
|
@ -1,88 +1,100 @@
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
SxProps,
|
||||
Theme,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Button,
|
||||
SxProps,
|
||||
Theme,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import ComplexNavText from "./ComplexNavText";
|
||||
import QuizCard from "./QuizCard";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
import { isAxiosError } from "axios";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import React from "react";
|
||||
import { quizStore } from "@root/quizes";
|
||||
import FirstQuiz from "./FirstQuiz";
|
||||
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 {
|
||||
outerContainerSx?: SxProps<Theme>;
|
||||
children?: React.ReactNode;
|
||||
outerContainerSx?: SxProps<Theme>;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function MyQuizzesFull({
|
||||
outerContainerSx: sx,
|
||||
children,
|
||||
outerContainerSx: sx,
|
||||
children,
|
||||
}: Props) {
|
||||
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(500));
|
||||
useSWR("quizes", () => quizApi.getList(), {
|
||||
onSuccess: setQuizes,
|
||||
onError: error => {
|
||||
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.keys(listQuizes).length === 0 ? (
|
||||
<FirstQuiz />
|
||||
) : (
|
||||
<SectionWrapper maxWidth="lg">
|
||||
<ComplexNavText text1="Кабинет квизов" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mt: "20px",
|
||||
mb: "30px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Мои квизы</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
padding: isMobile ? "10px" : "10px 47px",
|
||||
minWidth: "44px",
|
||||
}}
|
||||
onClick={() => createQuiz(navigate)}
|
||||
>
|
||||
{isMobile ? "+" : "Создать +"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
py: "10px",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "40px",
|
||||
mb: "60px",
|
||||
}}
|
||||
>
|
||||
{Object.values(listQuizes).map(({ id, name }) => (
|
||||
<QuizCard
|
||||
key={id}
|
||||
name={name}
|
||||
openCount={0}
|
||||
applicationCount={0}
|
||||
conversionPercent={0}
|
||||
onClickDelete={() => {
|
||||
removeQuiz(id);
|
||||
}}
|
||||
onClickEdit={() => navigate(`/setting/${id}`)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
{children}
|
||||
</SectionWrapper>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
devlog("Error creating quiz", error);
|
||||
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
|
||||
},
|
||||
});
|
||||
const quizArray = useQuizArray();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(500));
|
||||
|
||||
return (
|
||||
<>
|
||||
{quizArray.length === 0 ? (
|
||||
<FirstQuiz />
|
||||
) : (
|
||||
<SectionWrapper maxWidth="lg">
|
||||
<ComplexNavText text1="Кабинет квизов" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mt: "20px",
|
||||
mb: "30px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4">Мои квизы</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
padding: isMobile ? "10px" : "10px 47px",
|
||||
minWidth: "44px",
|
||||
}}
|
||||
onClick={() => createQuiz(navigate)}
|
||||
>
|
||||
{isMobile ? "+" : "Создать +"}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
py: "10px",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "40px",
|
||||
mb: "60px",
|
||||
}}
|
||||
>
|
||||
{quizArray.map(quiz => (
|
||||
<QuizCard
|
||||
key={quiz.id}
|
||||
quiz={quiz}
|
||||
openCount={0}
|
||||
applicationCount={0}
|
||||
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 LinkIcon from "@icons/LinkIcon";
|
||||
import PencilIcon from "@icons/PencilIcon";
|
||||
import { Quiz } from "@model/quiz/quiz";
|
||||
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 {
|
||||
name: string;
|
||||
openCount?: number;
|
||||
applicationCount?: number;
|
||||
conversionPercent?: number;
|
||||
onClickDelete?: () => void;
|
||||
onClickEdit?: () => void;
|
||||
quiz: Quiz;
|
||||
openCount?: number;
|
||||
applicationCount?: number;
|
||||
conversionPercent?: number;
|
||||
}
|
||||
|
||||
export default function QuizCard({
|
||||
name,
|
||||
openCount = 0,
|
||||
applicationCount = 0,
|
||||
conversionPercent = 0,
|
||||
onClickDelete,
|
||||
onClickEdit,
|
||||
quiz,
|
||||
openCount = 0,
|
||||
applicationCount = 0,
|
||||
conversionPercent = 0,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "white",
|
||||
width: "560px",
|
||||
height: "280px",
|
||||
p: "20px",
|
||||
borderRadius: "12px",
|
||||
boxSizing: "border-box",
|
||||
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
|
||||
function handleEditClick() {
|
||||
navigate(`/setting/${quiz.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "white",
|
||||
width: "560px",
|
||||
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 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
|
||||
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
|
||||
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
|
||||
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",
|
||||
}}
|
||||
}}
|
||||
>
|
||||
Заявки
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<PencilIcon />}
|
||||
onClick={onClickEdit}
|
||||
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={onClickDelete}
|
||||
>
|
||||
<MoreHorizIcon sx={{ transform: "scale(1.75)" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
<Typography variant="h5">{quiz.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">{applicationCount}</Typography>
|
||||
<Typography color={theme.palette.grey3.main}>Заявок</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: "1 1 0" }}>
|
||||
<Typography variant="h5">{conversionPercent} %</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",
|
||||
}}
|
||||
>
|
||||
Заявки
|
||||
</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 SwitchStepPages from "@ui_kit/switchStepPages";
|
||||
import React, { useState } from "react";
|
||||
import PenaLogo from "@ui_kit/PenaLogo";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
FormControl,
|
||||
IconButton,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { isAxiosError } from "axios";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import BackArrowIcon from "@icons/BackArrowIcon";
|
||||
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 useSWR from "swr";
|
||||
import { SidebarMobile } from "./Sidebar/SidebarMobile";
|
||||
|
||||
const DESCRIPTIONS = [
|
||||
"Настройка стартовой страницы",
|
||||
"Задайте вопросы",
|
||||
"Настройте авторезультаты",
|
||||
"Оценка графа карты вопросов",
|
||||
"Настройте форму контактов",
|
||||
"Установите квиз",
|
||||
"Запустите рекламу",
|
||||
] as const;
|
||||
|
||||
export default function StartPage() {
|
||||
const { listQuizes, updateQuizesList, removeQuiz, createBlank } = quizStore();
|
||||
const params = Number(useParams().quizId);
|
||||
const activeStep = listQuizes[params].step;
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||
useSWR("quizes", () => quizApi.getList(), {
|
||||
onSuccess: setQuizes,
|
||||
onError: error => {
|
||||
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
|
||||
|
||||
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 = () => {
|
||||
let result = listQuizes[params].step - 1;
|
||||
updateQuizesList(params, { step: result ? result : 1 });
|
||||
};
|
||||
const quizConfig = quiz?.config;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*хедер*/}
|
||||
<Container
|
||||
component="nav"
|
||||
maxWidth={false}
|
||||
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
|
||||
return (
|
||||
<>
|
||||
{/*хедер*/}
|
||||
<Container
|
||||
component="nav"
|
||||
maxWidth={false}
|
||||
disableGutters
|
||||
sx={{
|
||||
ml: "11px",
|
||||
backgroundColor: theme.palette.orange.main,
|
||||
height: "36px",
|
||||
width: "36px",
|
||||
px: "16px",
|
||||
display: "flex",
|
||||
height: isMobile ? "51px" : "80px",
|
||||
alignItems: "center",
|
||||
bgcolor: isMobile ? "#333647" : "white",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
>
|
||||
<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={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
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "30px",
|
||||
overflow: "hidden",
|
||||
ml: "20px",
|
||||
}}
|
||||
sx={{
|
||||
display: isMobile ? "block" : "flex",
|
||||
}}
|
||||
>
|
||||
<NavMenuItem text="Редактировать" isActive />
|
||||
<NavMenuItem text="Заявки" />
|
||||
<NavMenuItem text="Аналитика" />
|
||||
<NavMenuItem text="История" />
|
||||
<NavMenuItem text="Помощь" />
|
||||
{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",
|
||||
}}
|
||||
>
|
||||
{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
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import quizCreationImage1 from "../../assets/quiz-creation-1.png";
|
||||
import quizCreationImage2 from "../../assets/quiz-creation-2.png";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { quizStore } from "@root/quizes";
|
||||
import { setQuizType } from "@root/quizes/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
|
||||
|
||||
export default function StepOne() {
|
||||
const params = Number(useParams().quizId);
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const quiz = useCurrentQuiz();
|
||||
const config = quiz?.config;
|
||||
|
||||
const { listQuizes, updateQuizesList } = quizStore();
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
overflowX: "scroll",
|
||||
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 });
|
||||
}}
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
overflowX: "scroll",
|
||||
padding: "0 5px 15px",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<CreationCard
|
||||
header="Создание квиз-опроса"
|
||||
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
|
||||
image={quizCreationImage1}
|
||||
border={
|
||||
listQuizes[params].config.type === "quize"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
let SPageClone = listQuizes[params].config;
|
||||
SPageClone.type = "form";
|
||||
updateQuizesList(params, { config: SPageClone });
|
||||
}}
|
||||
>
|
||||
<CreationCard
|
||||
header="Создание анкеты"
|
||||
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
|
||||
image={quizCreationImage2}
|
||||
border={
|
||||
listQuizes[params].config.type === "form"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: "720px",
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
mt: "60px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
data-cy="create-quiz-card"
|
||||
onClick={() => {
|
||||
setQuizType(quiz.id, "quiz", navigate);
|
||||
}}
|
||||
>
|
||||
<CreationCard
|
||||
header="Создание квиз-опроса"
|
||||
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
|
||||
image={quizCreationImage1}
|
||||
border={
|
||||
config.type === "quiz"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setQuizType(quiz.id,"form", navigate);
|
||||
}}
|
||||
>
|
||||
<CreationCard
|
||||
header="Создание анкеты"
|
||||
text="У стартовой страницы одна ключевая задача - заинтересовать посетителя пройти квиз. С ней сложно ошибиться, сформулируйте суть предложения и подберите живую фотографию, остальное мы сделаем за вас"
|
||||
image={quizCreationImage2}
|
||||
border={
|
||||
config.type === "form"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,93 +1,99 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} 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 cardImage2 from "../../assets/card-2.png";
|
||||
import cardImage3 from "../../assets/card-3.png";
|
||||
import { quizStore } from "@root/quizes";
|
||||
import { useParams } from "react-router-dom";
|
||||
import CardWithImage from "./CardWithImage";
|
||||
|
||||
|
||||
export default function Steptwo() {
|
||||
const params = Number(useParams().quizId);
|
||||
const { listQuizes, updateQuizesList } = quizStore();
|
||||
const theme = useTheme();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300));
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1300));
|
||||
const quiz = useCurrentQuiz();
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: "60px" }}>
|
||||
<Typography variant="h5">Стартовая страница</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
overflowX: "scroll",
|
||||
paddingBottom: "15px",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: "950px",
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
mt: "40px",
|
||||
padding: isSmallMonitor ? "0 15px 15px" : 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
data-cy="select-quiz-layout-standard"
|
||||
onClick={() => {
|
||||
updateQuizesList(params, { startpage: "standard" });
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage1}
|
||||
text="Standard"
|
||||
border={
|
||||
listQuizes[params].startpage === "standard"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
updateQuizesList(params, { startpage: "expanded" });
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage2}
|
||||
text="Expanded"
|
||||
border={
|
||||
listQuizes[params].startpage === "expanded"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
updateQuizesList(params, { startpage: "centered" });
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage3}
|
||||
text="Centered"
|
||||
border={
|
||||
listQuizes[params].startpage === "centered"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
const config = quiz?.config;
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: "60px" }}>
|
||||
<Typography variant="h5">Стартовая страница</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
overflowX: "scroll",
|
||||
paddingBottom: "15px",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: "950px",
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
mt: "40px",
|
||||
padding: isSmallMonitor ? "0 15px 15px" : 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
data-cy="select-quiz-layout-standard"
|
||||
onClick={() => {
|
||||
setQuizStartpageType(quiz.id, "standard", navigate);
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage1}
|
||||
text="Standard"
|
||||
border={
|
||||
config.startpageType === "standard"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setQuizStartpageType(quiz.id, "expanded", navigate);
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage2}
|
||||
text="Expanded"
|
||||
border={
|
||||
config.startpageType === "expanded"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setQuizStartpageType(quiz.id, "centered", navigate);
|
||||
}}
|
||||
>
|
||||
<CardWithImage
|
||||
image={cardImage3}
|
||||
text="Centered"
|
||||
border={
|
||||
config.startpageType === "centered"
|
||||
? "1px solid #7E2AEA"
|
||||
: "none"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ export const setQuestionFieldOptimistic = async <T extends keyof Question>(
|
||||
} catch (error) {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
|
||||
devlog("Error editing question", { error, question: question, currentUpdatedQuestion });
|
||||
devlog("Error editing question", { error, question, currentUpdatedQuestion });
|
||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||
if (!savedOriginalQuestion) {
|
||||
devlog("Cannot rollback question");
|
||||
|
195
src/stores/quizes/actions.ts
Normal file
195
src/stores/quizes/actions.ts
Normal file
@ -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);
|
||||
}
|
17
src/stores/quizes/hooks.ts
Normal file
17
src/stores/quizes/hooks.ts
Normal file
@ -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;
|
||||
}
|
25
src/stores/quizes/store.ts
Normal file
25
src/stores/quizes/store.ts
Normal file
@ -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 { questionStore } from "@root/questions";
|
||||
import {
|
||||
decrementCurrentQuestionIndex,
|
||||
incrementCurrentQuestionIndex,
|
||||
useQuizPreviewStore,
|
||||
decrementCurrentQuestionIndex,
|
||||
incrementCurrentQuestionIndex,
|
||||
useQuizPreviewStore,
|
||||
} from "@root/quizPreview";
|
||||
import { DefiniteQuestionType } from "model/questionTypes/shared";
|
||||
import { FC, useEffect } from "react";
|
||||
import { AnyQuizQuestion } from "model/questionTypes/shared";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
||||
import Date from "./QuizPreviewQuestionTypes/Date";
|
||||
@ -21,144 +21,140 @@ import Text from "./QuizPreviewQuestionTypes/Text";
|
||||
import Variant from "./QuizPreviewQuestionTypes/Variant";
|
||||
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() {
|
||||
const quizId = useParams().quizId ?? 0;
|
||||
const listQuestions = questionStore((state) => state.listQuestions);
|
||||
const currentQuizStep = useQuizPreviewStore(
|
||||
(state) => state.currentQuestionIndex
|
||||
);
|
||||
const quizId = useParams().quizId ?? 0;
|
||||
const listQuestions = questionStore((state) => state.listQuestions);
|
||||
const currentQuizStep = useQuizPreviewStore(
|
||||
(state) => state.currentQuestionIndex
|
||||
);
|
||||
|
||||
const quizQuestions = listQuestions[quizId] ?? [];
|
||||
const nonDeletedQuizQuestions = quizQuestions.filter(
|
||||
(question) => !question.deleted
|
||||
);
|
||||
const maxCurrentQuizStep =
|
||||
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
|
||||
const currentProgress = Math.floor(
|
||||
(currentQuizStep / maxCurrentQuizStep) * 100
|
||||
);
|
||||
const quizQuestions = listQuestions[quizId] ?? [];
|
||||
const nonDeletedQuizQuestions = quizQuestions.filter(
|
||||
(question) => !question.deleted
|
||||
);
|
||||
const maxCurrentQuizStep =
|
||||
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
|
||||
const currentProgress = Math.floor(
|
||||
(currentQuizStep / maxCurrentQuizStep) * 100
|
||||
);
|
||||
|
||||
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
|
||||
const QuestionComponent = currentQuestion
|
||||
? QuestionPreviewComponentByType[
|
||||
currentQuestion.type as DefiniteQuestionType
|
||||
]
|
||||
: null;
|
||||
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
|
||||
|
||||
const questionElement = QuestionComponent ? (
|
||||
<QuestionComponent key={currentQuestion.id} question={currentQuestion} />
|
||||
) : null;
|
||||
useEffect(
|
||||
function resetCurrentQuizStep() {
|
||||
if (currentQuizStep > maxCurrentQuizStep) {
|
||||
decrementCurrentQuestionIndex();
|
||||
}
|
||||
},
|
||||
[currentQuizStep, maxCurrentQuizStep]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function resetCurrentQuizStep() {
|
||||
if (currentQuizStep > maxCurrentQuizStep) {
|
||||
decrementCurrentQuestionIndex();
|
||||
}
|
||||
},
|
||||
[currentQuizStep, maxCurrentQuizStep]
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
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,
|
||||
}}
|
||||
return (
|
||||
<Paper
|
||||
className="quiz-preview-draghandle"
|
||||
data-cy="quiz-preview-layout"
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
borderRadius: "12px",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
{nonDeletedQuizQuestions.length > 0
|
||||
? `Вопрос ${currentQuizStep + 1} из ${
|
||||
nonDeletedQuizQuestions.length
|
||||
}`
|
||||
: "Нет вопросов"}
|
||||
</Typography>
|
||||
{nonDeletedQuizQuestions.length > 0 && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentProgress}
|
||||
sx={{
|
||||
"&.MuiLinearProgress-colorPrimary": {
|
||||
backgroundColor: "fadePurple.main",
|
||||
},
|
||||
"& .MuiLinearProgress-barColorPrimary": {
|
||||
backgroundColor: "brightPurple.main",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
<Box
|
||||
sx={{
|
||||
p: "16px",
|
||||
whiteSpace: "break-spaces",
|
||||
overflowY: "auto",
|
||||
flexGrow: 1,
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<QuestionPreviewComponent question={currentQuestion} />
|
||||
</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>
|
||||
{nonDeletedQuizQuestions.length > 0
|
||||
? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length
|
||||
}`
|
||||
: "Нет вопросов"}
|
||||
</Typography>
|
||||
{nonDeletedQuizQuestions.length > 0 && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={currentProgress}
|
||||
sx={{
|
||||
"&.MuiLinearProgress-colorPrimary": {
|
||||
backgroundColor: "fadePurple.main",
|
||||
},
|
||||
"& .MuiLinearProgress-barColorPrimary": {
|
||||
backgroundColor: "brightPurple.main",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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 { useParams } from "react-router-dom";
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
useTheme,
|
||||
List,
|
||||
Typography,
|
||||
IconButton,
|
||||
Container,
|
||||
Box,
|
||||
useTheme,
|
||||
List,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
|
||||
import { quizStore } from "@root/quizes";
|
||||
@ -24,156 +24,157 @@ import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
|
||||
import GearIcon from "@icons/GearIcon";
|
||||
import LayoutIcon from "@icons/LayoutIcon";
|
||||
import MenuItem from "./MenuItem";
|
||||
import { quizSetupSteps } from "@model/quizSettings";
|
||||
import { useQuizStore } from "@root/quizes/store";
|
||||
import { setCurrentStep } from "@root/quizes/actions";
|
||||
|
||||
const createQuizMenuItems = [
|
||||
[LayoutIcon, "Стартовая страница"],
|
||||
[QuestionIcon, "Вопросы"],
|
||||
[ChartPieIcon, "Результаты"],
|
||||
[QuestionsMapIcon, "Карта вопросов"],
|
||||
[ContactBookIcon, "Форма контактов"],
|
||||
[FlowArrowIcon, "Установка квиза"],
|
||||
[MegaphoneIcon, "Запуск рекламы"],
|
||||
[LayoutIcon, "Стартовая страница"],
|
||||
[QuestionIcon, "Вопросы"],
|
||||
[ChartPieIcon, "Результаты"],
|
||||
[QuestionsMapIcon, "Карта вопросов"],
|
||||
[ContactBookIcon, "Форма контактов"],
|
||||
[FlowArrowIcon, "Установка квиза"],
|
||||
[MegaphoneIcon, "Запуск рекламы"],
|
||||
] as const;
|
||||
|
||||
const quizSettingsMenuItems = [
|
||||
[TagIcon, "Дополнения"],
|
||||
[PencilCircleIcon, "Дизайн"],
|
||||
[PuzzlePieceIcon, "Интеграции"],
|
||||
[GearIcon, "Настройки"],
|
||||
[TagIcon, "Дополнения"],
|
||||
[PencilCircleIcon, "Дизайн"],
|
||||
[PuzzlePieceIcon, "Интеграции"],
|
||||
[GearIcon, "Настройки"],
|
||||
] as const;
|
||||
|
||||
export default function Sidebar() {
|
||||
const theme = useTheme();
|
||||
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false);
|
||||
const [progress, setProgress] = useState<number>(1 / 7);
|
||||
const quizId = Number(useParams().quizId);
|
||||
const { listQuizes, updateQuizesList } = quizStore();
|
||||
const theme = useTheme();
|
||||
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false);
|
||||
const [progress, setProgress] = useState<number>(1 / 7);
|
||||
const currentStep = useQuizStore(state => state.currentStep);
|
||||
|
||||
const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
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
|
||||
const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey2.main,
|
||||
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",
|
||||
}}
|
||||
>
|
||||
Создание квиза
|
||||
</Typography>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={handleMenuCollapseToggle}
|
||||
sx={{ ml: isMenuCollapsed ? undefined : "auto" }}
|
||||
>
|
||||
<CollapseMenuIcon
|
||||
height="16px"
|
||||
width="16px"
|
||||
color={theme.palette.grey2.main}
|
||||
transform={isMenuCollapsed ? "rotate(180deg)" : ""}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<List disablePadding>
|
||||
{createQuizMenuItems.map((menuItem, index) => {
|
||||
const Icon = menuItem[0];
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
updateQuizesList(quizId, { step: index + 1 });
|
||||
}}
|
||||
key={menuItem[1]}
|
||||
text={menuItem[1]}
|
||||
isCollapsed={isMenuCollapsed}
|
||||
isActive={listQuizes[quizId].step === index + 1}
|
||||
icon={
|
||||
<Icon
|
||||
color={
|
||||
listQuizes[quizId].step === index + 1
|
||||
? theme.palette.brightPurple.main
|
||||
: isMenuCollapsed
|
||||
? "white"
|
||||
: theme.palette.grey2.main
|
||||
}
|
||||
height={isMenuCollapsed ? "35px" : "24px"}
|
||||
width={isMenuCollapsed ? "35px" : "24px"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
{!isMenuCollapsed && (
|
||||
<Typography
|
||||
sx={{
|
||||
px: "16px",
|
||||
mt: "16px",
|
||||
mb: "11px",
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey2.main,
|
||||
}}
|
||||
>
|
||||
Настройки квиза
|
||||
</Typography>
|
||||
)}
|
||||
<List disablePadding>
|
||||
{quizSettingsMenuItems.map((menuItem, index) => {
|
||||
const Icon = menuItem[0];
|
||||
const totalIndex = index + createQuizMenuItems.length;
|
||||
const isActive = listQuizes[quizId].step === totalIndex + 1;
|
||||
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>
|
||||
);
|
||||
<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={{
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey2.main,
|
||||
}}
|
||||
>
|
||||
Создание квиза
|
||||
</Typography>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={handleMenuCollapseToggle}
|
||||
sx={{ ml: isMenuCollapsed ? undefined : "auto" }}
|
||||
>
|
||||
<CollapseMenuIcon
|
||||
height="16px"
|
||||
width="16px"
|
||||
color={theme.palette.grey2.main}
|
||||
transform={isMenuCollapsed ? "rotate(180deg)" : ""}
|
||||
/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<List disablePadding>
|
||||
{createQuizMenuItems.map((menuItem, index) => {
|
||||
const Icon = menuItem[0];
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={() => setCurrentStep(index + 1)}
|
||||
key={menuItem[1]}
|
||||
text={menuItem[1]}
|
||||
isCollapsed={isMenuCollapsed}
|
||||
isActive={quizSetupSteps[currentStep].displayStep === index + 1}
|
||||
icon={
|
||||
<Icon
|
||||
color={
|
||||
quizSetupSteps[currentStep].displayStep === index + 1
|
||||
? theme.palette.brightPurple.main
|
||||
: isMenuCollapsed
|
||||
? "white"
|
||||
: theme.palette.grey2.main
|
||||
}
|
||||
height={isMenuCollapsed ? "35px" : "24px"}
|
||||
width={isMenuCollapsed ? "35px" : "24px"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
{!isMenuCollapsed && (
|
||||
<Typography
|
||||
sx={{
|
||||
px: "16px",
|
||||
mt: "16px",
|
||||
mb: "11px",
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey2.main,
|
||||
}}
|
||||
>
|
||||
Настройки квиза
|
||||
</Typography>
|
||||
)}
|
||||
{/* <List disablePadding> // TODO
|
||||
{quizSettingsMenuItems.map((menuItem, index) => {
|
||||
const Icon = menuItem[0];
|
||||
const totalIndex = index + createQuizMenuItems.length;
|
||||
const isActive = listQuizes[quizId].step === totalIndex + 1;
|
||||
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 MobileStepper from "@mui/material/MobileStepper";
|
||||
import { QuizSetupStep, maxDisplayQuizSetupSteps, quizSetupSteps } from "@model/quizSettings";
|
||||
|
||||
interface Props {
|
||||
desc?: string;
|
||||
activeStep?: number;
|
||||
steps?: number;
|
||||
activeStep: QuizSetupStep;
|
||||
}
|
||||
|
||||
export default function ProgressMobileStepper({
|
||||
desc,
|
||||
activeStep = 1,
|
||||
steps = 8,
|
||||
activeStep,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
@ -31,9 +28,9 @@ export default function ProgressMobileStepper({
|
||||
>
|
||||
<MobileStepper
|
||||
variant="progress"
|
||||
steps={steps}
|
||||
steps={maxDisplayQuizSetupSteps}
|
||||
position="static"
|
||||
activeStep={activeStep}
|
||||
activeStep={activeStep - 1}
|
||||
sx={{
|
||||
width: "100%",
|
||||
flexGrow: 1,
|
||||
@ -55,9 +52,9 @@ export default function ProgressMobileStepper({
|
||||
sx={{ fontWeight: 400, fontSize: "12px", lineHeight: "14.22px" }}
|
||||
>
|
||||
{" "}
|
||||
Шаг {activeStep} из {steps - 1}
|
||||
Шаг {quizSetupSteps[activeStep].displayStep} из {maxDisplayQuizSetupSteps}
|
||||
</Typography>
|
||||
<Typography>{desc}</Typography>
|
||||
<Typography>{quizSetupSteps[activeStep].text}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,49 +1,37 @@
|
||||
import * as React from "react";
|
||||
import StepOne from "../pages/startPage/stepOne";
|
||||
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 { QuizSetupStep } from "@model/quizSettings";
|
||||
import { notReachable } from "../utils/notReachable";
|
||||
import ContactFormPage from "../pages/ContactFormPage/ContactFormPage";
|
||||
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 { 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 {
|
||||
activeStep: number;
|
||||
quizType: string;
|
||||
startpage: string;
|
||||
createResult: boolean;
|
||||
activeStep: QuizSetupStep;
|
||||
quizType: string;
|
||||
}
|
||||
|
||||
export default function SwitchStepPages({
|
||||
activeStep = 1,
|
||||
quizType,
|
||||
startpage,
|
||||
createResult,
|
||||
activeStep = 1,
|
||||
quizType,
|
||||
}: Props) {
|
||||
switch (activeStep) {
|
||||
case 1:
|
||||
if (!quizType) return <StepOne />;
|
||||
if (!startpage) return <Steptwo />;
|
||||
return <StartPageSettings />;
|
||||
case 2:
|
||||
if (quizType === "form") return <FormQuestionsPage />;
|
||||
return <QuestionsPage />;
|
||||
case 3:
|
||||
if (!createResult) return <Result />;
|
||||
return <Setting />;
|
||||
case 4:
|
||||
return <QuestionsMap />;
|
||||
case 5:
|
||||
return <ContactFormPage />;
|
||||
case 6:
|
||||
return <InstallQuiz />;
|
||||
case 7:
|
||||
return <>Реклама</>;
|
||||
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
switch (activeStep) {
|
||||
case 1: return <StepOne />;
|
||||
case 2: return <Steptwo />;
|
||||
case 3: return <StartPageSettings />;
|
||||
case 4: return quizType === "form" ? <QuestionsPage /> : <FormQuestionsPage />;
|
||||
case 5: return <Result />;
|
||||
case 6: return <Setting />;
|
||||
case 7: return <QuestionsMap />;
|
||||
case 8: return <ContactFormPage />;
|
||||
case 9: return <InstallQuiz />;
|
||||
case 10: return <>Реклама</>;
|
||||
default: return notReachable(activeStep);
|
||||
}
|
||||
}
|
||||
|
3
src/utils/notReachable.ts
Normal file
3
src/utils/notReachable.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function notReachable(_: never): never {
|
||||
throw new Error(`Shouldn't reach here: ${_}`);
|
||||
}
|
@ -13,6 +13,9 @@
|
||||
],
|
||||
"@api/*": [
|
||||
"./api/*"
|
||||
],
|
||||
"@model/*": [
|
||||
"./model/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user