Merge branch 'dev' into 'main'

хелпер не учитывает результ и безтиповые вопросы, CSкомпонент не получает...

See merge request frontend/squiz!73
This commit is contained in:
Nastya 2023-12-14 18:00:39 +00:00
commit 87ca644de3
116 changed files with 23979 additions and 2354 deletions

19013
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -35,7 +35,6 @@
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-cytoscapejs": "^2.0.0",
"react-datepicker": "^4.24.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
@ -78,7 +77,6 @@
"@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.4",
"@types/react-datepicker": "^4.19.3",
"craco-alias": "^3.0.1",
"cypress": "^13.4.0"
}

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

@ -18,6 +18,7 @@ function createQuestion(body: CreateQuestionRequest) {
}
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
console.log("body" , body)
if (!body?.quiz_id) return null;
const response = await makeRequest<GetQuestionListRequest, GetQuestionListResponse>({

@ -173,9 +173,9 @@ export default function Notebook({ color }: Props) {
width="0.998971"
height="1.57978"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
@ -194,9 +194,9 @@ export default function Notebook({ color }: Props) {
width="2.51719"
height="2.51694"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
@ -215,9 +215,9 @@ export default function Notebook({ color }: Props) {
width="1.11616"
height="1.57978"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
@ -236,9 +236,9 @@ export default function Notebook({ color }: Props) {
width="1.22643"
height="0.993766"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
@ -257,9 +257,9 @@ export default function Notebook({ color }: Props) {
width="279"
height="12.0981"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"

@ -38,9 +38,9 @@ export default function YoutobeIcon({ color, width }: Props) {
width="155.738"
height="115.205"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 823 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -0,0 +1,43 @@
import { Box, useTheme } from "@mui/material";
type CheckboxIconProps = {
checked?: boolean;
};
export const CheckboxIcon = ({ checked = false }: CheckboxIconProps) => {
const theme = useTheme();
return (
<Box
sx={{
height: "24px",
width: "24px",
borderRadius: "6px",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: checked
? theme.palette.brightPurple.main
: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
}}
>
{checked && (
<svg
xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 25 18"
fill="none"
>
<path
d="M2 9L10 16.5L22.5 1.5"
stroke="#ffffff"
strokeWidth="4"
strokeLinecap="round"
/>
</svg>
)}
</Box>
);
};

@ -0,0 +1,86 @@
import { useTheme } from "@mui/material";
interface Props {
width?: number;
}
export default function CloseBold({ width }: Props) {
const theme = useTheme();
return (
<svg
width="35"
height="33"
viewBox="0 0 35 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Close">
<g id="Rectangle 57" opacity="0.3" filter="url(#filter0_d_4080_12482)">
<rect x="6" y="4" width="24" height="24" rx="12" fill="#9A9AAF" />
</g>
<g id="Group 331">
<path
id="Vector 586"
d="M22.8516 10.9517L12.9521 20.8512"
stroke="#FDFDFF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
id="Vector 587"
d="M22.8516 20.8462L12.9521 10.9467"
stroke="#FDFDFF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</g>
<defs>
<filter
id="filter0_d_4080_12482"
x="0"
y="0"
width="36"
height="36"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result="hardAlpha"
/>
<feMorphology
radius="1"
operator="dilate"
in="SourceAlpha"
result="effect1_dropShadow_4080_12482"
/>
<feOffset dy="2" />
<feGaussianBlur stdDeviation="2.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0.416562 0 0 0 0 0.452406 0 0 0 0 0.775 0 0 0 0.18 0"
/>
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow_4080_12482"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow_4080_12482"
result="shape"
/>
</filter>
</defs>
</svg>
);
}

@ -0,0 +1,37 @@
import { useTheme, SxProps, Box } from "@mui/material";
interface Props {
sx?: SxProps;
}
export default function ExpandIcon({ sx }: Props) {
const theme = useTheme();
return (
<Box
sx={{
...sx,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="30" height="30" rx="6" fill="#EEE4FC" />
<path
d="M22.5 11.25L15 18.75L7.5 11.25"
stroke="#7E2AEA"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -1,13 +1,20 @@
import { IconButton } from "@mui/material";
import { IconButton, SxProps } from "@mui/material";
type InfoProps = {
width?: number;
height?: number;
sx?: SxProps;
onClick?: any;
className?: string
};
export default function Info({ width = 20, height = 20 }: InfoProps) {
export default function Info({ width = 20, height = 20, sx, onClick, className }: InfoProps) {
return (
<IconButton>
<IconButton
sx={sx}
className={className}
onClick={onClick}
>
<svg
width={width}
height={height}

@ -1,4 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 -2.62268e-07C21.3137 -1.17422e-07 24 2.68629 24 6L24 18C24 21.3137 21.3137 24 18 24L6 24C2.68629 24 -9.31652e-07 21.3137 -7.86805e-07 18L-5.24537e-07 12L-2.62268e-07 6C-1.17422e-07 2.68629 2.68629 -9.31652e-07 6 -7.86805e-07L18 -2.62268e-07Z" fill="#9A9AAF" fill-opacity="0.7"/>
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" strokeLinecap="round" strokeLinejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 493 B

@ -12,7 +12,7 @@ export default function StarIconMini({ color, width = 30, sx }: Props) {
return (
<Box
sx={{
height: "30px",
height: "50px",
width: width + "px",
display: "flex",
alignItems: "center",
@ -20,7 +20,7 @@ export default function StarIconMini({ color, width = 30, sx }: Props) {
...sx,
}}
>
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width={width} height={width} viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.551 21.8375L20.851 25.8375C21.6635 26.35 22.6635 25.5875 22.426 24.65L20.601 17.475C20.5516 17.2762 20.5595 17.0674 20.6236 16.8728C20.6877 16.6781 20.8056 16.5056 20.9635 16.375L26.6135 11.6625C27.351 11.05 26.976 9.81253 26.0135 9.75003L18.6385 9.27503C18.4372 9.26332 18.2438 9.19325 18.0817 9.07338C17.9197 8.95351 17.7961 8.78902 17.726 8.60003L14.976 1.67503C14.9032 1.47491 14.7706 1.30204 14.5961 1.17988C14.4217 1.05772 14.2139 0.992188 14.001 0.992188C13.788 0.992188 13.5802 1.05772 13.4058 1.17988C13.2314 1.30204 13.0988 1.47491 13.026 1.67503L10.276 8.60003C10.2059 8.78902 10.0823 8.95351 9.92021 9.07338C9.75816 9.19325 9.5647 9.26332 9.36347 9.27503L1.98847 9.75003C1.02597 9.81253 0.650971 11.05 1.38847 11.6625L7.03847 16.375C7.19639 16.5056 7.3142 16.6781 7.37834 16.8728C7.44247 17.0674 7.45032 17.2762 7.40097 17.475L5.71347 24.125C5.42597 25.25 6.62597 26.1625 7.58847 25.55L13.451 21.8375C13.6154 21.733 13.8062 21.6775 14.001 21.6775C14.1958 21.6775 14.3866 21.733 14.551 21.8375Z"
fill={color}

@ -1,7 +1,7 @@
import { Box } from "@mui/material";
interface Props {
color: string;
color?: string;
}
export default function SettingIcon({ color }: Props) {

@ -0,0 +1,64 @@
import { useTheme, SxProps, Box } from "@mui/material";
interface Props {
sx?: SxProps;
}
export default function Trash({ sx }: Props) {
const theme = useTheme();
return (
<Box
sx={{
...sx,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.25 5.73438H3.75"
stroke="#4D4D4D"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.75 10.2344V16.2344"
stroke="#4D4D4D"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.25 10.2344V16.2344"
stroke="#4D4D4D"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M18.75 5.73438V19.9844C18.75 20.1833 18.671 20.3741 18.5303 20.5147C18.3897 20.6554 18.1989 20.7344 18 20.7344H6C5.80109 20.7344 5.61032 20.6554 5.46967 20.5147C5.32902 20.3741 5.25 20.1833 5.25 19.9844V5.73438"
stroke="#4D4D4D"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.75 5.73438V4.23438C15.75 3.83655 15.592 3.45502 15.3107 3.17371C15.0294 2.89241 14.6478 2.73438 14.25 2.73438H9.75C9.35218 2.73438 8.97064 2.89241 8.68934 3.17371C8.40804 3.45502 8.25 3.83655 8.25 4.23438V5.73438"
stroke="#4D4D4D"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -18,6 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
video: "",
},
rule: {
children: [],
main: [] as QuestionBranchingRuleMain[],
parentId: "",
default: ""

@ -10,6 +10,6 @@ export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id" | "backendId"> = {
required: false,
innerNameCheck: false,
innerName: "",
type: "all",
type: "picture",
},
};

@ -8,19 +8,10 @@ export const QUIZ_QUESTION_RESULT: Omit<QuizQuestionResult, "id" | "backendId">
type: "result",
content: {
...QUIZ_QUESTION_BASE.content,
multi: false,
own: false,
innerNameCheck: false,
video: "",
innerName: "",
required: false,
variants: [
{
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
},
],
text: "",
price: [0],
useImage: true
},
};

@ -10,4 +10,20 @@ body {
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@keyframes blinking {
0% {
opacity: 100;
}
50% {
opacity: 0;
}
100% {
opacity: 100;
}
}
.blink {
animation: blinking 2s infinite ;
}

@ -12,6 +12,8 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import lightTheme from "./utils/themes/light";
import { SWRConfig } from "swr";
import {BrowserRouter} from "react-router-dom";
dayjs.locale("ru");
@ -28,13 +30,16 @@ root.render(
<DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<App />
</SnackbarProvider>
<BrowserRouter>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<App />
</SnackbarProvider>
</BrowserRouter>
</ThemeProvider>
</LocalizationProvider>
</DndProvider>

@ -1,29 +1,22 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionBranchingRule,
QuestionHint,
PreviewRule,
} from "./shared";
export interface QuizQuestionResult extends QuizQuestionBase {
type: "result";
content: {
id: string;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
video: string;
innerName: string;
text: string;
price: [number] | [number, number];
useImage: boolean;
rule: QuestionBranchingRule,
hint: QuestionHint;
autofill: boolean;
};
}

@ -10,6 +10,7 @@ import type { QuizQuestionSelect } from "./select";
import type { QuizQuestionText } from "./text";
import type { QuizQuestionVariant } from "./variant";
import type { QuizQuestionVarImg } from "./varimg";
import type { QuizQuestionResult } from "./result";
import { nanoid } from "nanoid";
export interface QuestionBranchingRuleMain {
@ -22,6 +23,7 @@ export interface QuestionBranchingRuleMain {
}
export interface QuestionBranchingRule {
children: string[],
//список условий
main: QuestionBranchingRuleMain[];
parentId: string | null | "root";
@ -58,7 +60,6 @@ export interface QuizQuestionBase {
type?: QuestionType | null;
expanded: boolean;
openedModalSettings: boolean;
required: boolean;
deleted: boolean;
deleteTimeoutId: number;
content: {
@ -92,7 +93,8 @@ export type AnyTypedQuizQuestion =
| QuizQuestionNumber
| QuizQuestionFile
| QuizQuestionPage
| QuizQuestionRating;
| QuizQuestionRating
| QuizQuestionResult;
type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[]; };

@ -33,12 +33,19 @@ export interface QuizConfig {
startpageType: QuizStartpageType;
results: QuizResultsType;
haveRoot: string | null;
resultInfo: {
when: 'before' | 'after' | 'email',
share: true | false,
replay: true | false,
theme: string,
reply: string,
replname: string,
}
startpage: {
description: string;
button: string;
position: QuizStartpageAlignType;
favIcon: string | null;
originalFavIcon: string | null;
logo: string | null;
originalLogo: string | null;
background: {
@ -67,12 +74,19 @@ export const defaultQuizConfig: QuizConfig = {
startpageType: null,
results: null,
haveRoot: null,
resultInfo: {
when: 'after',
share: false,
replay: false,
theme: "",
reply: "",
replname: "",
},
startpage: {
description: "",
button: "",
position: "left",
favIcon: null,
originalFavIcon: null,
logo: null,
originalLogo: null,
background: {

@ -1,72 +1,87 @@
import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import SectionStyled from './SectionStyled';
import React from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import SectionStyled from "./SectionStyled";
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
import QuizLogo from "./images/icons/QuizLogo";
import { useMediaQuery, useTheme } from "@mui/material";
import { setIsContactFormOpen } from "../../stores/contactForm";
import { useUserStore } from "@root/user";
import { useNavigate, Link, useLocation } from "react-router-dom";
const buttonMenu = ['Меню 1', 'Меню 2', 'Меню 3', 'Меню 4', 'Меню 5', 'Меню 1', 'Меню 2']
const buttonMenu = ["Меню 1", "Меню 2", "Меню 3", "Меню 4", "Меню 5", "Меню 1", "Меню 2"];
export default function Component() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
const [select, setSelect] = React.useState(0)
return (
<SectionStyled tag={'header'} bg={'#F2F3F7'} mwidth={'1160px'}
sxsect={{
minHeight: '80px',
borderBottom: '1px solid #E3E3E3',
position: "relative",
padding: isMobile ? "0 16px" : isTablet ? "0 40px" : 0,
zIndex: 3
}}
sxcont={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 0
}}>
<QuizLogo width={isMobile ? 100 : 150} />
{/*<Box*/}
{/* sx={{*/}
{/* maxWidth: '595px',*/}
{/* width: '100%',*/}
{/* display: 'flex',*/}
{/* justifyContent: 'space-between',*/}
{/* flexWrap: 'wrap',*/}
{/* marginRight: "138px",*/}
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [select, setSelect] = React.useState(0);
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
const location = useLocation()
{/* }}*/}
{/*>*/}
{/* {buttonMenu.map( (element, index) => (*/}
{/* <NavMenuItem*/}
{/* text={element}*/}
{/* // component={Link}*/}
{/* // to={url}*/}
{/* key={index}*/}
{/* onClick={() => {*/}
{/* setSelect(index);*/}
{/* }}*/}
{/* isActive={select === index}*/}
{/* />*/}
{/* ))}*/}
{/*</Box>*/}
<Button variant="outlined"
onClick={() => setIsContactFormOpen(true)}
sx={{
color: 'black',
border: '1px solid black',
textTransform: 'none',
fontWeight: '400',
fontSize: '18px',
lineHeight: '24px',
borderRadius: '8px',
padding: '8px 17px',
}}
>Предрегистрация</Button>
</SectionStyled>
)
}
const onClick = () => (userId ? navigate("/list") : navigate("/signin"));
return (
<SectionStyled
tag={"header"}
bg={"#F2F3F7"}
mwidth={"1160px"}
sxsect={{
minHeight: "80px",
borderBottom: "1px solid #E3E3E3",
position: "relative",
padding: isMobile ? "0 16px" : isTablet ? "0 40px" : 0,
zIndex: 3,
}}
sxcont={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: 0,
}}
>
<QuizLogo width={isMobile ? 100 : 150} />
{/*<Box*/}
{/* sx={{*/}
{/* maxWidth: '595px',*/}
{/* width: '100%',*/}
{/* display: 'flex',*/}
{/* justifyContent: 'space-between',*/}
{/* flexWrap: 'wrap',*/}
{/* marginRight: "138px",*/}
{/* }}*/}
{/*>*/}
{/* {buttonMenu.map( (element, index) => (*/}
{/* <NavMenuItem*/}
{/* text={element}*/}
{/* // component={Link}*/}
{/* // to={url}*/}
{/* key={index}*/}
{/* onClick={() => {*/}
{/* setSelect(index);*/}
{/* }}*/}
{/* isActive={select === index}*/}
{/* />*/}
{/* ))}*/}
{/*</Box>*/}
<Button
variant="outlined"
onClick={onClick}
sx={{
color: "black",
border: "1px solid black",
textTransform: "none",
fontWeight: "400",
fontSize: "18px",
lineHeight: "24px",
borderRadius: "8px",
padding: "8px 17px",
}}
>
{userId ? "Войти" : " Регистрация / Войти"}
</Button>
</SectionStyled>
);
}

@ -1,75 +1,82 @@
import React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import {Typography, useMediaQuery, useTheme} from "@mui/material";
import abstraction from '../../assets/Quiz-main.png'
import SectionStyled from './SectionStyled';
import { Link, redirect } from 'react-router-dom';
import {setIsContactFormOpen} from "@root/contactForm";
import React from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import abstraction from "../../assets/Quiz-main.png";
import SectionStyled from "./SectionStyled";
import { Link, redirect, useNavigate } from "react-router-dom";
import { setIsContactFormOpen } from "@root/contactForm";
import { useUserStore } from "@root/user";
export default function Component() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
return(
<SectionStyled tag={'section'} bg={'#f2f3f7'} mwidth={'1160px'}
sxsect={{
height: isMobile ? '702px' : (isTablet ? '986px' : '660px'),
}}
sxcont={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: isMobile ? "300px 16px 0 16px" : (isTablet ? "494px 40px 0 40px" : 0),
marginBottom: isMobile ? '55px' : (isTablet ? 0 : "55px"),
}}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '30px',
height: '100%',
justifyContent: 'space-between',
alignItems: "flex-start",
position: 'relative',
}}
>
<Typography variant='h2'>
Pena Quiz
</Typography>
<Box
sx={{
maxWidth: isTablet ? '715px' : '420px',
minHeight: '64px',
}}>
<Typography variant='body1'>
Помогаем посетителю оставить заявку. <br style={{display: isTablet ? 'flex' : "none"}}/> Готовые шаблоны квизов с легкой установкой на любой сайт и социальные сети
</Typography>
</Box>
{/*<Link */}
{/* to="/list"*/}
{/* style={{textDecoration: "none"}}>*/}
<Button variant="contained"
onClick={() => setIsContactFormOpen(true)}
>
Попробуйте бесплатно
</Button>
{/*</Link>*/}
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const userId = useUserStore((state) => state.userId);
const navigate = useNavigate();
<Box
component={"img"}
src={abstraction}
sx={{
position: "absolute",
bottom: isMobile ? undefined : (isTablet? "138px" : "-291px"),
maxWidth: isMobile ? "403px" : "810px",
width: isMobile ? "100%" : undefined,
left: isMobile ? "-20px" : (isTablet? "54px" : "401px"),
top: isMobile ? "-345px" : undefined
}}
/>
</Box>
</SectionStyled>
)
}
const tryItForFreeonClick = () => (userId ? navigate("/list") : setIsContactFormOpen(true));
return (
<SectionStyled
tag={"section"}
bg={"#f2f3f7"}
mwidth={"1160px"}
sxsect={{
height: isMobile ? "702px" : isTablet ? "986px" : "660px",
}}
sxcont={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: isMobile ? "300px 16px 0 16px" : isTablet ? "494px 40px 0 40px" : 0,
marginBottom: isMobile ? "55px" : isTablet ? 0 : "55px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "30px",
height: "100%",
justifyContent: "space-between",
alignItems: "flex-start",
position: "relative",
}}
>
<Typography variant="h2">Pena Quiz</Typography>
<Box
sx={{
maxWidth: isTablet ? "715px" : "420px",
minHeight: "64px",
}}
>
<Typography variant="body1">
Помогаем посетителю оставить заявку. <br style={{ display: isTablet ? "flex" : "none" }} /> Готовые шаблоны
квизов с легкой установкой на любой сайт и социальные сети
</Typography>
</Box>
{/*<Link */}
{/* to="/list"*/}
{/* style={{textDecoration: "none"}}>*/}
<Button variant="contained" onClick={tryItForFreeonClick}>
Попробуйте бесплатно
</Button>
{/*</Link>*/}
<Box
component={"img"}
src={abstraction}
sx={{
position: "absolute",
bottom: isMobile ? undefined : isTablet ? "138px" : "-291px",
maxWidth: isMobile ? "403px" : "810px",
width: isMobile ? "100%" : undefined,
left: isMobile ? "-20px" : isTablet ? "54px" : "401px",
top: isMobile ? "-345px" : undefined,
}}
/>
</Box>
</SectionStyled>
);
}

@ -9,7 +9,7 @@ import Blog from './Blog';
import HowItWorks from './HowItWorks';
import BusinessPluses from './BusinessPluses';
import HowToUse from './HowToUse';
import WhatTheySay from './WhatTheySay';
import StartWithTemplates from './StartWithTemplates';
import WhatTheFeatures from './WhatTheFeatures';
import FullScreenDialog from "./headerMobileLanding";
@ -18,13 +18,14 @@ import Collaboration from "./Collaboration";
export default function Landing() {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return (
<>
<CssBaseline />
<Header/>
<Hero/>
<Counter/>
<Collaboration/>
{/* <Collaboration/> */}
<HowItWorks/>
<BusinessPluses/>
<HowToUse/>

@ -77,7 +77,7 @@ export const AnswerItem = ({
placeholder={"Добавьте ответ"}
multiline={largeCheck}
onChange={({ target }) => {
setQuestionVariantAnswer(target.value);
setQuestionVariantAnswer(target.value || " ");
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (event.code === "Enter" && !largeCheck) {
@ -124,7 +124,7 @@ export const AnswerItem = ({
style={{ margin: "10px" }}
placeholder="Подсказка для этого ответа"
value={variant.hints}
onChange={e => setQuestionVariantAnswer(e.target.value)}
onChange={e => setQuestionVariantAnswer(e.target.value || " ")}
onKeyDown={(
event: KeyboardEvent<HTMLTextAreaElement>
) => event.stopPropagation()}

@ -1,12 +1,15 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import Cytoscape from "cytoscape";
import { Button } from "@mui/material";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions"
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
import { useQuestionsStore } from "@root/questions/store";
import { cleardragQuestionContentId, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { updateOpenedModalSettingsId, } from "@root/uiTools/actions";
import { cleardragQuestionContentId } from "@root/uiTools/actions";
import { withErrorBoundary } from "react-error-boundary";
import { storeToNodes } from "./helper";
@ -20,9 +23,8 @@ import type {
AbstractEventObject,
ElementDefinition,
} from "cytoscape";
import { QuestionsList } from "../SwitchBranchingPanel/QuestionsList";
import { enqueueSnackbar } from "notistack";
import { Typography } from "@mui/material";
import { useUiTools } from "@root/uiTools/store";
type PopperItem = {
id: () => string;
@ -113,16 +115,18 @@ interface Props {
}
function CsComponent ({
function CsComponent({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
setModalQuestionTargetContentId
}: Props) {
}: Props) {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions, desireToOpenABranchingModal } = useQuestionsStore()
const { dragQuestionContentId, desireToOpenABranchingModal } = useUiTools()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
@ -144,8 +148,8 @@ function CsComponent ({
}, [desireToOpenABranchingModal])
useLayoutEffect(() => {
updateOpenedModalSettingsId()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, [])
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
@ -156,6 +160,12 @@ function CsComponent ({
}, [modalQuestionTargetContentId])
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
@ -164,11 +174,11 @@ function CsComponent ({
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed', true)
cy?.add([
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
label: targetQuestion.title || "noname"
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title
}
},
{
@ -179,28 +189,45 @@ function CsComponent ({
}
])
cy?.layout(lyopts).run()
cy?.fit(es, 200)
} else {
enqueueSnackbar("Добавляемый вопрос не найден")
}
}
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
//смотрим не добавлен ли родителю result. Если да - убираем его. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => {
if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) {
deleteQuestion(targetQuestion.id);
}
})
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.content.id, question => {
question.content.rule.parentId = parentNodeContentId
question.content.rule.main = []
})
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, question => {
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]
})
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
if (parentNodeChildren >= 1) {
if (parentQuestion.content.rule.children >= 1) {
updateOpenedModalSettingsId(targetQuestion.content.id)
} else {
//Если ребёнок первый - добавляем его родителю как дефолтный
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
}
}
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
console.log("старт удаление")
const deleteNodes = [] as string[]
const deleteEdges: any = []
const cy = cyRef?.current
@ -230,10 +257,18 @@ function CsComponent ({
updateQuestion(targetNodeContentId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.children = []
question.content.rule.default = ""
})
trashQuestions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
clearRuleForAll()
} else {
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
if (targetNodeContentId && parentQuestionContentId) {
@ -243,7 +278,7 @@ function CsComponent ({
}
//После всех манипуляций удаляем грани из CS и ноды из бекенда
//После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке
deleteNodes.forEach((nodeId) => {//Ноды
cy?.remove(cy?.$("#" + nodeId))
@ -252,7 +287,10 @@ function CsComponent ({
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
question.content.rule.children = []
})
})
deleteEdges.forEach((edge: any) => {//Грани
@ -262,32 +300,47 @@ function CsComponent ({
removeButtons(targetNodeContentId)
cy?.data('changed', true)
cy?.layout(lyopts).run()
//удаляем result всех потомков
trashQuestions.forEach((qr) => {
if (qr.type === "result") {
if (deleteNodes.includes(qr.content.rule.parentId) || qr.content.rule.parentId === targetQuestion.content.id) {
deleteQuestion(qr.id);
}
}
})
}
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
updateQuestion(targetQuestionContentId, question => {
question.content.rule.parentId = ""
question.content.rule.children = []
question.content.rule.main = []
question.content.rule.default = ""
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId)
const newRule = {}
const newChildren = [...parentQuestion.content.rule.children]
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== targetQuestionContentId) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = questions.filter((q) => {
return q.content.rule.parentId === parentQuestionContentId && q.content.id !== targetQuestionContentId
})[0]?.content.id || ""
//Если этот вопрос был дефолтным у родителя - чистим дефолт
//Смотрим можем ли мы заменить id на один из main
newRule.default = parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default
newRule.children = newChildren
console.log(newRule)
updateQuestion(parentQuestionContentId, (PQ) => {
PQ.content.rule = newRule
})
}
useEffect(() => {
if (startCreate) {
addNode({ parentNodeContentId: startCreate });
@ -330,7 +383,7 @@ function CsComponent ({
positions: (e) => {
if (!e.cy().data('changed')) {
return e.data('oldPos')
} else { e.removeData('oldPos') }
}
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
@ -361,7 +414,7 @@ function CsComponent ({
while (queue.length) {
const task = queue.pop()
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
task.parent.data('subtreeWidth', task.parent.height() + 50)
continue
}
const unprocessed = task?.children.filter(e => {
@ -377,31 +430,30 @@ function CsComponent ({
task?.parent.data('subtreeWidth', task.children.reduce((p, n) => p + n.data('subtreeWidth'), 0))
}
const pos = { x: 0, y: 0 }
e.data('oldPos', pos)
queue.push({ task: children, parent: e })
while (queue.length) {
const task = queue.pop()
const oldPos = task.parent.data('oldPos')
let yoffset = oldPos.y - task.parent.data('subtreeWidth') / 2
task.task.forEach(n => {
const width = n.data('subtreeWidth')
n.data('oldPos', { x: 250 * n.data('layer'), y: yoffset + width / 2 })
yoffset += width
queue.push({ task: n.cy().edges(`[source="${n.id()}"]`).targets(), parent: n })
})
}
e.cy().data('changed', false)
return pos
} else {
if (e.cy().data('firstNode') !== 'root') {
e.cy().data('firstNode', 'nonroot')
return { x: 0, y: 0 }
}
if (e.cy().data('firstNode') === undefined)
e.cy().data('firstNode', 'nonroot')
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
const wing = (parent.data('children') === 1) ? 0 : parent.data('subtreeWidth') / 2 + 50
const lastOffset = parent.data('lastChild')
const step = wing * 2 / (parent.data('children') - 1)
//e.removeData('subtreeWidth')
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
const pos = { x: 250 * e.data('layer'), y: (lastOffset + step) }
e.data('oldPos', pos)
return pos
} else {
parent.data('lastChild', parent.position().y - wing)
const pos = { x: 250 * e.data('layer'), y: (parent.position().y - wing) }
e.data('oldPos', pos)
return pos
const opos = e.data('oldPos')
if (opos) {
return opos
}
}
}, // map of (node id) => (position obj); or function(node){ return somPos; }
@ -412,7 +464,7 @@ function CsComponent ({
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function (node, i) { return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
animateFilter: function (node, i) { return false; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: readyLO, // callback on layoutready
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
}
@ -420,7 +472,7 @@ function CsComponent ({
useEffect(() => {
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
const eles = cy?.add(storeToNodes(questions))
const eles = cy?.add(storeToNodes(questions.filter((question: AnyTypedQuizQuestion) => (question.type !== "result" && question.type !== null))))
cy.data('changed', true)
// cy.data('changed', true)
const elecs = eles.layout(lyopts).run()
@ -586,17 +638,15 @@ function CsComponent ({
return crossElement;
},
});
const gearsPopper = node.popper({
let gearsPopper = null
if (node.data().root !== true) {
gearsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
if (item.cy().edges(`[target="${itemId}"]`).sources().length === 0) {
return;
}
const itemElement = gearsContainer.current?.querySelector(
`.popper-gear[data-id='${itemId}']`
@ -611,24 +661,25 @@ function CsComponent ({
gearElement.style.zIndex = "1"
gearsContainer.current?.appendChild(gearElement);
gearElement.addEventListener("mouseup", (e) => {
console.log("up")
updateOpenedModalSettingsId(item.id())
});
return gearElement;
},
});
}
const update = async () => {
await plusesPopper.update();
await crossesPopper.update();
await gearsPopper.update();
await gearsPopper?.update();
await layoutsPopper.update();
};
const onZoom = (event: AbstractEventObject) => {
const zoom = event.cy.zoom();
update();
//update();
crossesPopper.setOptions({
modifiers: [
@ -643,6 +694,18 @@ function CsComponent ({
{ name: "offset", options: { offset: [0, -130 * zoom] } },
],
});
plusesPopper.setOptions({
modifiers: [
{ name: "flip", options: { boundary: node } },
{ name: "offset", options: { offset: [0, 0 * zoom] } },
],
});
gearsPopper?.setOptions({
modifiers: [
{ name: "flip", options: { boundary: node } },
{ name: "offset", options: { offset: [0, 0] } },
],
});
layoutsContainer.current
?.querySelectorAll("#popper-layouts > .popper-layout")
@ -672,7 +735,7 @@ function CsComponent ({
element.style.borderRadius = `${6 * zoom}px`;
});
gearsContainer.current
gearsContainer?.current
?.querySelectorAll("#popper-gears > .popper-gear")
.forEach((item) => {
const element = item as HTMLDivElement;
@ -681,13 +744,63 @@ function CsComponent ({
});
};
node?.on("position", update);
cy?.on("pan zoom resize render", onZoom);
//node?.on("position", update);
let pressed = false
let hide = false
cy?.on('mousedown', () => { pressed = true })
cy?.on('mouseup', () => {
pressed = false
hide = false
const gc = gearsContainer.current
if (gc) gc.style.display = 'block'
const pc = plusesContainer.current
const xc = crossesContainer.current
const lc = layoutsContainer.current
if (pc) pc.style.display = 'block'
if (xc) xc.style.display = 'block'
if (lc) lc.style.display = 'block'
update()
})
cy?.on('mousemove', () => {
if (pressed && !hide) {
hide = true
const gc = gearsContainer.current
if (gc) gc.style.display = 'none'
const pc = plusesContainer.current
const xc = crossesContainer.current
const lc = layoutsContainer.current
if (pc) pc.style.display = 'none'
if (xc) xc.style.display = 'none'
if (lc) lc.style.display = 'block'
}
});
cy?.on("zoom render", onZoom);
});
};
return (
<>
<Button
sx={{
mb: "20px",
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
onClick={() => {
cyRef.current?.fit()
}}
>
Выровнять
</Button>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
@ -698,9 +811,9 @@ function CsComponent ({
cy={(cy) => {
cyRef.current = cy;
}}
// autolock
autoungrabify={true}
/>
<button onClick={() => {
{/* <button onClick={() => {
console.log("NODES____________________________")
cyRef.current?.elements().forEach((ele: any) => {
console.log(ele.data())
@ -709,23 +822,23 @@ function CsComponent ({
<button onClick={() => {
console.log("ELEMENTS____________________________")
console.log(questions)
}}>elements</button>
}}>elements</button> */}
</>
);
};
function Clear () {
const quiz = useCurrentQuiz();
updateRootContentId(quiz.id, "")
clearRuleForAll()
return <></>
function Clear() {
const quiz = useCurrentQuiz();
updateRootContentId(quiz.id, "")
clearRuleForAll()
return <></>
}
export default withErrorBoundary(CsComponent, {
fallback: <Clear/>,
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось")
console.log(info)
console.log(error)
},
});
});

@ -1,10 +1,12 @@
import { Box } from "@mui/material"
import { useEffect, useRef, useState } from "react";
import { updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
import { useEffect, useRef, useLayoutEffect } from "react";
import { deleteQuestion, clearRuleForAll, updateQuestion } from "@root/questions/actions"
import { updateOpenedModalSettingsId } from "@root/uiTools/actions"
import { updateRootContentId } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useQuestionsStore } from "@root/questions/store"
import { enqueueSnackbar } from "notistack";
import { useUiTools } from "@root/uiTools/store";
interface Props {
setOpenedModalQuestions: (open: boolean) => void;
@ -12,7 +14,18 @@ interface Props {
}
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions } = useQuestionsStore()
useLayoutEffect(() => {
updateOpenedModalSettingsId()
console.log("first render firstComponent")
updateRootContentId(quiz.id, "")
clearRuleForAll()
}, [])
const { questions } = useQuestionsStore()
const { dragQuestionContentId } = useUiTools()
const Container = useRef<HTMLDivElement | null>(null);
const modalOpen = () => setOpenedModalQuestions(true)
@ -22,6 +35,11 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
if (dragQuestionContentId) {
updateRootContentId(quiz?.id, dragQuestionContentId)
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
//если были результаты - удалить
questions.forEach((q) => {
if (q.type === 'result') deleteQuestion(q.id)
})
}
} else {
enqueueSnackbar("Нет информации о взятом опроснике")
@ -44,6 +62,10 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
if (modalQuestionTargetContentId) {
updateRootContentId(quiz?.id, modalQuestionTargetContentId)
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
//если были результаты - удалить
questions.forEach((q) => {
if (q.type === 'result') deleteQuestion(q.id)
})
}
} else {
enqueueSnackbar("Нет информации о взятом опроснике")

@ -21,7 +21,7 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
if (question.content.rule.parentId) {
nodes.push({data: {
id: question.content.id,
label: question.title ? question.title : "noname"
label: question.title === "" || question.title === " " ? "noname" : question.title
}})
// nodes.push({
// data: {

@ -1,15 +1,15 @@
import { Box } from "@mui/material";
import { FirstNodeField } from "./FirstNodeField";
import CsComponent from "./CsComponent";
import { useQuestionsStore } from "@root/questions/store"
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useState } from "react";
import {BranchingQuestionsModal} from "../BranchingQuestionsModal"
import { useUiTools } from "@root/uiTools/store";
export const BranchingMap = () => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId } = useQuestionsStore()
const { dragQuestionContentId } = useUiTools()
const [modalQuestionParentContentId, setModalQuestionParentContentId] = useState<string>("")
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
@ -26,7 +26,7 @@ export const BranchingMap = () => {
borderRadius: "12px",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
marginBottom: "40px",
height: "521px",
height: "568px",
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed"
}}
>

@ -22,15 +22,16 @@ import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { TypeSwitch, BlockRule } from "./Settings";
import { getQuestionById, getQuestionByContentId, updateOpenedModalSettingsId, updateQuestion } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { getQuestionById, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { enqueueSnackbar } from "notistack";
export default function BranchingQuestions() {
const theme = useTheme();
const { openedModalSettingsId } = useQuestionsStore();
const { openedModalSettingsId } = useUiTools();
const [targetQuestion, setTargetQuestion] = useState<AnyTypedQuizQuestion | null>(getQuestionById(openedModalSettingsId) || getQuestionByContentId(openedModalSettingsId))
const [parentQuestion, setParentQuestion] = useState<AnyTypedQuizQuestion | null>(getQuestionByContentId(targetQuestion?.content.rule.parentId))
@ -165,7 +166,7 @@ export default function BranchingQuestions() {
onClick={() => {
let mutate = JSON.parse(JSON.stringify(parentQuestion))
mutate.content.rule.default = targetQuestion.id
mutate.content.rule.default = parentQuestion.content.rule.default === targetQuestion.content.id ? "" : targetQuestion.content.id
setParentQuestion(mutate)
}}
/>} label="Следующий вопрос по-умолчанию" />

@ -6,15 +6,17 @@ import { useState, useRef, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useQuestionsStore } from "@root/questions/store";
import { updateQuestion, getQuestionById } from "@root/questions/actions";
import { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
import { SelectChangeEvent } from '@mui/material/Select';
import CalendarIcon from "@icons/CalendarIcon";
import { DatePicker } from "@mui/x-date-pickers";
import * as dayjs from 'dayjs'
import dayjs from 'dayjs'
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"
const CONDITIONS = [
"Все условия обязательны",
"Обязательно хотя бы одно условие",
@ -289,12 +291,12 @@ const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQu
</Typography>
}
<DatePicker
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
defaultValue={dayjs(new Date(parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]).toLocaleDateString())}
onChange={(dateString) => {
const date = dateString?.$d?.toLocaleDateString("ru-RU", { year: "numeric", month: "2-digit", day: "2-digit" });
const date = dateString?.toDate().toLocaleDateString("ru-RU", { year: "numeric", month: "2-digit", day: "2-digit" });
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [date]
// setParentQuestion(newParentQuestion)
setParentQuestion(newParentQuestion)
}}
slots={{
openPickerIcon: () => <CalendarIcon />,
@ -514,47 +516,34 @@ const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParent
(Укажите один или несколько вариантов)
</Typography>
</Box>
<TextField
sx={{
marginTop: "20px",
width: "100%"
}}
placeholder="от"
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = 0
setParentQuestion(newParentQuestion)
}}
/>
{parentQuestion.content.chooseRange &&
<Box>
<TextField
placeholder="до"
sx={{
marginTop: "20px",
width: "100%"
}}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = 0
sx={{ marginTop: "20px", width: "100%" }}
placeholder="от"
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[0]}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] =(parentQuestion as QuizQuestionNumber).content.chooseRange ? previousValue ? `${target.value}${previousValue.split("—")[1] || 0}` : `${target.value}—0` : target.value;
setParentQuestion(newParentQuestion)
}}
/>
{(parentQuestion as QuizQuestionNumber).content.chooseRange &&
<TextField
placeholder="до"
sx={{ marginTop: "20px", width: "100%" }}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[1]}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = previousValue ? `${previousValue.split("—")[0] || 0}${target.value}` : `0—${target.value}`
setParentQuestion(newParentQuestion)
}
}}
/>
}
</Box>
</Box >
)
}

@ -1,15 +1,16 @@
import {Box, Typography, Switch, useTheme, Button, useMediaQuery, SxProps, Theme} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { updateOpenBranchingPanel } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store";
import {useRef} from "react";
import { useUiTools } from "@root/uiTools/store";
export const BranchingPanel = (sx?: SxProps<Theme>) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const {openBranchingPanel} = useQuestionsStore.getState()
const {openBranchingPanel} = useUiTools()
const ref = useRef()
return (
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
@ -26,10 +27,10 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
updateOpenBranchingPanel(value)
}}
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
@ -76,7 +77,7 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
/>
<Box>
<Typography ref={ref} sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
Логика ветвления
</Typography>
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
Настройте связи между вопросами

@ -13,18 +13,22 @@ export const BranchingQuestionsModal = ({
openedModalQuestions,
setOpenedModalQuestions,
setModalQuestionTargetContentId,
setModalQuestionParentContentId
setModalQuestionParentContentId,
}: Props) => {
const { questions } = useQuestionsStore();
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result"
);
const handleClose = () => {
setOpenedModalQuestions(false);
};
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
(question) => question.type && !question.content.rule.parentId
(question) => question.type && !question.content.rule.parentId && question.type !== "result"
) as AnyTypedQuizQuestion[];
if (typedQuestions.length === 0) return <></>
return (
<Modal open={openedModalQuestions} onClose={handleClose}>
<Box
@ -61,7 +65,7 @@ export const BranchingQuestionsModal = ({
borderRadius: "8px",
marginBottom: "20px",
boxShadow: "0px 10px 30px #e7e7e7",
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' strokeWidth='2' stroke-dasharray='8 8' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e");
border-radius: 8px;`,
"&:last-child": { marginBottom: 0 },
}}

@ -11,7 +11,8 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, deleteQuestion, updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/questions/actions";
import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout, clearRuleForAll, updateQuestion, getQuestionByContentId } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal, } from "@root/uiTools/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching";
@ -22,7 +23,9 @@ import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@root/questions/store";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useUiTools } from "@root/uiTools/store";
interface Props {
switchState: string;
@ -40,8 +43,13 @@ export default function ButtonsOptions({
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz();
const { questions } = useQuestionsStore.getState();
const openedModal = () => {
updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
};
const { openBranchingPanel } = useQuestionsStore.getState()
const buttonSetting: {
icon: JSX.Element;
@ -60,15 +68,15 @@ export default function ButtonsOptions({
title: "Настройки",
value: "setting",
},
{
icon: (
<Clue
color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
/>
),
title: "Подсказка",
value: "help",
},
// {
// icon: (
// <Clue
// color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
// />
// ),
// title: "Подсказка",
// value: "help",
// },
{
icon: (
<Branching
@ -80,8 +88,9 @@ export default function ButtonsOptions({
title: "Ветвление",
value: "branching",
myFunc: (question) => {
updateOpenBranchingPanel(true)
updateDesireToOpenABranchingModal(question.content.id)
console.log("buttons opiums")
updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
}
},
];
@ -157,8 +166,9 @@ export default function ButtonsOptions({
<MiniButtonSetting
key={title}
onClick={() => {
SSHC(value);
myFunc(question);
openedModal();
// SSHC(value);
// myFunc(question);
}}
sx={{
backgroundColor:
@ -264,24 +274,67 @@ export default function ButtonsOptions({
</IconButton>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { // TODO
// const removedId = question.id;
// if (question.deleteTimeoutId) {
// clearTimeout(question.deleteTimeoutId);
// }
onClick={() => {
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
// removeQuestion(quizId, totalIndex);
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
});
// const newTimeoutId = window.setTimeout(() => {
// removeQuestionForce(quizId, removedId);
// }, 5000);
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
const newRule = {};
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
// ...question,
// deleteTimeoutId: newTimeoutId,
// });
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id, quiz.id);
deleteQuestion(question.id);
} else {
console.log("удаляю безтипогово");
deleteQuestion(question.id);
}
};
deleteQuestionWithTimeout(question.id, deleteFn);
}}
data-cy="delete-question"
>

@ -10,7 +10,7 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, deleteQuestion, updateQuestion } from "@root/questions/actions";
import { copyQuestion, deleteQuestion, updateQuestion, clearRuleForAll, getQuestionByContentId, deleteQuestionWithTimeout } from "@root/questions/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { useEffect, useState } from "react";
@ -23,10 +23,11 @@ import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import { QuizQuestionVariant } from "@model/questionTypes/variant";
import { updateOpenedModalSettingsId } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
import { useQuestionsStore } from "@root/questions/store";
import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
interface Props {
@ -46,7 +47,7 @@ export default function ButtonsOptionsAndPict({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const { openBranchingPanel } = useQuestionsStore.getState()
const { questions } = useQuestionsStore.getState();
const quiz = useCurrentQuiz();
useEffect(() => {
@ -190,8 +191,9 @@ export default function ButtonsOptionsAndPict({
onMouseEnter={() => setButtonHover("branching")}
onMouseLeave={() => setButtonHover("")}
onClick={() => {
updateOpenBranchingPanel(true)
updateDesireToOpenABranchingModal(question.content.id)
console.log("buttonsOptions")
updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
}}
sx={{
height: "30px",
@ -306,24 +308,67 @@ export default function ButtonsOptionsAndPict({
</IconButton>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { // TODO
// const removedId = question.id;
// if (question.deleteTimeoutId) {
// clearTimeout(question.deleteTimeoutId);
// }
onClick={() => {
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
// removeQuestion(quizId, totalIndex);
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
});
// const newTimeoutId = window.setTimeout(() => {
// removeQuestionForce(quizId, removedId);
// }, 5000);
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
const newRule = {};
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
// ...question,
// deleteTimeoutId: newTimeoutId,
// });
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id, quiz?.id);
deleteQuestion(question.id);
} else {
console.log("удаляю безтипогово");
deleteQuestion(question.id);
}
};
deleteQuestionWithTimeout(question.id, deleteFn);
}}
data-cy="delete-question"
>

@ -29,7 +29,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
flexDirection: isWrappColumn ? "column" : null,
}}
>
<Box
{/* <Box
sx={{
pt: "20px",
pb: isMobile ? "25px" : "20px",
@ -67,7 +67,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
});
}}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
@ -86,10 +86,10 @@ export default function SettingsData({ question }: SettingsDataProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.required = !target.checked;
updateQuestion<QuizQuestionDate>(question.id, question => {
question.content.required = !target.checked;
});
}}
/>
@ -109,7 +109,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionDate>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});
@ -125,7 +125,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => setInnerName(target.value)}
onChange={({ target }) => setInnerName(target.value || " ")}
/>
)}
</Box>

@ -3,8 +3,10 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { memo, useEffect } from "react";
import { Draggable } from "react-beautiful-dnd";
import QuestionsPageCard from "./QuestionPageCard";
import { updateEditSomeQuestion } from "@root/questions/actions"
import { useQuestionsStore } from "@root/questions/store"
import { cancelQuestionDeletion } from "@root/questions/actions";
import { updateEditSomeQuestion } from "@root/uiTools/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
type Props = {
@ -15,23 +17,21 @@ type Props = {
function DraggableListItem({ question, isDragging, index }: Props) {
const theme = useTheme();
const { editSomeQuestion } = useQuestionsStore()
const { editSomeQuestion } = useUiTools();
useEffect(() => {
if (editSomeQuestion !== null) {
const setI = setInterval(() => {
let comp = document.getElementById(editSomeQuestion)
console.log(comp)
if(comp !== null) {
clearInterval(setI)
comp.scrollIntoView({behavior: 'instant'})
updateEditSomeQuestion()
}
}, 200)
let comp = document.getElementById(editSomeQuestion);
if (comp !== null) {
clearInterval(setI);
comp.scrollIntoView({ behavior: 'instant' });
updateEditSomeQuestion();
}
}, 200);
}
console.log(editSomeQuestion)
}, [editSomeQuestion])
}, [editSomeQuestion]);
return (
<Draggable draggableId={question.id.toString()} index={index}>
@ -62,11 +62,8 @@ function DraggableListItem({ question, isDragging, index }: Props) {
Вопрос удалён.
</Typography>
<Typography
onClick={() => { // TODO
// updateQuestionsList<QuizQuestionBase>(quizId, index, {
// ...questionData,
// deleted: false,
// });
onClick={() => {
cancelQuestionDeletion(question.id);
}}
sx={{
cursor: "pointer",

@ -29,7 +29,8 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, createUntypedQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
import { copyQuestion, createUntypedQuestion, deleteQuestion, clearRuleForAll, toggleExpandQuestion, updateQuestion, updateUntypedQuestion, getQuestionByContentId, deleteQuestionWithTimeout } from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce";
@ -39,7 +40,8 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -49,6 +51,7 @@ interface Props {
}
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
const { questions } = useQuestionsStore();
const [plusVisible, setPlusVisible] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false);
const theme = useTheme();
@ -68,7 +71,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
return (
<>
<Paper
id={question.id}
id={question.id}
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
@ -99,7 +102,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
<TextField
defaultValue={question.title}
placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value)}
onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value || " ")}
InputProps={{
startAdornment: (
<Box>
@ -238,24 +241,67 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
padding: "0",
margin: "0 5px 0 10px",
}}
onClick={() => { // TODO
const removedId = question.id;
// if (question.deleteTimeoutId) {
// clearTimeout(question.deleteTimeoutId);
// }
onClick={() => {
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
// removeQuestion(quizId, totalIndex);
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
});
// const newTimeoutId = window.setTimeout(() => {
// removeQuestionForce(quizId, removedId);
// }, 5000);
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
const newRule = {};
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId;
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
// updateQuestionsList<AnyTypedQuizQuestion>(quizId, totalIndex, {
// ...question,
// deleteTimeoutId: newTimeoutId,
// });
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id, quiz.id);
deleteQuestion(question.id);
} else {
console.log("удаляю безтипогово");
deleteQuestion(question.id);
}
};
deleteQuestionWithTimeout(question.id, deleteFn);
}}
data-cy="delete-question"
>
@ -265,26 +311,28 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
</IconButton>
</Box>
)}
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
>
{index + 1}
</Box>
{question.type !== null &&
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
>
{question.page + 1}
</Box>
}
<IconButton
disableRipple
sx={{
@ -327,7 +375,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
}}
>
<Box
onClick={() => createUntypedQuestion(question.quizId)}
onClick={() => createUntypedQuestion(question.quizId, question.id)}
sx={{
display: plusVisible && !isDragging ? "flex" : "none",
width: "100%",

@ -1,26 +1,24 @@
import { Box } from "@mui/material";
import { reorderQuestions } from "@root/questions/actions";
import { useQuestions } from "@root/questions/hooks";
import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import DraggableListItem from "./DraggableListItem";
import { useQuestionsStore } from "@root/questions/store";
export const DraggableList = () => {
const { questions, isLoading } = useQuestions();
const { questions } = useQuestionsStore()
const filteredQuestions = questions.filter((question) => question.type !== "result")
const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index);
};
if (isLoading && !questions) return <Box>Загрузка вопросов...</Box>;
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable-list">
{(provided, snapshot) => (
<Box ref={provided.innerRef} {...provided.droppableProps}>
{questions.map((question, index) => (
{filteredQuestions.map((question, index) => (
<DraggableListItem
key={question.id}
question={question}

@ -58,7 +58,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
maxWidth: isFigmaTablte ? "297px" : "360px",
}}
>
<Typography
{/* <Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
@ -79,7 +79,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
question.content.multi = target.checked;
})
}
/>
/> */}
<Box
sx={{
display: isMobile ? "none" : "block",
@ -128,10 +128,10 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
</Typography>
<CustomCheckbox
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionSelect>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -141,7 +141,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionSelect>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});
@ -184,7 +184,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
<CustomTextField
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => debounced(target.value)}
onChange={({ target }) => debounced(target.value || " ")}
/>
)}
</Box>

@ -30,7 +30,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
flexDirection: isWrappColumn ? "column" : "none",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -66,12 +66,12 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
question.content.own = target.checked;
})}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isTablet ? "20px" : "",
pl: "20px",
pr: isFigmaTablte ? "30px" : "20px",
display: "flex",
flexDirection: "column",
@ -85,11 +85,11 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
handleChange={(e) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
if (question.type !== "emoji") return;
question.content.required = !e.target.checked;
question.content.required = !target.checked;
})}
/>
<Box
@ -107,7 +107,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestion(question.id, question => {
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
})}

@ -1,14 +1,15 @@
import { Box } from "@mui/material";
import { reorderQuestions } from "@root/questions/actions";
import { useQuestions } from "@root/questions/hooks";
import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import FormDraggableListItem from "./FormDraggableListItem";
import { useQuestions } from "@root/questions/hooks";
export const FormDraggableList = () => {
const { questions } = useQuestions();
const { questions } = useQuestions()
const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index);
};
@ -18,7 +19,7 @@ export const FormDraggableList = () => {
<Droppable droppableId="droppable-list">
{(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}>
{questions.map((question, index) => (
{questions?.map((question, index) => (
<FormDraggableListItem
key={question.id}
question={question}

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

@ -111,11 +111,11 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={question.content.required}
handleChange={({ target }) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
if (question.type !== "varimg") return;
question.content.required = target.checked;
question.content.required = !target.checked;
})}
/>
<Box sx={{ display: "flex", alignItems: "center" }}>
@ -126,7 +126,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => updateQuestion(question.id, question => {
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = "";
})}

@ -64,7 +64,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -148,7 +148,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
})
}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "25px" : "20px",
@ -161,7 +161,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
width: "100%",
}}
>
<Box
{/* <Box
sx={{
marginBottom: "5px",
opacity: question.content.xy !== "1:1" ? 1 : 0,
@ -202,7 +202,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
isActive={question.content.format === "masonry"}
Icon={FormatIcon1}
/>
</Box>
</Box> */}
<Typography
sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
>
@ -211,11 +211,11 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
<CustomCheckbox
sx={{ alignItems: isMobile ? "flex-start" : "" }}
label={"Необязательный вопрос"}
checked={question.content.required}
handleChange={({ target }) => updateQuestion(question.id, question => {
checked={!question.content.required}
handleChange={({ target }) => updateQuestion<QuizQuestionImages>(question.id, question => {
if (question.type !== "images") return;
question.content.required = target.checked;
question.content.required = !target.checked;
})
}
/>

@ -54,7 +54,7 @@ export default function SettingTextField({
marginRight: isFigmaTablte ? "0px" : "32px",
}}
>
<Box
{/* <Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
@ -126,7 +126,7 @@ export default function SettingTextField({
});
}}
/>
</Box>
</Box> */}
<Box
sx={{
pt: isMobile ? "0px" : "20px",
@ -148,7 +148,7 @@ export default function SettingTextField({
>
Настройки вопросов
</Typography>
<CustomCheckbox
{/* <CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
@ -161,7 +161,7 @@ export default function SettingTextField({
question.content.autofill = target.checked;
});
}}
/>
/> */}
<CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
@ -169,10 +169,10 @@ export default function SettingTextField({
alignItems: isMobile ? "flex-end" : "center",
}}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionText>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -193,7 +193,7 @@ export default function SettingTextField({
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionText>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked
? question.content.innerName

@ -1,18 +1,23 @@
import {
Box,
} from "@mui/material";
Box, useMediaQuery, useTheme,
} from "@mui/material";
import { DraggableList } from "./DraggableList";
import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
import { BranchingMap } from "./BranchingMap";
import {useQuestionsStore} from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
export const QuestionSwitchWindowTool = () => {
const {openBranchingPanel} = useQuestionsStore.getState()
console.log(openBranchingPanel)
const {questions} = useQuestionsStore.getState()
const {openBranchingPanel} = useUiTools()
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
console.log("questions ", questions)
console.log("rules ", questions.filter((q) => q.type !== null).map((q) => ({id: q.content.id, rule: q.content.rule})))
return (
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap", marginBottom: isMobile ? "20px" : undefined }}>
<Box sx={{ flexBasis: "796px" }}>
{openBranchingPanel? <BranchingMap /> : <DraggableList />}
</Box>

@ -17,15 +17,14 @@ import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal"
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
import { useQuestionsStore } from "@root/questions/store";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
export default function QuestionsPage() {
const theme = useTheme();
const { openedModalSettingsId, openBranchingPanel } = useQuestionsStore();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const { openedModalSettingsId, openBranchingPanel } = useUiTools();
const isMobile = false//useMediaQuery(theme.breakpoints.down(660));
const quiz = useCurrentQuiz();
console.log(quiz)
useLayoutEffect(() => {
updateOpenBranchingPanel(false)
updateEditSomeQuestion()
@ -48,7 +47,8 @@ export default function QuestionsPage() {
margin: "60px 0 40px 0",
}}
>
<Typography variant={"h5"}>Заголовок квиза</Typography>
<Typography variant={"h5"}>{
quiz.name ? quiz.name : "Заголовок квиза" }</Typography>
<Button
sx={{
display: openBranchingPanel ? "none" : "flex",
@ -115,4 +115,4 @@ export default function QuestionsPage() {
{openedModalSettingsId !== null && <BranchingQuestions/>}
</>
);
}
}

@ -148,12 +148,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<CustomCheckbox
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionRating>(question.id, question => {
if (question.type !== "rating") return;
question.required = !e.target.checked;
question.content.required = !e.target.checked;
});
}}
/>

@ -3,6 +3,7 @@ import {
Select as MuiSelect,
MenuItem,
FormControl,
Typography,
useTheme,
} from "@mui/material";
@ -16,6 +17,7 @@ type SelectProps = {
empty?: boolean;
onChange?: (item: string, num: number) => void;
sx?: SxProps;
placeholder?: string;
};
export const Select = ({
@ -24,6 +26,7 @@ export const Select = ({
empty,
onChange,
sx,
placeholder = "",
}: SelectProps) => {
const [activeItem, setActiveItem] = useState<number>(
empty ? -1 : activeItemIndex
@ -35,9 +38,17 @@ export const Select = ({
}, [activeItemIndex]);
const handleChange = (event: SelectChangeEvent) => {
const activeItemIndex = Number(event.target.value);
setActiveItem(activeItemIndex);
onChange?.(items[activeItemIndex], activeItemIndex);
const newItemIndex = Number(event.target.value);
if (newItemIndex === activeItem) {
setActiveItem(-1);
onChange?.("", -1);
return;
}
setActiveItem(newItemIndex);
onChange?.(items[newItemIndex], newItemIndex);
};
return (
@ -47,10 +58,19 @@ export const Select = ({
sx={{ width: "100%", height: "48px", ...sx }}
>
<MuiSelect
displayEmpty
renderValue={(value) =>
value ? (
items[Number(value)]
) : (
<Typography sx={{ color: theme.palette.grey2.main }}>
{placeholder}
</Typography>
)
}
id="display-select"
variant="outlined"
value={activeItem === -1 ? "" : String(activeItem)}
displayEmpty
onChange={handleChange}
sx={{
width: "100%",

@ -49,7 +49,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
height: isMobile ? "100%" : "auto",
alignItems: isMobile ? "flex-start" : "center",
}}
label={"Выбор диапозона (два ползунка)"}
label={"Выбор диапазона (два ползунка)"}
checked={question.content.chooseRange}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
@ -78,12 +78,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionNumber>(question.id, question => {
if (question.type !== "number") return;
question.required = !e.target.checked;
question.content.required = !e.target.checked;
});
}}
/>

@ -2,12 +2,18 @@ import { useParams } from "react-router-dom";
import { Box, Button, IconButton, Typography } from "@mui/material";
import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
import { useQuestionsStore } from "@root/questions/store";
import { updateDragQuestionContentId } from "@root/questions/actions";
import { useEffect } from "react";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
import {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "@model/questionTypes/shared";
import { Pencil } from "../../startPage/Sidebar/icons/Pencil";
import {updateOpenBranchingPanel, updateEditSomeQuestion} from "@root/questions/actions"
import {
updateOpenBranchingPanel,
updateEditSomeQuestion,
updateDragQuestionContentId,
} from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
const getItemStyle = (isDragging: any, draggableStyle: any) => ({
// some basic styles to make the items look a bit nicer
@ -19,12 +25,16 @@ const getItemStyle = (isDragging: any, draggableStyle: any) => ({
background: isDragging ? "lightgreen" : "grey",
// styles we need to apply on draggables
...draggableStyle
...draggableStyle,
});
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion;
export const QuestionsList = () => {
const { questions, desireToOpenABranchingModal } = useQuestionsStore()
const { desireToOpenABranchingModal } = useUiTools();
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result"
);
return (
<Box sx={{ padding: "15px" }}>
@ -53,47 +63,56 @@ export const QuestionsList = () => {
}}
>
{/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/}
{questions.filter((q: AnyQuestion) => q.type).map(({ title, content }, index) => (
<Button
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
if (!content.rule.parentId) updateDragQuestionContentId(content.id)
}}
key={index}
sx={{
width: "100%",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px",
background: "#FFFFFF",
borderRadius: "8px",
border: desireToOpenABranchingModal === content.id ? "4px solid #7e2aea" : "none",
marginBottom: "20px",
boxShadow: "0px 10px 30px #e7e7e7",
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
{questions
.filter((q: AnyQuestion) => q.type)
.map(({ title, content }, index) => (
<Button
onMouseDown={() => {
//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
if (!content.rule.parentId)
updateDragQuestionContentId(content.id);
}}
key={index}
sx={{
width: "100%",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px",
background: "#FFFFFF",
borderRadius: "8px",
border:
desireToOpenABranchingModal === content.id
? "4px solid #7e2aea"
: "none",
marginBottom: "20px",
boxShadow: "0px 10px 30px #e7e7e7",
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' strokeWidth='2' stroke-dasharray='8 8' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e");
border-radius: 8px;`,
"&:last-child": { marginBottom: 0 },
}}
>
<Typography sx={{ width: "100%", color: content.rule.parentId ? "#9A9AAF" : "#000" }}>
{title || "нет заголовка"}
</Typography>
<IconButton
onClick={() => {
updateOpenBranchingPanel(false)
updateEditSomeQuestion(content.id)
"&:last-child": { marginBottom: 0 },
}}
>
<Pencil />
</IconButton>
{content.rule.parentId && <CheckedIcon />}
</Button>
))}
<Typography
sx={{
width: "100%",
color: content.rule.parentId ? "#9A9AAF" : "#000",
}}
>
{title || "нет заголовка"}
</Typography>
<IconButton
onClick={() => {
updateOpenBranchingPanel(false);
updateEditSomeQuestion(content.id);
}}
>
<Pencil style={{ color: "#7e2aea" }} />
</IconButton>
{content.rule.parentId && <CheckedIcon />}
</Button>
))}
</Box>
</Box>
);
}
};

@ -1,18 +1,23 @@
import {Box, Typography, Switch, useTheme, Button, useMediaQuery} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { updateOpenBranchingPanel } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store";
import {useRef} from "react";
import { useUiTools } from "@root/uiTools/store";
export const SwitchBranchingPanel = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const {openBranchingPanel} = useQuestionsStore.getState()
console.log(openBranchingPanel)
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const {openBranchingPanel} = useUiTools()
const ref = useRef()
return (
return ( !isTablet || openBranchingPanel ?
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
<Box
sx={{
@ -26,11 +31,10 @@ export const SwitchBranchingPanel = () => {
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
console.log("меняю на " + value)
updateOpenBranchingPanel(value)
}}
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
@ -87,5 +91,7 @@ export const SwitchBranchingPanel = () => {
</Box>
{ openBranchingPanel && <QuestionsList /> }
</Box>
:
<></>
);
};

@ -27,7 +27,7 @@ type DesignItem = {
};
const DESIGN_TYPES: DesignItem[] = [
{ name: "Все типы файлов", value: "all" },
// { name: "Все типы файлов", value: "all" },
{ name: "Изображения", value: "picture" },
{ name: "Видео", value: "video" },
{ name: "Аудио", value: "audio" },

@ -40,7 +40,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
}}
>
<Typography>Настройки вопроса</Typography>
<CustomCheckbox
{/* <CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
@ -52,17 +52,17 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
question.content.autofill = target.checked;
});
}}
/>
/> */}
<CustomCheckbox
sx={{
display: isMobile ? "flex" : "block",
mr: isMobile ? "0px" : "16px",
}}
label={"Необязательный вопрос"}
checked={!question.required}
checked={!question.content.required}
handleChange={(e) => {
updateQuestion(question.id, question => {
question.required = !e.target.checked;
updateQuestion<QuizQuestionFile>(question.id, question => {
question.content.required = !e.target.checked;
});
}}
/>
@ -82,7 +82,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
updateQuestion<QuizQuestionFile>(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});

@ -99,7 +99,7 @@ export const UploadVideoModal = ({
<CustomTextField
placeholder={"http://example.com"}
text={video}
onChange={({ target }) => onUpload(target.value)}
onChange={({ target }) => onUpload(target.value || " ")}
/>
</Box>
) : (
@ -114,7 +114,7 @@ export const UploadVideoModal = ({
<input
onChange={({ target }) => {
if (target.files?.length) {
onUpload(URL.createObjectURL(target.files[0]));
onUpload(URL.createObjectURL(target.files[0] || " "));
}
}}
hidden

@ -1,9 +1,9 @@
import {
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
@ -12,165 +12,166 @@ import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
interface Props {
question: QuizQuestionVariant;
question: QuizQuestionVariant;
}
export default function ResponseSettings({ question }: Props) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value);
}, 200);
const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value);
}, 200);
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : "none",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
flexDirection: isTablet ? "column" : "none",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
}}
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
<Box
sx={{
pt: isMobile ? "25px" : "20px",
pb: isMobile ? "25px" : "20px",
pl: "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
Настройки ответов
</Typography>
{/* <CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
checked={question.content.largeCheck}
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("largeCheck" in question.content)) return;
question.content.largeCheck = target.checked;
});
}}
/> */}
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={question.content.multi}
dataCy="multiple-answers-checkbox"
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("multi" in question.content)) return;
question.content.multi = target.checked;
});
}}
/>
{/* <CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={question.content.own}
handleChange={({ target }) => {
updateQuestion(question.id, (question) => {
if (!("own" in question.content)) return;
question.content.own = target.checked;
});
}}
/> */}
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.content.required}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.required = !target.checked;
});
}}
/>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{
mr: isMobile ? "0px" : "9px",
height: isMobile ? "42px" : "26px",
alignItems: "start",
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked
? question.content.innerName
: "";
});
}}
/>
{isMobile && (
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки ответов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Длинный текстовый ответ"}
checked={question.content.largeCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("largeCheck" in question.content)) return;
question.content.largeCheck = target.checked;
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Можно несколько"}
checked={question.content.multi}
dataCy="multiple-answers-checkbox"
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("multi" in question.content)) return;
question.content.multi = target.checked;
});
}}
/>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={'Вариант "свой ответ"'}
checked={question.content.own}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
if (!("own" in question.content)) return;
question.content.own = target.checked;
});
}}
/>
</Box>
<Box
sx={{
boxSizing: "border-box",
pt: isMobile ? "0px" : "20px",
pb: "20px",
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
pr: isFigmaTablte ? "19px" : "20px",
display: "flex",
flexDirection: "column",
gap: "14px",
width: "100%",
}}
>
<Typography
sx={{
height: isMobile ? "18px" : "auto",
fontWeight: "500",
fontSize: "18px",
color: " #4D4D4D",
}}
>
Настройки вопросов
</Typography>
<CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px" }}
label={"Необязательный вопрос"}
checked={!question.required}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.required = !target.checked;
});
}}
/>
<Box
sx={{
width: isMobile ? "90%" : "auto",
display: "flex",
alignItems: "center",
}}
>
<CustomCheckbox
sx={{
mr: isMobile ? "0px" : "9px",
height: isMobile ? "42px" : "26px",
alignItems: "start",
}}
label={"Внутреннее название вопроса"}
checked={question.content.innerNameCheck}
handleChange={({ target }) => {
updateQuestion(question.id, question => {
question.content.innerNameCheck = target.checked;
question.content.innerName = target.checked ? question.content.innerName : "";
});
}}
/>
{isMobile && (
<Tooltip
title="Будет отображаться как заголовок вопроса в приходящих заявках."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
{question.content.innerNameCheck && (
<CustomTextField
sx={{ mr: isMobile ? "0px" : "16px" }}
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => updateQuestionInnerName(target.value)}
/>
)}
</Box>
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
);
{question.content.innerNameCheck && (
<CustomTextField
sx={{ mr: isMobile ? "0px" : "16px" }}
placeholder={"Развёрнутое описание вопроса"}
text={question.content.innerName}
onChange={({ target }) => updateQuestionInnerName(target.value || " ")}
/>
)}
</Box>
</Box>
);
}

@ -62,7 +62,7 @@ export default function HelpQuestions({ question }: HelpQuestionsProps) {
<CustomTextField
placeholder={"Текст консультанта"}
text={question.content.hint.text}
onChange={({ target }) => updateQuestionHint(target.value)}
onChange={({ target }) => updateQuestionHint(target.value || " ")}
/>
</>
) : (

@ -9,7 +9,7 @@ export default function ImageAndVideoButtons() {
return (
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
<AddImage onClick={undefined/* TODO () => openCropModal("", "") */} />
<AddImage onClick={undefined} />
<Typography
sx={{
fontWeight: 400,

@ -7,6 +7,7 @@ import {
Typography,
} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
@ -39,62 +40,34 @@ const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
whiteSpace: "nowrap",
},
},
{
title: "ƒ",
type: "ƒ",
sx: {
width: "38px",
height: "48px",
border: "1px solid #9A9AAF",
},
},
{
title: "Скидка",
type: "discount",
sx: {
width: "93px",
height: "48px",
border: "1px solid #9A9AAF",
},
},
];
type Props = {
ButtonsActive: (index: number, type: string) => void;
priceButtonsActive: number | undefined;
resultData: QuizQuestionResult
};
export default function PriceButtons({
ButtonsActive,
priceButtonsActive,
resultData
}: Props) {
return (
<Box>
<Box sx={{ display: "flex", alignItems: "center", mb: "14xp" }}>
<Typography component={"h6"} sx={{ weight: "500", fontSize: "18px" }}>
Стоимость
Заголовок
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
</Box>
<Box
component="div"
sx={{ display: "flex", flexWrap: "wrap", gap: "8px", mb: "20px" }}
>
{priceButtonsArray.map(({ title, type, sx }, index) => (
<Button
onClick={() => ButtonsActive(index, type)}
key={title}
sx={{
bgcolor: priceButtonsActive === index ? "#7E2AEA" : "#F2F3F7",
color: priceButtonsActive === index ? "#FFFF" : "#9A9AAF",
...sx,
}}
>
{title}
</Button>
))}
<CustomTextField
placeholder={"Вы прошли опрос"}
sx={{
borderRadius: "8px",
height: "48px",
width: "100%",
}}
/>
</Box>
</Box>
);

@ -1,76 +1,111 @@
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
type Props = {
text: string;
text2: string;
image: string;
};
import { ResultSettings } from "./ResultSettings"
import { createFrontResult } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { Box, Typography, useTheme, useMediaQuery, Button } from "@mui/material";
import image from "../../assets/Rectangle 110.png";
import { enqueueSnackbar } from "notistack";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
export default function CreationFullCard({ text, text2, image }: Props) {
export const FirstEntry = () => {
const theme = useTheme();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1500));
const quiz = useCurrentQuiz();
const { questions } = useQuestionsStore();
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1250));
const create = () => {
if (quiz?.config.haveRoot) {
questions
.filter((question:AnyTypedQuizQuestion) => {
return question.type !== null && question.content.rule.parentId.length !== 0 && question.content.rule.children.length === 0
})
.forEach(question => {
createFrontResult(quiz.id, question.content.id)
})
} else {
createFrontResult(quiz.id, "line")
}
}
return (
<Box
sx={{
flexGrow: 1,
backgroundColor: "white",
p: "20px",
marginTop: "50px",
borderRadius: "12px",
display: isSmallMonitor ? "block" : "flex",
flexDirection: isSmallMonitor ? "column" : "row",
gap: "20px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
<>
<Box
sx={{
flexGrow: 1,
backgroundColor: "white",
p: "20px",
marginTop: "50px",
borderRadius: "12px",
display: isSmallMonitor ? "block" : "flex",
flexDirection: isSmallMonitor ? "column" : "row",
gap: "20px",
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)`,
}}
>
<Box
sx={{
mr: !isSmallMonitor ? "104px" : 0,
marginBottom: "20px",
position: "relative",
}}
>
<Typography variant="h5" sx={{ marginBottom: "20px" }}>
Результаты квиза в зависимости от ответов
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
height: "100%",
maxHeight: isSmallMonitor ? "none" : "220px",
gap: "25px",
mr: !isSmallMonitor ? "104px" : 0,
marginBottom: isSmallMonitor ? "20px" : 0,
position: "relative",
height: "100%"
}}
>
<Typography sx={{ color: "#4D4D4D", width: "95%" }}>
{text}
<Typography variant="h5" sx={{ marginBottom: "20px" }}>
Результаты квиза в зависимости от ответов
</Typography>
<Typography
<Box
sx={{
color: "#9A9AAF",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
height: "100%",
gap: "25px",
}}
>
{text2}
</Typography>
<Typography sx={{ color: "#4D4D4D", width: "95%" }}>
Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке.
</Typography>
<Typography
sx={{
color: "#9A9AAF",
width: "100%",
}}
>
Этот шаг - необязательный, квиз будет работать и без автоматических результатов.
</Typography>
</Box>
</Box>
<img
src={image}
alt="quiz creation"
style={{
display: "block",
width: isSmallMonitor ? "100%" : "auto",
maxHeight: isSmallMonitor ? "none" : "270px",
}}
/>
</Box>
<img
src={image}
alt="quiz creation"
style={{
display: "block",
width: isSmallMonitor ? "100%" : "auto",
maxHeight: isSmallMonitor ? "none" : "270px",
<Button
onClick={create}
variant="contained"
sx={{
backgroundColor: "#7E2AEA",
fontSize: "18px",
lineHeight: "18px",
width: "216px",
height: "44px",
mt: "30px",
p: "10px 20px"
}}
/>
</Box>
>
Создать результаты
</Button>
</>
);
}
}

@ -3,7 +3,7 @@ import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import image from "../../assets/Rectangle 110.png";
import Info from "../../assets/icons/Info";
import CreationFullCard from "./FirstEntry";
// import CreationFullCard from "./FirstEntry";
export const Result = () => {
@ -13,11 +13,11 @@ export const Result = () => {
return (
<Box component="section">
<CreationFullCard
{/* <CreationFullCard
text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке."
text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов."
image={image}
/>
/> */}
<Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}>
<Button
variant="contained"

@ -1,19 +1,15 @@
import { useQuestionsStore } from "@root/questions/store";
import FirstEntry from "./FirstEntry"
import { FirstEntry } from "./FirstEntry"
import { ResultSettings } from "./ResultSettings"
export default function ResultPage() {
export const ResultPage = () => {
const { questions } = useQuestionsStore();
//ищём хотя бы один result
const haveResult = questions.some((question) => {
question.type === "result"
})
const haveResult = questions.some((question) => question.type === "result")
return (
<>
</>
haveResult ?
<ResultSettings />
:
<FirstEntry />
);
}

@ -0,0 +1,121 @@
import IconPlus from "@icons/IconPlus";
import Info from "@icons/Info";
import Plus from "@icons/Plus";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { Box, Button, Typography, Paper, Modal, TextField } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions";
import CustomWrapper from "@ui_kit/CustomWrapper";
import { DescriptionForm } from "./DescriptionForm/DescriptionForm";
import { ResultListForm } from "./ResultListForm";
import { SettingForm } from "./SettingForm";
import { useEffect, useRef, useState } from "react";
import { WhenCard } from "./cards/WhenCard";
import { ResultCard, checkEmptyData } from "./cards/ResultCard";
import { EmailSettingsCard } from "./cards/EmailSettingsCard";
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useQuestionsStore } from "@root/questions/store";
import { createFrontResult, deleteQuestion } from "@root/questions/actions";
import { QuizQuestionResult } from "@model/questionTypes/result";
export const ResultSettings = () => {
const { questions } = useQuestionsStore()
const quiz = useCurrentQuiz()
const results = useQuestionsStore().questions.filter((q): q is QuizQuestionResult => q.type === "result")
const [quizExpand, setQuizExpand] = useState(true)
const [resultContract, setResultContract] = useState(true)
const isReadyToLeaveRef = useRef(true);
useEffect(function calcIsReadyToLeave(){
let isReadyToLeave = true;
results.forEach((result) => {
if (checkEmptyData({ resultData: result })) {
isReadyToLeave = false;
}
});
isReadyToLeaveRef.current = isReadyToLeave;
}, [results])
useEffect(() => {
return () => {
if (isReadyToLeaveRef.current === false) alert("Пожалуйста, проверьте, что вы заполнили все результаты");
};
}, []);
return (
<Box sx={{ maxWidth: "796px" }}>
<Box sx={{
display: "flex",
alignItems: "center",
margin: "60px 0 40px 0",
}}>
<Typography variant="h5">
Настройки результатов
</Typography>
<Info />
<Button
disableRipple
sx={{
ml: "auto",
width: "73px",
height: "19px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
"&:hover": {
background: "none",
textDecoration: "underline",
},
}}
variant="text"
onClick={() => setQuizExpand(!quizExpand)}
>
Свернуть
</Button>
</Box>
<WhenCard quizExpand={quizExpand} />
{quiz.config.resultInfo.when === "email" && <EmailSettingsCard quizExpand={quizExpand} />}
<Box
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
>
<Typography variant="p1" sx={{ color: "#4D4D4D", fontSize: "14px" }}>
Создайте результат
</Typography>
<Button
disableRipple
sx={{
ml: "auto",
height: "19px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
"&:hover": {
background: "none",
textDecoration: "underline",
},
}}
variant="text"
onClick={() => setResultContract(!resultContract)}
>
Развернуть все
</Button>
</Box>
{
results.map((resultQuestion) => <ResultCard resultContract={resultContract} resultData={resultQuestion} key={resultQuestion.id} />)
}
<Modal
open={false}
// onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<></>
</Modal>
</Box>
);
};

@ -1,111 +0,0 @@
import IconPlus from "@icons/IconPlus";
import Info from "@icons/Info";
import Plus from "@icons/Plus";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { Box, Button, Typography } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions";
import CustomWrapper from "@ui_kit/CustomWrapper";
import { DescriptionForm } from "./DescriptionForm/DescriptionForm";
import { ResultListForm } from "./ResultListForm";
import { SettingForm } from "./SettingForm";
export const Setting = () => {
return (
<Box sx={{ maxWidth: "796px" }}>
<Box sx={{ display: "flex", alignItems: "center", mb: "40px" }}>
<Typography sx={{ pr: "10px" }} variant="h5">
Настройки результатов
</Typography>
<Info />
<Button
disableRipple
sx={{
ml: "auto",
width: "73px",
height: "19px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
"&:hover": {
background: "none",
textDecoration: "underline",
},
}}
variant="text"
>
Свернуть
</Button>
</Box>
<CustomWrapper sx={{ mt: "30px" }} text="Показывать результат" />
<CustomWrapper sx={{ mt: "30px" }} text="Настройки почты" />
<Box
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
>
<Typography variant="p1" sx={{ color: "#4D4D4D", fontSize: "14px" }}>
Создайте результат
</Typography>
<Button
disableRipple
sx={{
ml: "auto",
height: "19px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
"&:hover": {
background: "none",
textDecoration: "underline",
},
}}
variant="text"
>
Развернуть все
</Button>
</Box>
<CustomWrapper result={true} text="Показывать результат" />
<Box
sx={{
display: "flex",
width: "100%",
alignItems: "center",
columnGap: "10px",
}}
>
<Box
sx={{
boxSizing: "border-box",
width: "100%",
height: "1px",
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage:
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/>
<IconPlus />
</Box>
<CustomWrapper result={true} text="Настройки почты" />
<Box sx={{ pt: "30px", display: "flex", alignItems: "center" }}>
<Plus />
<Typography component="div" sx={{ ml: "auto" }}>
<Button
variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px" }}
>
<ArrowLeft />
</Button>
<Button variant="contained" sx={{ ml: "10px" }} onClick={incrementCurrentStep}>
Настроить форму
</Button>
</Typography>
</Box>
<SettingForm />
<ResultListForm />
<DescriptionForm />
</Box>
);
};

@ -5,10 +5,11 @@ import * as React from "react";
interface Props {
text: string;
icon: string;
onClick?: () => void;
onClick?: (a:any) => void;
value: boolean
}
export const SwitchSetting = ({ text, icon, onClick }: Props) => {
export const SwitchSetting = ({ text, icon, onClick, value }: Props) => {
return (
<Box
sx={{
@ -24,7 +25,7 @@ export const SwitchSetting = ({ text, icon, onClick }: Props) => {
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "left", maxWidth: "756px", width: "100%" }}>
<img src={icon} alt="icon" />
<FormControlLabel
value="start"
checked={value}
control={<CustomizedSwitch />}
label={text}
labelPlacement="start"

@ -0,0 +1,247 @@
import { useEffect, useState } from "react";
import { useCurrentQuiz } from "@root/quizes/hooks"
import {
Box,
TextField,
IconButton,
Paper,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import { updateQuiz } from "@root/quizes/actions";
interface Props {
quizExpand: boolean
}
export const EmailSettingsCard = ({ quizExpand }: Props) => {
const quiz = useCurrentQuiz()
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1100));
const [expand, setExpand] = useState(true)
useEffect(() => {
setExpand(false)
}, [quizExpand])
return (
<Paper
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
padding: expand ? isMobile ? "10px 10px 0 10px" : "20px 20px 0 20px" : isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<Typography
sx={{
margin: isMobile ? "10px 0" : 0,
color: expand ? "#9A9AAF" : "#000000",
}}
>
Настройки почты
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
</Box>
</Box>
{expand && (
<Box
sx={{
p: "0 20px 20px 20px",
}}
>
<Typography
sx={{
color: "#4D4D4D",
m: "20px 0 14px 0",
fontSize: "18px",
fontWeight: 500
}}
>
Тема письма
</Typography>
<TextField
value={quiz.config.resultInfo.theme}
placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.theme = target.value
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: expand
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: expand ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: "18px",
},
}}
/>
<Typography
sx={{
color: "#4D4D4D",
m: "20px 0 14px 0",
fontSize: "18px",
fontWeight: 500
}}
>
E-mail ответа
</Typography>
<TextField
value={quiz.config.resultInfo.reply}
placeholder={"noreplay@example.ru"}
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.reply = target.value
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: expand
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: expand ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: "18px",
},
}}
/>
<Typography
sx={{
color: "#4D4D4D",
m: "20px 0 14px 0",
fontSize: "18px",
fontWeight: 500
}}
>
Имя отправителя
</Typography>
<TextField
value={quiz.config.resultInfo.replname}
placeholder={"Название компании"}
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.replname = target.value
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: expand
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: expand ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
fontSize: "18px",
lineHeight: "21px",
py: 0,
paddingLeft: "18px",
},
}}
/>
</Box>
)}
</Paper>
)
}

@ -0,0 +1,610 @@
import * as React from "react";
import { getQuestionByContentId, updateQuestion, uploadQuestionImage } from "@root/questions/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { UploadImageModal } from "../../Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../../../utils/useDisclosure";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import {
Box,
IconButton,
Paper,
Button,
Typography,
TextField,
useMediaQuery,
useTheme,
FormControl,
Popover
} from "@mui/material";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import Trash from "@icons/trash";
import Info from "@icons/Info";
import SettingIcon from "@icons/questionsPage/settingIcon";
import { QuizQuestionResult } from "@model/questionTypes/result";
import { MutableRefObject } from "react";
interface Props {
resultContract: boolean;
resultData: QuizQuestionResult;
}
export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => {
let check = true
if (
resultData.title.length > 0 ||
resultData.description.length > 0 ||
resultData.content.back.length > 0 ||
resultData.content.originalBack.length > 0 ||
resultData.content.innerName.length > 0 ||
resultData.content.text.length > 0 ||
resultData.content.video.length > 0 ||
resultData.content.hint.text.length > 0
) check = false
return check
}
const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => {
const checkEmpty = checkEmptyData({ resultData })
const question = getQuestionByContentId(resultData.content.rule.parentId)
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
return (
<>
<Info
sx={{
"MuiIconButton-root": {
boxShadow: "0 0 10px 10px red"
}
}}
className={checkEmpty ? "blink" : ""}
onClick={handleClick}
/>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Paper
sx={{
p: '20px',
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column"
}}
>
<Typography>
{resultData?.content.rule.parentId === "line" ? "Единый результат в конце прохождения опросника без ветвления"
:
`Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"`
}
</Typography>
{checkEmpty &&
<Typography color="red">
Вы не заполнили этот результат никакими данными
</Typography>
}
</Paper>
</Popover>
</>
)
}
export const ResultCard = ({ resultContract, resultData }: Props) => {
console.log("resultData", resultData)
const quizQid = useCurrentQuiz()?.qid;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(800));
const [expand, setExpand] = React.useState(true)
const [resultCardSettings, setResultCardSettings] = React.useState(false)
const [buttonPlus, setButtonPlus] = React.useState(true)
React.useEffect(() => {
setExpand(true)
}, [resultContract])
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
async function handleImageUpload(file: File) {
const url = await uploadQuestionImage(resultData.id, quizQid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
closeImageUploadModal();
openCropModal(file, url);
}
function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(resultData.id, quizQid, imageBlob, (question, url) => {
question.content.back = url;
});
}
return (
<Paper
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: expand ? "none" : "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<FormControl
variant="standard"
sx={{
p: 0,
maxWidth: isTablet ? "549px" : "640px",
width: "100%",
marginRight: isMobile ? "0px" : "16.1px",
}}
>
<TextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)}
sx={{
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: expand
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: !expand ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
p: 0,
fontSize: "18px",
lineHeight: "21px",
},
}}
/>
</FormControl>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
<InfoView resultData={resultData} />
</Box>
</Box>
{expand && (
<>
<Box
sx={{
overflow: "hidden",
maxWidth: "796px",
height: "100%",
bgcolor: "#FFFFFF",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Box sx={{ p: "0 20px", pt: "30px" }}>
<Box
sx={{
width: "100%",
maxWidth: "760px",
display: "flex",
alignItems: "center",
gap: "10px",
mb: "19px",
}}
>
<CustomTextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)} />
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
<ExpandLessIconBG />
</IconButton>
<InfoView resultData={resultData} />
</Box>
<Box
sx={{
margin: "20px 0"
}}
>
<CustomTextField
value={resultData.description}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.description = target.value)}
placeholder={"Заголовок пожирнее"}
sx={{
borderRadius: "8px",
height: "48px",
width: "100%",
}}
/>
</Box>
<TextField
value={resultData.content.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.text = target.value)}
fullWidth
placeholder="Описание"
multiline
rows={4}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "100%",
height: "110px",
borderRadius: "10px",
},
}}
inputProps={{
sx: {
height: "85px",
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
<Box
sx={{
mt: "20px",
display: "flex",
gap: "10px",
flexDirection: "column"
}}
>
<Box
sx={{
display: "flex",
}}
>
<Button
sx={{
color: resultData.content.useImage ? "#7E2AEA" : "#9A9AAF",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => question.content.useImage = true)}
>
Изображение
</Button>
<Button
sx={{
color: resultData.content.useImage ? "#9A9AAF" : "#7E2AEA",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => question.content.useImage = false)}
>
Видео
</Button>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
</Box>
{
resultData.content.useImage &&
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px"
}}
>
<AddOrEditImageButton
imageSrc={resultData.content.back}
onImageClick={() => {
if (resultData.content.back) {
return openCropModal(
resultData.content.back,
resultData.content.originalBack
);
}
openImageUploadModal();
}}
onPlusClick={() => {
openImageUploadModal();
}}
/>
</Box>
}
{
!resultData.content.useImage &&
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px"
}}
>
<CustomTextField
placeholder="URL видео"
text={resultData.content.video ?? ""}
onChange={e => updateQuestion(resultData.id, q => {
resultData.content.video = e.target.value;
})}
/>
</Box>
}
</Box>
{
buttonPlus ?
<Button
onClick={() => {
setButtonPlus(false)
}}
sx={{
display: "inline flex",
height: "48px",
padding: "10px 20px",
justifyContent: "center",
alignItems: "center",
gap: "8px",
flexShrink: 0,
borderRadius: "8px",
border: "1px solid #9A9AAF",
background: " #F2F3F7",
color: "#9A9AAF",
mb: "30px"
}}
>
Кнопка +
</Button>
:
<Box
sx={{
mb: "30px"
}}
>
<Box>
<Typography component={"span"} sx={{ weight: "500", fontSize: "18px", mb: "10px" }}>
Призыв к действию
</Typography>
<IconButton
onClick={() => {
setButtonPlus(true)
updateQuestion(resultData.id, (q) => q.content.hint.text = "")
}}
>
<Trash />
</IconButton>
</Box>
<TextField
value={resultData.content.hint.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.hint.text = target.value)}
fullWidth
placeholder="Например: узнать подробнее"
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "409px",
height: "48px",
borderRadius: "8px",
},
}}
inputProps={{
sx: {
height: "85px",
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
</Box>
}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
background: "#F2F3F7",
}}
>
<Box
sx={{
padding: "20px",
display: "flex",
flexWrap: "wrap",
gap: "10px",
}}
>
<MiniButtonSetting
onClick={() => {
setResultCardSettings(!resultCardSettings)
}}
sx={{
backgroundColor:
resultCardSettings
? theme.palette.brightPurple.main
: "transparent",
color:
resultCardSettings ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
backgroundColor: resultCardSettings ? "#581CA7" : "#7E2AEA",
color: "white"
}
}}
>
<SettingIcon
color={
resultCardSettings ? "#ffffff" : theme.palette.grey3.main
}
/>
{!isTablet && "Настройки"}
</MiniButtonSetting>
</Box>
</Box>
</Box>
{
resultCardSettings &&
<Box
sx={{
backgroundColor: "white",
p: "20px",
borderRadius: "0 0 12px 12px"
}}
>
<CustomTextField
placeholder={"Внутреннее описание вопроса"}
value={resultData.innerName}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.innerName = target.value)}
/>
</Box>
}
</>
)
}
</Paper >
)
}

@ -0,0 +1,187 @@
import { useEffect, useState } from "react";
import { updateQuiz } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { SwitchSetting } from "../SwichResult";
import {
Box,
IconButton,
Paper,
Button,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import ShareNetwork from "@icons/ShareNetwork.svg";
import ArrowCounterClockWise from "@icons/ArrowCounterClockWise.svg";
const whenValues = [
{
title: "До формы контактов",
value: "before",
},
{
title: "После формы контактов",
value: "after",
},
{
title: "Отправить на E-mail",
value: "email",
},
];
interface Props {
quizExpand: boolean
}
export const WhenCard = ({ quizExpand }: Props) => {
const quiz = useCurrentQuiz()
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1100));
const [expand, setExpand] = useState(true)
useEffect(() => {
setExpand(false)
}, [quizExpand])
return (
<Paper
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<Typography
sx={{
margin: isMobile ? "10px 0" : 0,
color: expand ? "#9A9AAF" : "#000000",
}}
>
Показывать результат
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
</Box>
</Box>
{expand && (
<>
<Box
sx={{
p: "33px 20px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: isSmallMonitor ? "column" : "row",
justifyContent: "space-between",
gap: "20px",
mb: "20px",
}}
>
{whenValues.map(({ title, value }, index) => (
<Button
onClick={() => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.when = value)}
key={title}
sx={{
bgcolor: quiz?.config.resultInfo.when === value ? " #7E2AEA" : "#F2F3F7",
color: quiz?.config.resultInfo.when === value ? " white" : "#9A9AAF",
minWidth: isSmallMonitor ? "300px" : "auto",
borderRadius: "8px",
width: "237px",
height: "44px",
border: quiz?.config.resultInfo.when === value ? "none" : "1px solid #9A9AAF",
"&:hover": {
backgroundColor: quiz?.config.resultInfo.when === value ? "#581CA7" : "#7E2AEA",
color: "white"
}
}}
>
{title}
</Button>
))}
</Box>
{
(quiz?.config.resultInfo.when !== "email") && <SwitchSetting
icon={ShareNetwork}
text="Поделиться результатами"
onClick={(event) => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.share = event.target.checked)}
value={quiz?.config.resultInfo.share}
/>
}
{
quiz?.config.resultInfo.when === "before" && <SwitchSetting
icon={ArrowCounterClockWise}
text="Кнопка `Пройти тест заново`"
onClick={(event) => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.replay = event.target.checked)}
value={quiz?.config.resultInfo.replay}
/>
}
</Box>
</>
)}
</Paper>
)
}

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Box, Typography, Button, useTheme } from "@mui/material";
import { Box, Button, useTheme } from "@mui/material";
import { useQuizViewStore } from "@root/quizView";
@ -11,43 +11,92 @@ import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
type FooterProps = {
questions: AnyTypedQuizQuestion[];
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
question: QuizQuestionBase;
question: AnyTypedQuizQuestion;
};
export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
const [disabledQuestionsId, setDisabledQuestionsId] = useState<Set<string>>(
new Set()
);
export const Footer = ({
setCurrentQuestion,
questions,
question,
}: FooterProps) => {
const [disablePreviousButton, setDisablePreviousButton] =
useState<boolean>(false);
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const { answers } = useQuizViewStore();
const theme = useTheme();
const linear = !questions.find(
({ content }) => content.rule.parentId === "root"
);
useEffect(() => {
// Логика для аргумента disabled у кнопки "Назад"
if (question?.content.rule.parentId === "root") {
setDisablePreviousButton(true);
if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = questions[questionIndex - 1];
if (previousQuestion) {
setDisablePreviousButton(false);
} else {
setDisablePreviousButton(true);
}
} else {
setDisablePreviousButton(false);
if (question?.content.rule.parentId === "root") {
setDisablePreviousButton(true);
} else {
setDisablePreviousButton(false);
}
}
// Логика для аргумента disabled у кнопки "Далее"
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
const answer = answers.find(
({ questionId }) => questionId === question.content.id
);
if ("required" in question.content && question.content.required && answer) {
setDisableNextButton(false);
} else {
const nextQuestion = getQuestionByContentId(
question.content.rule.default
);
if (nextQuestion?.type) {
return;
}
if (
"required" in question.content &&
question.content.required &&
!answer
) {
setDisableNextButton(true);
return;
}
if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const nextQuestion = questions[questionIndex + 1];
if (nextQuestion) {
setDisableNextButton(false);
} else {
setDisableNextButton(true);
}
} else {
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
setDisableNextButton(false);
} else {
const nextQuestion = getQuestionByContentId(
question.content.rule.default
);
if (nextQuestion?.type) {
setDisableNextButton(false);
} else {
setDisableNextButton(true);
}
}
}
}, [question]);
}, [question, answers]);
const getNextQuestionId = () => {
if (answers.length) {
@ -57,28 +106,56 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
let readyBeNextQuestion = "";
question.content.rule.main.forEach(({ next, rules }) => {
let longerArray = Math.max(
rules[0].answers.length,
[answer?.answer].length
);
(question as QuizQuestionBase).content.rule.main.forEach(
({ next, rules }) => {
let longerArray = Math.max(
rules[0].answers.length,
answer?.answer && Array.isArray(answer?.answer)
? answer?.answer.length
: [answer?.answer].length
);
for (
var i = 0;
i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива
) {
if (rules[0].answers[i] === answer?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
for (
var i = 0;
i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива
) {
if (Array.isArray(answer?.answer)) {
if (
answer?.answer.find((item) =>
String(item === rules[0].answers[i])
)
) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
return;
}
if (String(rules[0].answers[i]) === answer?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
}
});
);
return readyBeNextQuestion;
}
};
const followPreviousStep = () => {
if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = questions[questionIndex - 1];
if (previousQuestion) {
setCurrentQuestion(previousQuestion);
}
return;
}
if (question?.content.rule.parentId !== "root") {
const parent = getQuestionByContentId(question?.content.rule.parentId);
if (parent?.type) {
@ -92,6 +169,18 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
};
const followNextStep = () => {
if (linear) {
const questionIndex = questions.findIndex(({ id }) => id === question.id);
const nextQuestion = questions[questionIndex + 1];
if (nextQuestion) {
setCurrentQuestion(nextQuestion);
}
return;
}
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {

@ -13,15 +13,13 @@ import { Page } from "./questions/Page";
import { Rating } from "./questions/Rating";
import { Footer } from "./Footer";
import { useState, type FC } from "react";
import { useState, type FC, useEffect } from "react";
import type { QuestionType } from "../../model/question/question";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { getQuestionByContentId } from "@root/questions/actions";
import { getQuestionByContentId } from "@root/questions/actions";
type QuestionProps = {
stepNumber: number;
setStepNumber: (step: number) => void;
questions: AnyTypedQuizQuestion[];
};
@ -39,12 +37,24 @@ const QUESTIONS_MAP: any = {
rating: Rating,
};
export const Question = ({
questions,
}: QuestionProps) => {
export const Question = ({ questions }: QuestionProps) => {
const quiz = useCurrentQuiz();
const [currentQuestion, setCurrentQuestion] = useState(getQuestionByContentId(quiz?.config.haveRoot || ""))
if (!currentQuestion) return <>не смог отобразить вопрос</>
const [currentQuestion, setCurrentQuestion] =
useState<AnyTypedQuizQuestion>();
useEffect(() => {
const nextQuestion = getQuestionByContentId(quiz?.config.haveRoot || "");
if (nextQuestion?.type) {
setCurrentQuestion(nextQuestion);
return;
}
setCurrentQuestion(questions[0]);
}, []);
if (!currentQuestion) return <>не смог отобразить вопрос</>;
const QuestionComponent =
QUESTIONS_MAP[currentQuestion.type as Exclude<QuestionType, "nonselected">];
@ -60,9 +70,10 @@ export const Question = ({
margin: "0 auto",
}}
>
<QuestionComponent currentQuestion={currentQuestion}/>
<QuestionComponent currentQuestion={currentQuestion} />
</Box>
<Footer
questions={questions}
question={currentQuestion}
setCurrentQuestion={setCurrentQuestion}
/>

@ -1,162 +1,298 @@
import { useParams } from "react-router-dom";
import {
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useTheme,
useMediaQuery,
useTheme,
} from "@mui/material";
import useSWR from "swr";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import { devlog } from "@frontend/kitui";
import { useCurrentQuiz } from "@root/quizes/hooks";
import YoutubeEmbedIframe from "../../ui_kit/StartPagePreview/YoutubeEmbedIframe";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice";
import { quizApi } from "@api/quiz";
interface Props {
setVisualStartPage: (a:boolean) => void
}
import { useQuizStore } from "@root/quizes/store";
import { useQuestions } from "@root/questions/hooks";
import { setQuizes } from "@root/quizes/actions";
type StartPageViewPublicationProps = {
setVisualStartPage: (bool: boolean) => void;
showNextButton:boolean
};
export const StartPageViewPublication = ({
setVisualStartPage,
showNextButton
}: StartPageViewPublicationProps) => {
const quizId = Number(useParams().quizId);
const { quizes } = useQuizStore();
const { questions } = useQuestions();
export const StartPageViewPublication = ({setVisualStartPage}:Props) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630));
const quiz = quizes.find(({ backendId }) => quizId === backendId);
const isMediaFileExist =
quiz?.config.startpage.background.desktop ||
quiz?.config.startpage.background.video;
const quiz = useCurrentQuiz();
const { isMobileDevice } = useUADevice();
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: (error: unknown) => {
const message = isAxiosError<string>(error)
? error.response?.data ?? ""
: "";
if (!quiz) return null;
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const handleCopyNumber = () => {
navigator.clipboard.writeText(quiz.config.info.phonenumber);
};
const background = quiz.config.startpage.background.type === "image"
? quiz.config.startpage.background.desktop
? (
<img
src={quiz.config.startpage.background.desktop}
alt=""
style={{
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden"
}}
/>
)
: null
: quiz.config.startpage.background.type === "video"
? quiz.config.startpage.background.video
? (
<YoutubeEmbedIframe videoUrl={quiz.config.startpage.background.video}
containerSX={{
width: quiz.config.startpageType === "centered" ? "550px" : quiz.config.startpageType === "expanded" ? "100vw" : "100%",
height: quiz.config.startpageType === "centered" ? "275px" : quiz.config.startpageType === "expanded" ? "100vh" : "100%",
borderRadius: quiz.config.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform: quiz.config.startpageType === "centered" ? "" : quiz.config.startpageType === "expanded" ? "scale(1.5)" : "scale(2.4)",
}
}}
/>
)
: null
: null;
return (
<Box
<Paper className="quiz-preview-draghandle"
sx={{
height: "100vh",
display: "flex",
flexDirection:
quiz?.config.startpage.position === "left" ? "row" : "row-reverse",
flexGrow: 1,
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: isMediaFileExist && !isTablet ? "40%" : "100%",
padding: "16px",
display: "flex",
flexDirection: "column",
alignItems: isMediaFileExist && !isTablet ? "flex-start" : "center",
}}
>
<Box
sx={{
background: quiz.config.startpageType === "expanded" ?
quiz.config.startpage.position === "left" ? "linear-gradient(90deg,#272626,transparent)" :
quiz.config.startpage.position === "center" ? "linear-gradient(180deg,transparent,#272626)" :
"linear-gradient(270deg,#272626,transparent)"
: "",
color: quiz.config.startpageType === "expanded" ? "white" : "black"
}}>
<QuizPreviewLayoutByType
quizHeaderBlock={<Box
p={quiz.config.startpageType === "standard" ? "" : "16px"}
>
<Box sx={{
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
{quiz?.config.startpage.background.mobile && (
<img
src={quiz.config.startpage.background.mobile}
style={{
height: "50px",
maxWidth: "100px",
objectFit: "cover",
}}
alt=""
/>
)}
<Typography sx={{ fontSize: "18px" }}>
{quiz?.config.info.orgname}
</Typography>
</Box>
<Box
sx={{
flexGrow: 1,
display: "flex",
gap: "10px",
flexDirection: "column",
justifyContent: "center",
}}
>
<Typography sx={{ fontWeight: "bold", fontSize: "20px" }}>
{quiz?.name}
</Typography>
<Typography sx={{ fontSize: "16px" }}>
{quiz?.config.startpage.description}
</Typography>
<Box>
<Button
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
// disabled={!questions.length}
onClick={() => setVisualStartPage(false)}
>
{quiz?.config.startpage.button
? quiz?.config.startpage.button
: "Пройти тест"}
</Button>
</Box>
</Box>
<Box>
<Typography
sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}
>
{quiz?.config.info.phonenumber}
</Typography>
<Typography sx={{ fontSize: "12px" }}>
{quiz?.config.info.law}
</Typography>
</Box>
</Box>
{!isTablet && isMediaFileExist && (
<Box sx={{ width: "60%" }}>
{quiz?.config.startpage.background.mobile && (
<img
src={quiz.config.startpage.background.mobile}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
{quiz.config.startpage.background.type === "video" &&
quiz.config.startpage.background.video && (
<video
src={quiz.config.startpage.background.video}
controls
mb: "7px"
}}>
{quiz.config.startpage.logo && (
<img
src={quiz.config.startpage.logo}
style={{
width: "100%",
height: "100%",
height: "37px",
maxWidth: "43px",
objectFit: "cover",
}}
alt=""
/>
)}
</Box>
)}
</Box>
<Typography sx={{ fontSize: "14px" }}>
{quiz.config.info.orgname}
</Typography>
</Box>
<Link mb="16px" href={quiz.config.info.site}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.site}
</Typography>
</Link>
</Box>}
quizMainBlock={<>
<Box sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: quiz.config.startpageType === "centered" ? "center" :
quiz.config.startpageType === "expanded"
? quiz.config.startpage.position === "center" ?
"center"
: "start": "start",
mt: "28px",
width: "100%"
}}>
<Typography sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
}}>{quiz.name}</Typography>
<Typography sx={{
fontSize: "16px",
m: "16px 0"
}}>
{quiz.config.startpage.description}
</Typography>
<Box width={ quiz.config.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
width: quiz.config.startpageType === "standard" ? "100%" : "auto"
}}
onClick={() => setVisualStartPage(true)}
>
{quiz.config.startpage.button.trim() ? quiz.config.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
mt: "46px"
}}
>
{quiz.config.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${quiz.config.info.phonenumber}`}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
{quiz.config.info.phonenumber}
</Typography>
)}
<Typography sx={{ fontSize: "12px", textAlign: "end" }}>
{quiz.config.info.law}
</Typography>
</Box>
</>}
backgroundBlock={background}
startpageType={quiz.config.startpageType}
alignType={quiz.config.startpage.position}
/>
</Paper>
);
}
function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlock, startpageType, alignType }: {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
}) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630));
switch (startpageType) {
case null:
case "standard": {
return (
<Box sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
flexGrow: 1,
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
width: !isTablet ? "40%" : "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: !isTablet ? "flex-start" : "center",
p: "25px"
}}>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
width: "60%",
overflow: "hidden"
}}>
{backgroundBlock}
</Box>
</Box>
);
}
case "expanded": {
return (
<Box sx={{
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
width: "40%",
position: "relative",
padding: "16px",
zIndex: 2,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
position: "absolute",
left: 0,
top: 0,
height: "100%",
width: "100%",
zIndex: 1,
overflow: "hidden"
}}>
{backgroundBlock}
</Box>
</Box>
);
}
case "centered": {
return (
<Box sx={{
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
overflow: "hidden"
}}>
{quizHeaderBlock}
{backgroundBlock &&
<Box>
{backgroundBlock}
</Box>
}
{quizMainBlock}
</Box>
);
}
default: notReachable(startpageType);
}
}
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
left: "start",
center: "center",
right: "end",
};

@ -1,34 +1,65 @@
import { useLayoutEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Box } from "@mui/material";
import { StartPageViewPublication } from "./StartPageViewPublication";
import { Question } from "./Question";
import { useQuestions } from "@root/questions/hooks";
import { useCurrentQuiz } from "@root/quizes/hooks";
import useSWR from "swr";
import { quizApi } from "@api/quiz";
import { setQuizes } from "@root/quizes/actions";
import { isAxiosError } from "axios";
import { devlog } from "@frontend/kitui";
import { useQuizStore } from "@root/quizes/store";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@root/questions/store";
import { setQuestions } from "@root/questions/actions";
import { questionApi } from "@api/question";
export const ViewPage = () => {
const quiz = useCurrentQuiz();
const { questions } = useQuestions();
const [visualStartPage, setVisualStartPage] = useState<boolean>(!quiz?.config.noStartPage);
const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore();
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList()
setQuizes(quizes)
const questions = await questionApi.getList({ quiz_id: editQuizId })
setQuestions(questions)
}
getData()
}, [])
useEffect(() => {
setVisualStartPage(quiz?.config.noStartPage)
}, [questions])
const filteredQuestions = questions.filter(
({ type }) => type
) as AnyTypedQuizQuestion[];
const [visualStartPage, setVisualStartPage] = useState<boolean>();
useEffect(() => {
const link = document.querySelector('link[rel="icon"]');
if (link && quiz?.config.startpage.favIcon) {
link.setAttribute("href", quiz.config.startpage.favIcon);
}
}, [quiz?.config.startpage.favIcon]);
const filteredQuestions = (
questions.filter(({ type }) => type) as AnyTypedQuizQuestion[]
).sort((previousItem, item) => previousItem.page - item.page);
console.log("visualStartPage ", visualStartPage)
if (visualStartPage === undefined) return <></>
return (
<Box>
{visualStartPage ? (
<StartPageViewPublication
setVisualStartPage={setVisualStartPage}
showNextButton={!!filteredQuestions.length}
/>
{!visualStartPage ? (
<StartPageViewPublication setVisualStartPage={setVisualStartPage} />
) : (
<Question
questions={filteredQuestions}
/>
<Question questions={filteredQuestions} />
)}
</Box>
);

@ -1,11 +1,11 @@
import DatePicker from "react-datepicker";
import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import "react-datepicker/dist/react-datepicker.css";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon";
type DateProps = {
currentQuestion: QuizQuestionDate;
@ -13,10 +13,10 @@ type DateProps = {
export const Date = ({ currentQuestion }: DateProps) => {
const { answers } = useQuizViewStore();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
)?.answer as string;
const [day, month, year] = answer?.split(".") || [];
return (
<Box>
@ -30,19 +30,54 @@ export const Date = ({ currentQuestion }: DateProps) => {
}}
>
<DatePicker
selected={answer ? new window.Date(answer) : new window.Date()}
onChange={(date) =>
slots={{
openPickerIcon: () => <CalendarIcon />,
}}
value={dayjs(
answer
? new window.Date(`${month}.${day}.${year}`)
: new window.Date()
)}
onChange={(date) => {
if (!date) {
return;
}
updateAnswer(
currentQuestion.content.id,
String(
date?.toLocaleDateString("ru-RU", {
new window.Date(date.toDate()).toLocaleDateString("ru-RU", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})
)
)
}
);
}}
slotProps={{
openPickerButton: {
sx: {
p: 0,
},
"data-cy": "open-datepicker",
},
}}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
borderRadius: "10px",
maxWidth: "250px",
pr: "22px",
"& input": {
py: "11px",
pl: "20px",
lineHeight: "19px",
},
"& fieldset": {
borderColor: "#9A9AAF",
},
},
}}
/>
</Box>
</Box>

@ -1,4 +1,3 @@
import { useEffect } from "react";
import {
Box,
Typography,
@ -6,9 +5,10 @@ import {
FormControlLabel,
Radio,
useTheme,
FormControl,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -22,20 +22,19 @@ type EmojiProps = {
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
useEffect(() => {
if (!answer) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
}
}, []);
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
onChange={({ target }) =>
updateAnswer(
currentQuestion.content.id,
@ -50,33 +49,74 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{currentQuestion.content.variants.map(
({ id, answer, extendedText }, index) => (
<FormControlLabel
key={id}
<Box sx={{ display: "flex", width: "100%", gap: "42px" }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={variant.id}
sx={{
borderRadius: "12px",
border: `1px solid ${theme.palette.grey2.main}`,
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
}}
>
<Box
sx={{
marginBottom: "15px",
borderRadius: "5px",
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize={"100px"}>
{variant.extendedText}
</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.grey2.main,
border: `1px solid ${theme.palette.grey2.main}`,
color: "#4D4D4D",
display: "flex",
gap: "10px",
}}
value={index}
onClick={(event) => {
event.preventDefault();
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[index].id
);
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.content.id);
}
}}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography>{extendedText}</Typography>
<Typography>{answer}</Typography>
<Typography>{variant.answer}</Typography>
</Box>
}
/>
)
)}
</FormControl>
))}
</Box>
</RadioGroup>
</Box>

@ -1,28 +1,56 @@
import { Box, Typography, ButtonBase } from "@mui/material";
import UploadBox from "@ui_kit/UploadBox";
import {
Box,
Typography,
ButtonBase,
useTheme,
IconButton,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import { UPLOAD_FILE_TYPES_MAP } from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File";
import UploadIcon from "@icons/UploadIcon";
import CloseBold from "@icons/CloseBold";
import type { ChangeEvent } from "react";
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import type { DragEvent } from "react";
import type { UploadFileType } from "@model/questionTypes/file";
type FileProps = {
currentQuestion: QuizQuestionFile;
};
export const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
UploadFileType,
{ title: string; description: string }
> = {
all: { title: "Добавить файл", description: "Принимает любые файлы" },
picture: {
title: "Добавить изображение",
description: "Принимает изображения",
},
video: {
title: "Добавить видео",
description: "Принимает .mp4 и .mov формат — максимум 100мб",
},
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
document: { title: "Добавить документ", description: "Принимает документы" },
} as const;
export const File = ({ currentQuestion }: FileProps) => {
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
)?.answer as string;
const theme = useTheme();
const uploadFile = ({ target }: ChangeEvent<HTMLInputElement>) => {
const file = target.files?.[0];
if (file) {
updateAnswer(currentQuestion.content.id, `${file.name}|${URL.createObjectURL(file)}`);
updateAnswer(
currentQuestion.content.id,
`${file.name}|${URL.createObjectURL(file)}`
);
}
};
@ -35,22 +63,112 @@ export const File = ({ currentQuestion }: FileProps) => {
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "550px",
}}
>
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={uploadFile}
hidden
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
multiple
type="file"
/>
<UploadBox icon={<UploadIcon />} text="5 MB максимум" />
</ButtonBase>
{answer?.split("|")[0] && (
<Typography sx={{ marginTop: "15px" }}>
{answer?.split("|")[0]}
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography>Вы загрузили:</Typography>
<Box
sx={{
padding: "5px 5px 5px 16px",
backgroundColor: theme.palette.brightPurple.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<Typography>{answer?.split("|")[0]}</Typography>
<IconButton
sx={{ p: 0 }}
onClick={() => {
updateAnswer(currentQuestion.content.id, "");
}}
>
<CloseBold />
</IconButton>
</Box>
</Box>
)}
{!answer?.split("|")[0] && (
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
<input
onChange={uploadFile}
hidden
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
multiple
type="file"
/>
<Box
onDragOver={(event: DragEvent<HTMLDivElement>) =>
event.preventDefault()
}
sx={{
width: "100%",
height: "120px",
display: "flex",
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography
sx={{
color: theme.palette.grey2.main,
fontWeight: 500,
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.title
}
</Typography>
<Typography
sx={{
color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
{
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
.description
}
</Typography>
</Box>
</Box>
</ButtonBase>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
alt=""
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
}}
/>
)}
{answer && currentQuestion.content.type === "video" && (
<video
src={answer.split("|")[1]}
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
objectFit: "cover",
}}
/>
)}
</Box>
</Box>

@ -1,15 +1,14 @@
import { useEffect } from "react";
import {
Box,
Typography,
RadioGroup,
FormControlLabel,
Radio,
useTheme,
useMediaQuery, FormControl,
Box,
Typography,
RadioGroup,
FormControlLabel,
Radio,
useTheme,
useMediaQuery,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -22,28 +21,21 @@ type ImagesProps = {
export const Images = ({ currentQuestion }: ImagesProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(500));
useEffect(() => {
if (!answer) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
}
}, []);
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
onChange={({ target }) =>
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[Number(target.value)].id
)
}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
@ -64,50 +56,59 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
width: "100%",
}}
>
{currentQuestion.content.variants.map(
({ id, answer, extendedText }, index) => (
<Box
key={index}
sx={{
borderRadius: "5px",
border: `1px solid ${theme.palette.grey2.main}`,
}}
>
<Box
sx={{ display: "flex", alignItems: "center", gap: "10px" }}
>
<Box sx={{ width: "100%", height: "300px" }}>
{extendedText && (
<img
src={extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
</Box>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid ${theme.palette.grey2.main}`,
}}
onClick={(event) => {
event.preventDefault();
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[index].id
);
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.content.id);
}
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
</Box>
<FormControl
key={id}
sx={{
display: "block",
textAlign: "center",
color: theme.palette.grey2.main,
marginTop: "10px",
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={answer}
/>
</Box>
)
)}
<FormControlLabel
key={variant.id}
sx={{
display: "block",
textAlign: "center",
color: theme.palette.grey2.main,
marginTop: "10px",
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={variant.answer}
/>
</Box>
))}
</Box>
</RadioGroup>
</Box>

@ -1,7 +1,9 @@
import { useEffect } from "react";
import { useState, useEffect } from "react";
import { Box, Typography, Slider, useTheme } from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomTextField from "@ui_kit/CustomTextField";
import { CustomSlider } from "@ui_kit/CustomSlider";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
@ -12,16 +14,47 @@ type NumberProps = {
};
export const Number = ({ currentQuestion }: NumberProps) => {
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100");
const theme = useTheme();
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const updateMinRangeDebounced = useDebouncedCallback(
(value, crowded = false) => {
if (crowded) {
setMinRange(maxRange);
}
updateAnswer(currentQuestion.content.id, value);
},
1000
);
const updateMaxRangeDebounced = useDebouncedCallback(
(value, crowded = false) => {
if (crowded) {
setMaxRange(minRange);
}
updateAnswer(currentQuestion.content.id, value);
},
1000
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
)?.answer as string;
const min = window.Number(currentQuestion.content.range.split("—")[0]);
const max = window.Number(currentQuestion.content.range.split("—")[1]);
const sliderValue = answer || currentQuestion.content.start + "—" + max;
useEffect(() => {
if (answer) {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
}
if (!answer) {
updateAnswer(currentQuestion.content.id, "1");
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
}
}, []);
@ -34,40 +67,105 @@ export const Number = ({ currentQuestion }: NumberProps) => {
flexDirection: "column",
width: "100%",
marginTop: "20px",
gap: "30px",
}}
>
<CustomTextField
placeholder="0"
value={answer || ""}
onChange={({ target }) => {
updateAnswer(
currentQuestion.content.id,
window.Number(target.value) > max
? String(max)
: window.Number(target.value) < min
? String(min)
: target.value
);
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
}}
/>
<Slider
value={window.Number(answer || 1)}
<CustomSlider
value={
currentQuestion.content.chooseRange
? sliderValue.split("—").length || 0 > 1
? sliderValue.split("—").map((item) => window.Number(item))
: [min, min + 1]
: window.Number(sliderValue.split("—")[0])
}
min={min}
max={max}
step={currentQuestion.content.step || 1}
sx={{
color: theme.palette.brightPurple.main,
padding: "0",
marginTop: "25px",
}}
onChange={(_, value) => {
updateAnswer(currentQuestion.content.id, String(value));
const range = String(value).replace(",", "—");
updateAnswer(currentQuestion.content.id, range);
}}
onChangeCommitted={(_, value) => {
if (currentQuestion.content.chooseRange) {
const range = value as number[];
setMinRange(String(range[0]));
setMaxRange(String(range[1]));
}
}}
/>
{!currentQuestion.content.chooseRange && (
<CustomTextField
placeholder="0"
value={answer}
onChange={({ target }) => {
updateAnswer(
currentQuestion.content.id,
window.Number(target.value) > max
? String(max)
: window.Number(target.value) < min
? String(min)
: target.value
);
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
}}
/>
)}
{currentQuestion.content.chooseRange && (
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
"& .MuiFormControl-root": { width: "auto" },
}}
>
<CustomTextField
placeholder="0"
value={minRange}
onChange={({ target }) => {
setMinRange(target.value);
if (window.Number(target.value) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
return;
}
updateMinRangeDebounced(`${target.value}${maxRange}`);
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
}}
/>
<Typography>до</Typography>
<CustomTextField
placeholder="0"
value={maxRange}
onChange={({ target }) => {
setMaxRange(target.value);
if (window.Number(target.value) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return;
}
updateMaxRangeDebounced(`${minRange}${target.value}`);
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
}}
/>
</Box>
)}
</Box>
</Box>
);

@ -7,6 +7,12 @@ import {
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
@ -15,53 +21,82 @@ type RatingProps = {
currentQuestion: QuizQuestionRating;
};
const buttonRatingForm = [
{
name: "star",
icon: (color: string) => <StarIconMini width={50} color={color} />,
},
{
name: "trophie",
icon: (color: string) => <TropfyIcon color={color} />,
},
{
name: "flag",
icon: (color: string) => <FlagIcon color={color} />,
},
{
name: "heart",
icon: (color: string) => <HeartIcon color={color} />,
},
{
name: "like",
icon: (color: string) => <LikeIcon color={color} />,
},
{
name: "bubble",
icon: (color: string) => <LightbulbIcon color={color} />,
},
{
name: "hashtag",
icon: (color: string) => <HashtagIcon color={color} />,
},
];
export const Rating = ({ currentQuestion }: RatingProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const form = buttonRatingForm.find(
({ name }) => name === currentQuestion.content.form
);
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
}}
>
<RatingComponent
value={Number(answer || 0)}
onChange={(_, value) => updateAnswer(currentQuestion.content.id, String(value))}
sx={{ height: "50px" }}
max={currentQuestion.content.steps}
icon={
<StarIconMini
color={theme.palette.brightPurple.main}
width={50}
sx={{ transform: "scale(0.8)" }}
/>
}
emptyIcon={
<StarIconMini
color={theme.palette.grey2.main}
width={50}
sx={{ transform: "scale(0.8)" }}
/>
}
/>
<Typography sx={{ color: theme.palette.grey2.main }}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
maxWidth: `${currentQuestion.content.steps * 50}px`,
color: theme.palette.grey2.main,
display: "inline-block",
width: "100%",
}}
>
<Typography>{currentQuestion.content.ratingNegativeDescription}</Typography>
<Typography>{currentQuestion.content.ratingPositiveDescription}</Typography>
<RatingComponent
value={Number(answer || 0)}
onChange={(_, value) =>
updateAnswer(currentQuestion.content.id, String(value))
}
sx={{ height: "50px", gap: "15px" }}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.brightPurple.main)}
emptyIcon={form?.icon(theme.palette.grey2.main)}
/>
</Box>
<Typography sx={{ color: theme.palette.grey2.main }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
);

@ -2,7 +2,7 @@ import { Box, Typography } from "@mui/material";
import { Select as SelectComponent } from "../../../pages/Questions/Select";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
@ -12,7 +12,10 @@ type SelectProps = {
export const Select = ({ currentQuestion }: SelectProps) => {
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
return (
<Box>
@ -26,9 +29,16 @@ export const Select = ({ currentQuestion }: SelectProps) => {
}}
>
<SelectComponent
activeItemIndex={Number(answer) || 0}
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
onChange={(_, value) => {
if (value < 0) {
deleteAnswer(currentQuestion.content.id);
return;
}
updateAnswer(currentQuestion.content.id, String(value));
}}
/>

@ -3,31 +3,57 @@ import {
Box,
Typography,
RadioGroup,
FormGroup,
FormControlLabel,
Radio,
Checkbox,
TextField,
useTheme,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import {
useQuizViewStore,
updateAnswer,
deleteAnswer,
updateOwnVariant,
deleteOwnVariant,
} from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { CheckboxIcon } from "@icons/Checkbox";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
type VariantProps = {
stepNumber: number;
currentQuestion: QuizQuestionVariant;
};
type VariantItemProps = {
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
};
export const Variant = ({ currentQuestion }: VariantProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const { answers, ownVariants } = useQuizViewStore();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const ownVariant = ownVariants.find(
(variant) => variant.contentId === currentQuestion.content.id
);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
useEffect(() => {
if (!answer) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
if (!ownVariant) {
updateOwnVariant(currentQuestion.content.id, "");
}
}, []);
@ -35,15 +61,11 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}>
<RadioGroup
<Group
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
onChange={({ target }) =>
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[Number(target.value)].id
)
}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
@ -53,30 +75,35 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", flexDirection: "row", flexWrap: "wrap", width: "100%", gap: "20px", }}>
{currentQuestion.content.variants.map(({ id, answer }, index) => (
<FormControlLabel
key={id}
sx={{
margin: "0",
borderRadius: "12px",
padding: "15px",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
maxWidth: "685px",
justifyContent: "space-between",
width: "100%"
}}
value={index}
labelPlacement="start"
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={answer}
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
width: "100%",
gap: "20px",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VariantItem
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
answer={answer}
index={index}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
/>
)}
</Box>
</RadioGroup>
</Group>
{currentQuestion.content.back && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img
@ -90,3 +117,70 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
</Box>
);
};
const VariantItem = ({
currentQuestion,
variant,
answer,
index,
own = false,
}: VariantItemProps) => {
const theme = useTheme();
return (
<FormControlLabel
key={variant.id}
sx={{
margin: "0",
borderRadius: "12px",
padding: "15px",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
maxWidth: "685px",
justifyContent: "space-between",
width: "100%",
"&.MuiFormControl-root": {
width: "100%",
}
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<CheckboxIcon checked />}
icon={<CheckboxIcon />}
/>
) : (
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={(event) => {
event.preventDefault();
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
updateAnswer(
currentQuestion.content.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId]
);
return;
}
updateAnswer(currentQuestion.content.id, variantId);
if (answer === variantId) {
deleteAnswer(currentQuestion.content.id);
}
}}
/>
);
};

@ -1,4 +1,3 @@
import { useEffect } from "react";
import {
Box,
Typography,
@ -8,7 +7,9 @@ import {
useTheme,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import gag from "./gag.png"
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -22,67 +23,97 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const variant = currentQuestion.content.variants.find(
({ id }) => answer === id
);
useEffect(() => {
if (!answer) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
}
}, []);
console.log(currentQuestion)
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}>
<Box sx={{ display: "flex", marginTop: "20px" }}>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
onChange={({ target }) =>
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[Number(target.value)].id
)
}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{currentQuestion.content.variants.map(({ id, answer }, index) => (
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={id}
key={variant.id}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.grey2.main,
color: "#4D4D4D",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
}}
value={index}
onClick={(event) => {
event.preventDefault();
updateAnswer(
currentQuestion.content.id,
currentQuestion.content.variants[index].id
);
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.content.id);
}
}}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={answer}
label={variant.answer}
/>
))}
</Box>
</RadioGroup>
{(variant?.extendedText || currentQuestion.content.back) && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img
src={answer ? variant?.extendedText : currentQuestion.content.back}
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
<Box
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF12",
color: "#9A9AAF"
}}
>
{
answer ?
<img
src={
variant?.extendedText || gag
}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
:
(variant?.extendedText || "Выберите вариант ответа слева")
}
</Box>
)}
{/* )} */}
</Box>
</Box>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@ -1,14 +1,14 @@
import { login } from "@api/auth";
import CloseIcon from "@mui/icons-material/Close";
import {
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
Box,
Button,
Dialog,
IconButton,
Link,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { setUserId, useUserStore } from "@root/user";
import InputTextfield from "@ui_kit/InputTextfield";
@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { Link as RouterLink, useNavigate, useLocation } from "react-router-dom";
import { object, string } from "yup";
interface Values {
@ -43,6 +43,8 @@ export default function SigninDialog() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate();
const location = useLocation();
const formik = useFormik<Values>({
initialValues,
validationSchema,
@ -110,11 +112,11 @@ export default function SigninDialog() {
borderRadius: "12px",
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
{
position: "absolute",
top: "46px",
margin: "0",
},
{
position: "absolute",
top: "46px",
margin: "0",
},
}}
>
<IconButton
@ -153,7 +155,7 @@ export default function SigninDialog() {
id="email"
label="Email"
gap={upMd ? "10px" : "10px"}
/>
/>
<PasswordInput
TextfieldProps={{
value: formik.values.password,
@ -190,16 +192,17 @@ export default function SigninDialog() {
Войти
</Button>
{/* <Link
component={RouterLink}
to="/"
href="#"
sx={{
color: "#4D4D4D",
mb: "15px",
}}
>
Забыли пароль?
</Link> */}
component={RouterLink}
to="/"
href="#"
sx={{
color: "#4D4D4D",
mb: "15px",
}}
>
Забыли пароль?
</Link> */}
<Box
sx={{
display: "flex",

@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
import { useFormik } from "formik";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import {Link as RouterLink, useLocation, useNavigate} from "react-router-dom";
import { object, ref, string } from "yup";
interface Values {
@ -50,6 +50,8 @@ export default function SignupDialog() {
const user = useUserStore((state) => state.user);
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const location = useLocation()
const navigate = useNavigate();
const formik = useFormik<Values>({
initialValues,
@ -220,6 +222,7 @@ export default function SignupDialog() {
<Link
component={RouterLink}
to="/signin"
state={{ backgroundLocation: location.state.backgroundLocation }}
sx={{
color: "#7E2AEA",
mt: "auto",

@ -1,5 +1,3 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import {
Box,
Button,
@ -9,17 +7,14 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { createQuiz } from "@root/quizes/actions";
import { useQuizes } from "@root/quizes/hooks";
import SectionWrapper from "@ui_kit/SectionWrapper";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import React from "react";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard";
import { setQuizes, createQuiz } from "@root/quizes/actions";
import { useQuizStore } from "@root/quizes/store";
interface Props {
@ -31,23 +26,14 @@ export default function MyQuizzesFull({
outerContainerSx: sx,
children,
}: Props) {
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
const quizArray = useQuizStore(state => state.quizes);
const { quizes } = useQuizes();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500));
return (
<>
{quizArray.length === 0 ? (
{quizes.length === 0 ? (
<FirstQuiz />
) : (
<SectionWrapper maxWidth="lg">
@ -83,7 +69,7 @@ export default function MyQuizzesFull({
mb: "60px",
}}
>
{quizArray.map(quiz => (
{quizes.map(quiz => (
<QuizCard
key={quiz.id}
quiz={quiz}

@ -156,7 +156,7 @@ export default function QuizCard({
anchorOrigin={{ vertical: "top", horizontal: "right" }}
>
<Box onClick={() => setSubMenuOpen(false)}>
<Button
{/* <Button
sx={{
display: "block",
width: "100%",
@ -165,7 +165,7 @@ export default function QuizCard({
onClick={()=>{}}
>
Копировать
</Button>
</Button> */}
<Button
sx={{
display: "block",

@ -29,30 +29,40 @@ import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import useSWR from "swr";
import { SidebarMobile } from "./Sidebar/SidebarMobile";
import {cleanQuestions, updateOpenBranchingPanel} from "@root/questions/actions";
import {BranchingPanel} from "../Questions/BranchingPanel";
import {useQuestionsStore} from "@root/questions/store";
import { cleanQuestions } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import { BranchingPanel } from "../Questions/BranchingPanel";
import { setQuestions } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizes } from "@root/quizes/hooks";
import { questionApi } from "@api/question";
import { useUiTools } from "@root/uiTools/store";
export default function StartPage() {
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting quiz list", error);
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
export default function EditPage() {
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList()
setQuizes(quizes)
const questions = await questionApi.getList({ quiz_id: editQuizId })
setQuestions(questions)
}
getData()
}, [])
const { openBranchingPanel } = useUiTools()
const theme = useTheme();
const navigate = useNavigate();
const editQuizId = useQuizStore(state => state.editQuizId);
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 {openBranchingPanel} = useQuestionsStore.getState()
const quizConfig = quiz?.config;
useEffect(() => {
@ -64,6 +74,7 @@ export default function StartPage() {
cleanQuestions();
}, []);
return (
<>
{/*хедер*/}
@ -208,12 +219,13 @@ export default function StartPage() {
sx={{
background: theme.palette.background.default,
width: "100%",
padding: isMobile ? "16px" : "25px",
padding: isMobile ? "16px 16px 140px 16px" : "25px",
height: "calc(100vh - 80px)",
overflow: "auto",
boxSizing: "border-box",
}}
>
{/* Выбор текущей страницы редактирования чего-либо находится здесь */}
{quizConfig &&
<>
<Stepper activeStep={currentStep} />
@ -226,7 +238,7 @@ export default function StartPage() {
</>
}
</Box>
{isTablet && [1, 2, 3].includes(currentStep) && (
{isTablet &&
<Box
sx={{
position: "absolute",
@ -240,75 +252,74 @@ export default function StartPage() {
background: "#FFF",
}}
>
<Box
sx={{
display: openBranchingPanel ? "none" : "display",
alignItems: "center",
gap: "15px",
padding: "18px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
updateOpenBranchingPanel(value)
}}
{[1, 2].includes(currentStep) && !openBranchingPanel && (
<Box
sx={{
width: 50,
height: 30,
padding: 0,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: "2px",
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(20px)",
color: theme.palette.brightPurple.main,
"& + .MuiSwitch-track": {
backgroundColor: "#E8DCF9",
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 25,
height: 25,
},
"& .MuiSwitch-track": {
borderRadius: 13,
backgroundColor:
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
display: "flex",
alignItems: "center",
gap: "15px",
padding: "18px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
/>
<Box>
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
</Typography>
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
Настройте связи между вопросами
</Typography>
>
<Switch
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
sx={{
width: 50,
height: 30,
padding: 0,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: "2px",
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(20px)",
color: theme.palette.brightPurple.main,
"& + .MuiSwitch-track": {
backgroundColor: "#E8DCF9",
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 25,
height: 25,
},
"& .MuiSwitch-track": {
borderRadius: 13,
backgroundColor:
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
}}
/>
<Box>
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
Логика ветвления
</Typography>
</Box>
</Box>
</Box>
)}
<Button
variant="contained"
sx={{
@ -320,7 +331,8 @@ export default function StartPage() {
Опубликовать
</Button>
</Box>
)}
}
</Box>
</>
);

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

@ -2,7 +2,6 @@ import AlignCenterIcon from "@icons/AlignCenterIcon";
import AlignLeftIcon from "@icons/AlignLeftIcon";
import AlignRightIcon from "@icons/AlignRightIcon";
import ArrowDown from "@icons/ArrowDownIcon";
import InfoIcon from "@icons/InfoIcon";
import LayoutCenteredIcon from "@icons/LayoutCenteredIcon";
import LayoutExpandedIcon from "@icons/LayoutExpandedIcon";
import LayoutStandartIcon from "@icons/LayoutStandartIcon";
@ -11,16 +10,14 @@ import { QuizStartpageType } from "@model/quizSettings";
import {
Box,
Button,
ButtonBase,
Checkbox,
FormControl,
FormControlLabel,
MenuItem,
Select,
Tooltip,
Typography,
useMediaQuery,
useTheme,
useTheme
} from "@mui/material";
import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
@ -28,15 +25,14 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton";
import { StartPagePreview } from "@ui_kit/StartPagePreview";
import UploadBox from "@ui_kit/UploadBox";
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
import { useState } from "react";
import { createPortal } from "react-dom";
import UploadIcon from "../../assets/icons/UploadIcon";
import FaviconDropZone from "./FaviconDropZone";
import ModalSizeImage from "./ModalSizeImage";
import SelectableIconButton from "./SelectableIconButton";
import { DropZone } from "./dropZone";
import Extra from "./extra";
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
const designTypes = [
@ -65,40 +61,20 @@ export default function StartPageSettings() {
const isTablet = useMediaQuery(theme.breakpoints.down(950));
const quiz = useCurrentQuiz();
const [formState, setFormState] = useState<"design" | "content">("design");
const designType = quiz?.config?.startpageType;
const videoHC = (videoInp: HTMLInputElement) => {
const file = videoInp.files?.[0];
if (file) {
setVideo(URL.createObjectURL(file));
}
};
const [video, setVideo] = useState("");
const [mobileVersion, setMobileVersion] = useState(false);
if (!quiz) return null;
const MobileVersionHC = (bool: boolean) => {
setMobileVersion(bool);
};
if (!quiz) return null; // TODO throw and catch with error boundary
const designType = quiz?.config?.startpageType;
const favIconDropZoneElement = (
<DropZone
sx={{ height: "48px", width: "48px" }}
deleteIconSx={{ right: -40, top: -10 }}
<FaviconDropZone
imageUrl={quiz.config.startpage.favIcon}
originalImageUrl={quiz.config.startpage.originalFavIcon}
onImageUploadClick={async file => {
const resizedImage = await resizeFavIcon(file);
uploadQuizImage(quiz.id, resizedImage, (quiz, url) => {
quiz.config.startpage.favIcon = url;
quiz.config.startpage.originalFavIcon = url;
});
}}
onImageSaveClick={async file => {
const resizedImage = await resizeFavIcon(file);
uploadQuizImage(quiz.id, resizedImage, (quiz, url) => {
quiz.config.startpage.favIcon = url;
@ -355,213 +331,70 @@ export default function StartPageSettings() {
<ModalSizeImage />
<Box
sx={{
mt: "10px",
display: "flex",
gap: "10px",
flexDirection: "column",
}}
>
<FormControlLabel
control={
<Checkbox
icon={<IconCheck />}
checkedIcon={<MobilePhoneIcon bgcolor={"#EEE4FC"} />}
/>
}
label="мобильная версия"
sx={{
color: theme.palette.brightPurple.main,
textDecorationLine: "underline",
textDecorationColor: theme.palette.brightPurple.main,
ml: "-9px",
userSelect: "none",
"& .css-14o5ia4-MuiTypography-root": {
fontSize: "16px"
}
}}
onClick={() => {
MobileVersionHC(!mobileVersion);
}}
/>
{mobileVersion ? (
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "11px",
mb: "14px",
}}
>
Изображение для мобильной версии
</Typography>
<DropZone
text={"5 MB максимум"}
imageUrl={quiz.config.startpage.background.mobile}
originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
});
}}
onDeleteClick={() => {
updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.mobile = null;
});
}}
/>
</Box>
) : (
<></>
)}
</Box>
</Box>
<Box
sx={{
display: quiz.config.startpage.background.type === "image" ? "none" : "flex",
flexDirection: "column",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "7px",
mt: "20px",
mb: "14px",
}}
>
<Typography
sx={{ fontWeight: 500, color: theme.palette.grey3.main }}
>
Добавить видео
</Typography>
<Tooltip title="Можно загрузить видео." placement="top">
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start" }}
>
<input
onChange={(event) => videoHC(event.target)}
hidden
accept=".mp4"
multiple
type="file"
/>
<UploadBox
icon={<UploadIcon />}
sx={{
height: "48px",
width: "48px",
marginBottom: "20px",
}}
/>
</ButtonBase>
{video ? <video src={video} width="400" controls /> : null}
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "5px",
}}
>
Настройки видео
</Typography>
<CustomCheckbox
label="Зацикливать видео"
checked={quiz.config.startpage.background.cycle}
handleChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.cycle = e.target.checked;
})}
/>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "11px",
mb: "14px",
}}
>
Изображение для мобильной версии
</Typography>
<DropZone
text={"5 MB максимум"}
imageUrl={quiz.config.startpage.background.mobile}
originalImageUrl={quiz.config.startpage.background.originalMobile}
onImageUploadClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
quiz.config.startpage.background.originalMobile = url;
});
}}
onImageSaveClick={file => {
uploadQuizImage(quiz.id, file, (quiz, url) => {
quiz.config.startpage.background.mobile = url;
});
}}
onDeleteClick={() => {
updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.mobile = null;
});
}}
/>
</Box>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "14px",
}}
>
Расположение элементов
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "left";
<CustomTextField
placeholder="URL видео"
text={quiz.config.startpage.background.video ?? ""}
onChange={e => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.background.video = e.target.value;
})}
isActive={quiz.config.startpage.position === "left"}
Icon={AlignLeftIcon}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "center";
})}
isActive={quiz.config.startpage.position === "center"}
Icon={AlignCenterIcon}
sx={{ display: designType === "centered" ? "flex" : "none" }}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right";
})}
isActive={quiz.config.startpage.position === "right"}
Icon={AlignRightIcon}
/>
</Box>
{designType !== "centered" &&
<>
<Typography
sx={{
fontWeight: 500,
color: theme.palette.grey3.main,
mt: "20px",
mb: "14px",
}}
>
Расположение элементов
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "left";
})}
isActive={quiz.config.startpage.position === "left"}
Icon={AlignLeftIcon}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "center";
})}
isActive={quiz.config.startpage.position === "center"}
Icon={AlignCenterIcon}
sx={{ display: designType === "standard" ? "none" : "flex" }}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right";
})}
isActive={quiz.config.startpage.position === "right"}
Icon={AlignRightIcon}
/>
</Box>
</>
}
{(isTablet || !isSmallMonitor) && (
<>
<Box
@ -613,25 +446,7 @@ export default function StartPageSettings() {
>
Favicon
</Typography>
<Box
sx={{
display: "flex",
alignItems: "end",
gap: "10px",
}}
>
{favIconDropZoneElement}
<Typography
sx={{
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
}}
>
5 MB максимум
</Typography>
</Box>
{favIconDropZoneElement}
</>
)}
</Box>
@ -695,25 +510,7 @@ export default function StartPageSettings() {
>
Favicon
</Typography>
<Box
sx={{
display: "flex",
alignItems: "end",
gap: "10px",
}}
>
{favIconDropZoneElement}
<Typography
sx={{
color: theme.palette.orange.main,
fontSize: "16px",
lineHeight: "19px",
textDecoration: "underline",
}}
>
5 MB максимум
</Typography>
</Box>
{favIconDropZoneElement}
</>
)}
{(!isSmallMonitor || (isSmallMonitor && formState === "content")) && (

@ -44,7 +44,7 @@ export const DropZone = ({ text, sx, deleteIconSx, imageUrl, originalImageUrl, o
setCropModalImageBlob,
} = useCropModalState();
if (!quiz) return null; // TODO throw and catch with error boundary
if (!quiz) return null;
async function handleImageUpload(file: File) {
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");

@ -10,7 +10,7 @@ export default function StepOne() {
const quiz = useCurrentQuiz();
const config = quiz?.config;
if (!config) return null; // TODO throw and catch with error boundary
if (!config) return null;
return (
<Box

@ -20,7 +20,7 @@ export default function Steptwo() {
const config = quiz?.config;
if (!config) return null; // TODO throw and catch with error boundary
if (!config) return null;
return (
<Box sx={{ mt: "60px" }}>

@ -1,4 +1,3 @@
import { questionApi } from "@api/question";
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
@ -11,24 +10,25 @@ import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
import { updateRootContentId } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { QuestionsStore, useQuestionsStore } from "./store";
import { useUiTools } from "../uiTools/store";
import { withErrorBoundary } from "react-error-boundary";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
const untypedQuestions = state.questions.filter(q => q.type === null);
const untypedResultQuestions = state.questions.filter(q => q.type === null || q.type === "result");
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
state.questions.push(...untypedQuestions);
state.questions.push(...untypedResultQuestions);
}, {
type: "setQuestions",
questions,
});
export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
state.questions.push({
export const createUntypedQuestion = (quizId: number, insertAfterQuestionId?: string) => setProducedState(state => {
const newUntypedQuestion = {
id: nanoid(),
quizId,
type: null,
@ -36,7 +36,16 @@ export const createUntypedQuestion = (quizId: number) => setProducedState(state
description: "",
deleted: false,
expanded: true,
});
};
if (insertAfterQuestionId) {
const index = state.questions.findIndex(q => q.id === insertAfterQuestionId);
if (index === -1) return;
state.questions.splice(index + 1, 0, newUntypedQuestion);
return;
}
state.questions.push(newUntypedQuestion);
}, {
type: "createUntypedQuestion",
quizId,
@ -87,6 +96,18 @@ const setQuestionBackendId = (questionId: string, backendId: number) => setProdu
backendId,
});
const updateQuestionOrders = () => {
const questions = useQuestionsStore.getState().questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
questions.forEach((question, index) => {
updateQuestion(question.id, question => {
question.page = index;
}, true);
});
};
export const reorderQuestions = (
sourceIndex: number,
destinationIndex: number,
@ -101,6 +122,8 @@ export const reorderQuestions = (
sourceIndex,
destinationIndex,
});
updateQuestionOrders();
};
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
@ -117,21 +140,53 @@ export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false);
}, "collapseAllQuestions");
const DELETE_TIMEOUT = 5000;
export const deleteQuestionWithTimeout = (questionId: string, deleteFn: (questionId: string) => void) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null || question.type === "result") {
queueMicrotask(() => deleteFn(questionId));
return;
}
question.deleted = true;
clearTimeout(question.deleteTimeoutId);
question.deleteTimeoutId = window.setTimeout(() => {
deleteFn(questionId);
}, DELETE_TIMEOUT);
}, {
type: "deleteQuestionWithTimeout",
questionId,
});
export const cancelQuestionDeletion = (questionId: string) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question || question.type === null || question.type === "result") return;
question.deleted = false;
clearTimeout(question.deleteTimeoutId);
}, {
type: "cancelQuestionDeletion",
questionId,
});
const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
export const updateQuestion = <T = AnyTypedQuizQuestion>(
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
updateFn: (question: T) => void,
skipQueue = false,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.type !== null && q.content.id === questionId);
if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
updateFn(question);
updateFn(question as T);
}, {
type: "updateQuestion",
questionId,
@ -139,21 +194,37 @@ export const updateQuestion = (
});
// clearTimeout(requestTimeoutId);
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const request = async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.type !== null && q.content.id === questionId);
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
const response = await questionApi.edit(questionToEditQuestionRequest(q));
try {
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
const questionResult = useQuestionsStore.getState().questions.find(questionResult => questionResult.type === "result" && questionResult.content.rule.parentId === q.content.id);
if (questionResult && q.content.rule.default.length !== 0) deleteQuestion(questionResult.quizId);
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
if (q.backendId !== response.updated) {
console.warn(`Question backend id has changed from ${q.backendId} to ${response.updated}`);
}
} catch (error) {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
}
};
if (skipQueue) {
request();
return;
}
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(request);
// }, REQUEST_DEBOUNCE);
};
@ -263,13 +334,13 @@ export const changeQuestionType = (
type: QuestionType,
) => {
updateQuestion(questionId, question => {
const oldId = question.content.id
const oldRule = question.content.rule
oldRule.main = []
const oldId = question.content.id;
const oldRule = question.content.rule;
oldRule.main = [];
question.type = type;
question.content = defaultQuestionByType[type].content;
question.content.id = oldId
question.content.rule = oldRule
question.content.id = oldId;
question.content.rule = oldRule;
});
};
@ -277,18 +348,21 @@ export const createTypedQuestion = async (
questionId: string,
type: QuestionType,
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
const questions = useQuestionsStore.getState().questions;
const question = questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
const untypedOrResultQuestionsLength = questions.filter(q => q.type === "result" || q.type === null).length;
try {
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
type,
title: question.title,
description: question.description,
page: 0,
required: true,
page: questions.length - untypedOrResultQuestionsLength,
required: false,
content: JSON.stringify(defaultQuestionByType[type].content),
});
@ -303,65 +377,35 @@ export const createTypedQuestion = async (
type: "createTypedQuestion",
question,
});
updateQuestionOrders();
} catch (error) {
devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос");
}
});
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
const { questions } = useQuestionsStore.getState()
const question = questions.find(q => q.id === questionId);
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
console.log("Я получил запрос на удаление. ИД - ", questionId)
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
console.log("delete question ", question)
if (!question) return;
if (question.type === null) {
console.log("removeQuestion")
removeQuestion(questionId);
return;
}
try {
await questionApi.delete(question.backendId);
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quizId, "")
clearRuleForAll()
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[]
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
getChildren(targetQuestion) //и ищем его потомков
}
})
}
getChildren(question)
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
const newRule = {}
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = questions.filter((q) => {
return q.content.rule.parentId === question.content.rule.parentId && q.content.id !== question.content.id
})[0]?.content.id || ""
//Если этот вопрос был дефолтным у родителя - чистим дефолт
//Смотрим можем ли мы заменить id на один из main
console.log(newRule)
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule
})
}
removeQuestion(questionId);
updateQuestionOrders();
} catch (error) {
devlog("Error deleting question", error);
enqueueSnackbar("Не удалось удалить вопрос");
@ -375,8 +419,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId
copiedQuestion.content.id = frontId
copiedQuestion.id = frontId;
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -394,9 +437,9 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId
copiedQuestion.content.id = frontId
copiedQuestion.content.rule = { main: [], parentId: "", default: "" }
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = { main: [], parentId: "", default: "", children: [] };
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -405,6 +448,8 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
questionId,
quizId,
});
updateQuestionOrders();
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
@ -419,9 +464,7 @@ function setProducedState<A extends string | { type: unknown; }>(
};
export const cleardragQuestionContentId = () => {
useQuestionsStore.setState({ dragQuestionContentId: null });
};
export const getQuestionById = (questionId: string | null) => {
if (questionId === null) return null;
@ -436,51 +479,38 @@ export const getQuestionByContentId = (questionContentId: string | null) => {
}) || null;
};
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
export const updateDragQuestionContentId = (contentId?: string) => {
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
}
export const clearRuleForAll = () => {
const { questions } = useQuestionsStore.getState()
const { questions } = useQuestionsStore.getState();
questions.forEach(question => {
if (question.type !== null && (question.content.rule.main.length > 0 || question.content.rule.default.length > 0 || question.content.rule.parentId.length > 0)) {
updateQuestion(question.content.id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
}
});
}
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: value });
};
let UDTOABM: ReturnType<typeof setTimeout>;
export const updateDesireToOpenABranchingModal = (contentId: string) => {
useQuestionsStore.setState({ desireToOpenABranchingModal: contentId })
clearTimeout(UDTOABM)
UDTOABM = setTimeout(() => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
}, 7000)
}
export const clearDesireToOpenABranchingModal = () => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
}
export const updateEditSomeQuestion = (contentId?: string) => {
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId })
}
export const createFrontResult = (quizId: number) => setProducedState(state => {
export const createFrontResult = (quizId: number, parentContentId?: string) => setProducedState(state => {
const frontId = nanoid();
const content = JSON.parse(JSON.stringify(defaultQuestionByType["result"].content));
content.id = frontId;
if (parentContentId) content.rule.parentId = parentContentId;
state.questions.push({
id: nanoid(),
id: frontId,
quizId,
type: "result",
title: "",
description: "",
deleted: false,
expanded: true,
page: 101,
required: true,
content
});
}, {
type: "createFrontResult",
@ -494,7 +524,7 @@ export const createBackResult = async (
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
if (question.type !== "result") throw new Error("Cannot upgrade already typed question");
try {
const createdQuestion = await questionApi.create({
@ -515,7 +545,7 @@ export const createBackResult = async (
rawQuestionToQuestion(createdQuestion)
);
}, {
type: "createTypedQuestion",
type: "createBackResult",
question,
});
} catch (error) {
@ -523,9 +553,3 @@ export const createBackResult = async (
enqueueSnackbar("Не удалось создать вопрос");
}
});
export const updateResult = () => {
}

@ -6,6 +6,7 @@ import useSWR from "swr";
import { setQuestions } from "./actions";
import { useQuestionsStore } from "./store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useEffect } from "react";
export function useQuestions() {

@ -5,20 +5,10 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = {
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
openedModalSettingsId: string | null;
dragQuestionContentId: string | null;
openBranchingPanel: boolean;
desireToOpenABranchingModal: string | null;
editSomeQuestion: string | null;
};
const initialState: QuestionsStore = {
questions: [],
openedModalSettingsId: null as null,
dragQuestionContentId: null,
openBranchingPanel: false,
desireToOpenABranchingModal: null as null,
editSomeQuestion: null as null,
};

@ -1,19 +1,28 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { QuestionVariant } from "../model/questionTypes/shared";
type Answer = {
questionId: string;
answer: string;
answer: string | string[];
};
type OwnVariant = {
contentId: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: Answer[];
ownVariants: OwnVariant[];
}
export const useQuizViewStore = create<QuizViewStore>()(
devtools(
(set, get) => ({
answers: [],
ownVariants: [],
}),
{
name: "quizView",
@ -21,9 +30,11 @@ export const useQuizViewStore = create<QuizViewStore>()(
)
);
export const updateAnswer = (questionId: string, answer: string) => {
export const updateAnswer = (questionId: string, answer: string | string[]) => {
const answers = [...useQuizViewStore.getState().answers];
const answerIndex = answers.findIndex((answer) => questionId === answer.questionId);
const answerIndex = answers.findIndex(
(answer) => questionId === answer.questionId
);
if (answerIndex < 0) {
answers.push({ questionId, answer });
@ -33,3 +44,53 @@ export const updateAnswer = (questionId: string, answer: string) => {
useQuizViewStore.setState({ answers });
};
export const deleteAnswer = (questionId: string) => {
const answers = [...useQuizViewStore.getState().answers];
const filteredItems = answers.filter(
(answer) => questionId !== answer.questionId
);
useQuizViewStore.setState({ answers: filteredItems });
};
export const updateOwnVariant = (contentId: string, answer: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const ownVariantIndex = ownVariants.findIndex(
(variant) => variant.contentId === contentId
);
if (ownVariantIndex < 0) {
ownVariants.push({
contentId,
variant: {
id: getRandom(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
ownVariants[ownVariantIndex].variant.answer = answer;
}
useQuizViewStore.setState({ ownVariants });
};
export const deleteOwnVariant = (contentId: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const filteredOwnVariants = ownVariants.filter(
(variant) => variant.contentId !== contentId
);
useQuizViewStore.setState({ ownVariants: filteredOwnVariants });
};
function getRandom() {
const min = Math.ceil(1000000);
const max = Math.floor(10000000);
return String(Math.floor(Math.random() * (max - min)) + min);
}

Some files were not shown because too many files have changed in this diff Show More