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

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

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

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

@ -38,9 +38,9 @@ export default function YoutobeIcon({ color, width }: Props) {
width="155.738" width="155.738"
height="115.205" height="115.205"
filterUnits="userSpaceOnUse" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB" colorInterpolationFilters="sRGB"
> >
<feFlood flood-opacity="0" result="BackgroundImageFix" /> <feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix <feColorMatrix
in="SourceAlpha" in="SourceAlpha"
type="matrix" 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 = { type InfoProps = {
width?: number; width?: number;
height?: 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 ( return (
<IconButton> <IconButton
sx={sx}
className={className}
onClick={onClick}
>
<svg <svg
width={width} width={width}
height={height} 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"> <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="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> </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 ( return (
<Box <Box
sx={{ sx={{
height: "30px", height: "50px",
width: width + "px", width: width + "px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -20,7 +20,7 @@ export default function StarIconMini({ color, width = 30, sx }: Props) {
...sx, ...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 <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" 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} fill={color}

@ -1,7 +1,7 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
interface Props { interface Props {
color: string; color?: string;
} }
export default function SettingIcon({ color }: Props) { 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: "", video: "",
}, },
rule: { rule: {
children: [],
main: [] as QuestionBranchingRuleMain[], main: [] as QuestionBranchingRuleMain[],
parentId: "", parentId: "",
default: "" default: ""

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

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

@ -10,4 +10,20 @@ body {
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; 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 "./index.css";
import lightTheme from "./utils/themes/light"; import lightTheme from "./utils/themes/light";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
import {BrowserRouter} from "react-router-dom";
dayjs.locale("ru"); dayjs.locale("ru");
@ -28,13 +30,16 @@ root.render(
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}> <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}> <ThemeProvider theme={lightTheme}>
<SnackbarProvider <BrowserRouter>
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }} <SnackbarProvider
> preventDuplicate={true}
<CssBaseline /> style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
<App /> >
</SnackbarProvider> <CssBaseline />
<App />
</SnackbarProvider>
</BrowserRouter>
</ThemeProvider> </ThemeProvider>
</LocalizationProvider> </LocalizationProvider>
</DndProvider> </DndProvider>

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

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

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

@ -1,72 +1,87 @@
import React from 'react'; import React from "react";
import Box from '@mui/material/Box'; import Box from "@mui/material/Box";
import Button from '@mui/material/Button'; import Button from "@mui/material/Button";
import SectionStyled from './SectionStyled'; import SectionStyled from "./SectionStyled";
import NavMenuItem from "@ui_kit/Header/NavMenuItem"; import NavMenuItem from "@ui_kit/Header/NavMenuItem";
import QuizLogo from "./images/icons/QuizLogo"; import QuizLogo from "./images/icons/QuizLogo";
import { useMediaQuery, useTheme } from "@mui/material"; import { useMediaQuery, useTheme } from "@mui/material";
import { setIsContactFormOpen } from "../../stores/contactForm"; 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() { export default function Component() {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)) const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [select, setSelect] = React.useState(0) const [select, setSelect] = React.useState(0);
return ( const userId = useUserStore((state) => state.userId);
<SectionStyled tag={'header'} bg={'#F2F3F7'} mwidth={'1160px'} const navigate = useNavigate();
sxsect={{ const location = useLocation()
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 onClick = () => (userId ? navigate("/list") : navigate("/signin"));
{/*>*/}
{/* {buttonMenu.map( (element, index) => (*/} return (
{/* <NavMenuItem*/} <SectionStyled
{/* text={element}*/} tag={"header"}
{/* // component={Link}*/} bg={"#F2F3F7"}
{/* // to={url}*/} mwidth={"1160px"}
{/* key={index}*/} sxsect={{
{/* onClick={() => {*/} minHeight: "80px",
{/* setSelect(index);*/} borderBottom: "1px solid #E3E3E3",
{/* }}*/} position: "relative",
{/* isActive={select === index}*/} padding: isMobile ? "0 16px" : isTablet ? "0 40px" : 0,
{/* />*/} zIndex: 3,
{/* ))}*/} }}
{/*</Box>*/} sxcont={{
<Button variant="outlined" display: "flex",
onClick={() => setIsContactFormOpen(true)} justifyContent: "space-between",
sx={{ alignItems: "center",
color: 'black', padding: 0,
border: '1px solid black', }}
textTransform: 'none', >
fontWeight: '400', <QuizLogo width={isMobile ? 100 : 150} />
fontSize: '18px', {/*<Box*/}
lineHeight: '24px', {/* sx={{*/}
borderRadius: '8px', {/* maxWidth: '595px',*/}
padding: '8px 17px', {/* width: '100%',*/}
}} {/* display: 'flex',*/}
>Предрегистрация</Button> {/* justifyContent: 'space-between',*/}
</SectionStyled> {/* 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 React from "react";
import Box from '@mui/material/Box'; import Box from "@mui/material/Box";
import Button from '@mui/material/Button'; import Button from "@mui/material/Button";
import {Typography, useMediaQuery, useTheme} from "@mui/material"; import { Typography, useMediaQuery, useTheme } from "@mui/material";
import abstraction from '../../assets/Quiz-main.png' import abstraction from "../../assets/Quiz-main.png";
import SectionStyled from './SectionStyled'; import SectionStyled from "./SectionStyled";
import { Link, redirect } from 'react-router-dom'; import { Link, redirect, useNavigate } from "react-router-dom";
import {setIsContactFormOpen} from "@root/contactForm"; import { setIsContactFormOpen } from "@root/contactForm";
import { useUserStore } from "@root/user";
export default function Component() { export default function Component() {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)) const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return( const userId = useUserStore((state) => state.userId);
<SectionStyled tag={'section'} bg={'#f2f3f7'} mwidth={'1160px'} const navigate = useNavigate();
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>*/}
<Box const tryItForFreeonClick = () => (userId ? navigate("/list") : setIsContactFormOpen(true));
component={"img"}
src={abstraction} return (
sx={{ <SectionStyled
position: "absolute", tag={"section"}
bottom: isMobile ? undefined : (isTablet? "138px" : "-291px"), bg={"#f2f3f7"}
maxWidth: isMobile ? "403px" : "810px", mwidth={"1160px"}
width: isMobile ? "100%" : undefined, sxsect={{
left: isMobile ? "-20px" : (isTablet? "54px" : "401px"), height: isMobile ? "702px" : isTablet ? "986px" : "660px",
top: isMobile ? "-345px" : undefined }}
}} sxcont={{
/> display: "flex",
</Box> justifyContent: "space-between",
</SectionStyled> 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 HowItWorks from './HowItWorks';
import BusinessPluses from './BusinessPluses'; import BusinessPluses from './BusinessPluses';
import HowToUse from './HowToUse'; import HowToUse from './HowToUse';
import WhatTheySay from './WhatTheySay';
import StartWithTemplates from './StartWithTemplates'; import StartWithTemplates from './StartWithTemplates';
import WhatTheFeatures from './WhatTheFeatures'; import WhatTheFeatures from './WhatTheFeatures';
import FullScreenDialog from "./headerMobileLanding"; import FullScreenDialog from "./headerMobileLanding";
@ -18,13 +18,14 @@ import Collaboration from "./Collaboration";
export default function Landing() { export default function Landing() {
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
return ( return (
<> <>
<CssBaseline /> <CssBaseline />
<Header/> <Header/>
<Hero/> <Hero/>
<Counter/> <Counter/>
<Collaboration/> {/* <Collaboration/> */}
<HowItWorks/> <HowItWorks/>
<BusinessPluses/> <BusinessPluses/>
<HowToUse/> <HowToUse/>

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

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

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

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

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

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

@ -6,15 +6,17 @@ import { useState, useRef, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { updateQuestion, getQuestionById } from "@root/questions/actions"; import { updateQuestion, getQuestionById } from "@root/questions/actions";
import { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
import { SelectChangeEvent } from '@mui/material/Select'; import { SelectChangeEvent } from '@mui/material/Select';
import CalendarIcon from "@icons/CalendarIcon"; import CalendarIcon from "@icons/CalendarIcon";
import { DatePicker } from "@mui/x-date-pickers"; 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 { TimePicker } from '@mui/x-date-pickers/TimePicker';
import InfoIcon from "@icons/Info"; import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"
const CONDITIONS = [ const CONDITIONS = [
"Все условия обязательны", "Все условия обязательны",
"Обязательно хотя бы одно условие", "Обязательно хотя бы одно условие",
@ -289,12 +291,12 @@ const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQu
</Typography> </Typography>
} }
<DatePicker <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) => { 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)) let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [date] newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [date]
// setParentQuestion(newParentQuestion) setParentQuestion(newParentQuestion)
}} }}
slots={{ slots={{
openPickerIcon: () => <CalendarIcon />, openPickerIcon: () => <CalendarIcon />,
@ -514,47 +516,34 @@ const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParent
(Укажите один или несколько вариантов) (Укажите один или несколько вариантов)
</Typography> </Typography>
</Box> </Box>
<TextField <Box>
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 &&
<TextField <TextField
placeholder="до" sx={{ marginTop: "20px", width: "100%" }}
sx={{ placeholder="от"
marginTop: "20px", value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[0]}
width: "100%" onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
}} const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]} 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;
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
setParentQuestion(newParentQuestion) 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 > </Box >
) )
} }

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

@ -13,18 +13,22 @@ export const BranchingQuestionsModal = ({
openedModalQuestions, openedModalQuestions,
setOpenedModalQuestions, setOpenedModalQuestions,
setModalQuestionTargetContentId, setModalQuestionTargetContentId,
setModalQuestionParentContentId setModalQuestionParentContentId,
}: Props) => { }: Props) => {
const { questions } = useQuestionsStore(); const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result"
);
const handleClose = () => { const handleClose = () => {
setOpenedModalQuestions(false); setOpenedModalQuestions(false);
}; };
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter( const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
(question) => question.type && !question.content.rule.parentId (question) => question.type && !question.content.rule.parentId && question.type !== "result"
) as AnyTypedQuizQuestion[]; ) as AnyTypedQuizQuestion[];
if (typedQuestions.length === 0) return <></>
return ( return (
<Modal open={openedModalQuestions} onClose={handleClose}> <Modal open={openedModalQuestions} onClose={handleClose}>
<Box <Box
@ -61,7 +65,7 @@ export const BranchingQuestionsModal = ({
borderRadius: "8px", borderRadius: "8px",
marginBottom: "20px", marginBottom: "20px",
boxShadow: "0px 10px 30px #e7e7e7", 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;`, border-radius: 8px;`,
"&:last-child": { marginBottom: 0 }, "&:last-child": { marginBottom: 0 },
}} }}

@ -11,7 +11,8 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } 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 MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon"; import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching"; 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 { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useQuestionsStore } from "@root/questions/store"; 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 { interface Props {
switchState: string; switchState: string;
@ -40,8 +43,13 @@ export default function ButtonsOptions({
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920)); const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const { questions } = useQuestionsStore.getState();
const openedModal = () => {
updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
};
const { openBranchingPanel } = useQuestionsStore.getState()
const buttonSetting: { const buttonSetting: {
icon: JSX.Element; icon: JSX.Element;
@ -60,15 +68,15 @@ export default function ButtonsOptions({
title: "Настройки", title: "Настройки",
value: "setting", value: "setting",
}, },
{ // {
icon: ( // icon: (
<Clue // <Clue
color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main} // color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
/> // />
), // ),
title: "Подсказка", // title: "Подсказка",
value: "help", // value: "help",
}, // },
{ {
icon: ( icon: (
<Branching <Branching
@ -80,8 +88,9 @@ export default function ButtonsOptions({
title: "Ветвление", title: "Ветвление",
value: "branching", value: "branching",
myFunc: (question) => { myFunc: (question) => {
updateOpenBranchingPanel(true) console.log("buttons opiums")
updateDesireToOpenABranchingModal(question.content.id) updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
} }
}, },
]; ];
@ -157,8 +166,9 @@ export default function ButtonsOptions({
<MiniButtonSetting <MiniButtonSetting
key={title} key={title}
onClick={() => { onClick={() => {
SSHC(value); openedModal();
myFunc(question); // SSHC(value);
// myFunc(question);
}} }}
sx={{ sx={{
backgroundColor: backgroundColor:
@ -264,24 +274,67 @@ export default function ButtonsOptions({
</IconButton> </IconButton>
<IconButton <IconButton
sx={{ borderRadius: "6px", padding: "2px" }} sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { // TODO onClick={() => {
// const removedId = question.id; const deleteFn = () => {
// if (question.deleteTimeoutId) { if (question.type !== null) {
// clearTimeout(question.deleteTimeoutId); 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(() => { //чистим rule родителя
// removeQuestionForce(quizId, removedId); const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
// }, 5000); 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, { updateQuestion(question.content.rule.parentId, (PQ) => {
// ...question, PQ.content.rule = newRule;
// deleteTimeoutId: newTimeoutId, });
// }); 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" data-cy="delete-question"
> >

@ -10,7 +10,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } 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 MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal"; import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -23,10 +23,11 @@ import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon"; import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import { QuizQuestionVariant } from "@model/questionTypes/variant"; import { QuizQuestionVariant } from "@model/questionTypes/variant";
import { updateOpenedModalSettingsId } from "@root/questions/actions"; 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 { useQuestionsStore } from "@root/questions/store";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
interface Props { interface Props {
@ -46,7 +47,7 @@ export default function ButtonsOptionsAndPict({
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050)); const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const { openBranchingPanel } = useQuestionsStore.getState() const { questions } = useQuestionsStore.getState();
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
useEffect(() => { useEffect(() => {
@ -190,8 +191,9 @@ export default function ButtonsOptionsAndPict({
onMouseEnter={() => setButtonHover("branching")} onMouseEnter={() => setButtonHover("branching")}
onMouseLeave={() => setButtonHover("")} onMouseLeave={() => setButtonHover("")}
onClick={() => { onClick={() => {
updateOpenBranchingPanel(true) console.log("buttonsOptions")
updateDesireToOpenABranchingModal(question.content.id) updateOpenBranchingPanel(true);
updateDesireToOpenABranchingModal(question.content.id);
}} }}
sx={{ sx={{
height: "30px", height: "30px",
@ -306,24 +308,67 @@ export default function ButtonsOptionsAndPict({
</IconButton> </IconButton>
<IconButton <IconButton
sx={{ borderRadius: "6px", padding: "2px" }} sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => { // TODO onClick={() => {
// const removedId = question.id; const deleteFn = () => {
// if (question.deleteTimeoutId) { if (question.type !== null) {
// clearTimeout(question.deleteTimeoutId); 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(() => { //чистим rule родителя
// removeQuestionForce(quizId, removedId); const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
// }, 5000); 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, { updateQuestion(question.content.rule.parentId, (PQ) => {
// ...question, PQ.content.rule = newRule;
// deleteTimeoutId: newTimeoutId, });
// }); 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" data-cy="delete-question"
> >

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

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

@ -29,7 +29,8 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } 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 { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
@ -39,7 +40,8 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal"; import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions"; import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question"; 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 { interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -49,6 +51,7 @@ interface Props {
} }
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) { export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
const { questions } = useQuestionsStore();
const [plusVisible, setPlusVisible] = useState<boolean>(false); const [plusVisible, setPlusVisible] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
@ -68,7 +71,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
return ( return (
<> <>
<Paper <Paper
id={question.id} id={question.id}
data-cy="quiz-question-card" data-cy="quiz-question-card"
sx={{ sx={{
maxWidth: "796px", maxWidth: "796px",
@ -99,7 +102,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
<TextField <TextField
defaultValue={question.title} defaultValue={question.title}
placeholder={"Заголовок вопроса"} placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value)} onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value || " ")}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<Box> <Box>
@ -238,24 +241,67 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
padding: "0", padding: "0",
margin: "0 5px 0 10px", margin: "0 5px 0 10px",
}} }}
onClick={() => { // TODO onClick={() => {
const removedId = question.id; const deleteFn = () => {
// if (question.deleteTimeoutId) { if (question.type !== null) {
// clearTimeout(question.deleteTimeoutId); 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(() => { //чистим rule родителя
// removeQuestionForce(quizId, removedId); const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
// }, 5000); 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, { updateQuestion(question.content.rule.parentId, (PQ) => {
// ...question, PQ.content.rule = newRule;
// deleteTimeoutId: newTimeoutId, });
// }); 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" data-cy="delete-question"
> >
@ -265,26 +311,28 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
</IconButton> </IconButton>
</Box> </Box>
)} )}
<Box {question.type !== null &&
style={{ <Box
display: "flex", style={{
alignItems: "center", display: "flex",
justifyContent: "center", alignItems: "center",
height: "30px", justifyContent: "center",
width: "30px", height: "30px",
marginLeft: "3px", width: "30px",
borderRadius: "50%", marginLeft: "3px",
fontSize: "16px", borderRadius: "50%",
color: question.expanded fontSize: "16px",
? theme.palette.brightPurple.main color: question.expanded
: "#FFF", ? theme.palette.brightPurple.main
background: question.expanded : "#FFF",
? "#EEE4FC" background: question.expanded
: theme.palette.brightPurple.main, ? "#EEE4FC"
}} : theme.palette.brightPurple.main,
> }}
{index + 1} >
</Box> {question.page + 1}
</Box>
}
<IconButton <IconButton
disableRipple disableRipple
sx={{ sx={{
@ -327,7 +375,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
}} }}
> >
<Box <Box
onClick={() => createUntypedQuestion(question.quizId)} onClick={() => createUntypedQuestion(question.quizId, question.id)}
sx={{ sx={{
display: plusVisible && !isDragging ? "flex" : "none", display: plusVisible && !isDragging ? "flex" : "none",
width: "100%", width: "100%",

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

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

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

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

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

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

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

@ -1,18 +1,23 @@
import { import {
Box, Box, useMediaQuery, useTheme,
} from "@mui/material"; } from "@mui/material";
import { DraggableList } from "./DraggableList"; import { DraggableList } from "./DraggableList";
import { SwitchBranchingPanel } from "./SwitchBranchingPanel"; import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
import { BranchingMap } from "./BranchingMap"; import { BranchingMap } from "./BranchingMap";
import {useQuestionsStore} from "@root/questions/store"; import {useQuestionsStore} from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
export const QuestionSwitchWindowTool = () => { export const QuestionSwitchWindowTool = () => {
const {openBranchingPanel} = useQuestionsStore.getState() const {questions} = useQuestionsStore.getState()
console.log(openBranchingPanel) 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 ( 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" }}> <Box sx={{ flexBasis: "796px" }}>
{openBranchingPanel? <BranchingMap /> : <DraggableList />} {openBranchingPanel? <BranchingMap /> : <DraggableList />}
</Box> </Box>

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

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

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

@ -49,7 +49,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
height: isMobile ? "100%" : "auto", height: isMobile ? "100%" : "auto",
alignItems: isMobile ? "flex-start" : "center", alignItems: isMobile ? "flex-start" : "center",
}} }}
label={"Выбор диапозона (два ползунка)"} label={"Выбор диапазона (два ползунка)"}
checked={question.content.chooseRange} checked={question.content.chooseRange}
handleChange={({ target }) => { handleChange={({ target }) => {
updateQuestion(question.id, question => { updateQuestion(question.id, question => {
@ -78,12 +78,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
<CustomCheckbox <CustomCheckbox
sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }} sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }}
label={"Необязательный вопрос"} label={"Необязательный вопрос"}
checked={!question.required} checked={!question.content.required}
handleChange={(e) => { handleChange={(e) => {
updateQuestion(question.id, question => { updateQuestion<QuizQuestionNumber>(question.id, question => {
if (question.type !== "number") return; 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 { Box, Button, IconButton, Typography } from "@mui/material";
import { ReactComponent as CheckedIcon } from "@icons/checked.svg"; import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { updateDragQuestionContentId } from "@root/questions/actions";
import { useEffect } from "react"; 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 { 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) => ({ const getItemStyle = (isDragging: any, draggableStyle: any) => ({
// some basic styles to make the items look a bit nicer // 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", background: isDragging ? "lightgreen" : "grey",
// styles we need to apply on draggables // styles we need to apply on draggables
...draggableStyle ...draggableStyle,
}); });
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion;
export const QuestionsList = () => { export const QuestionsList = () => {
const { questions, desireToOpenABranchingModal } = useQuestionsStore() const { desireToOpenABranchingModal } = useUiTools();
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result"
);
return ( return (
<Box sx={{ padding: "15px" }}> <Box sx={{ padding: "15px" }}>
@ -53,47 +63,56 @@ export const QuestionsList = () => {
}} }}
> >
{/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/} {/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/}
{questions.filter((q: AnyQuestion) => q.type).map(({ title, content }, index) => ( {questions
<Button .filter((q: AnyQuestion) => q.type)
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево) .map(({ title, content }, index) => (
if (!content.rule.parentId) updateDragQuestionContentId(content.id) <Button
}} onMouseDown={() => {
key={index} //Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
sx={{ if (!content.rule.parentId)
width: "100%", updateDragQuestionContentId(content.id);
cursor: "pointer", }}
display: "flex", key={index}
justifyContent: "space-between", sx={{
alignItems: "center", width: "100%",
padding: "12px", cursor: "pointer",
background: "#FFFFFF", display: "flex",
borderRadius: "8px", justifyContent: "space-between",
border: desireToOpenABranchingModal === content.id ? "4px solid #7e2aea" : "none", alignItems: "center",
marginBottom: "20px", padding: "12px",
boxShadow: "0px 10px 30px #e7e7e7", background: "#FFFFFF",
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"); 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;`, border-radius: 8px;`,
"&:last-child": { marginBottom: 0 }, "&:last-child": { marginBottom: 0 },
}}
>
<Typography sx={{ width: "100%", color: content.rule.parentId ? "#9A9AAF" : "#000" }}>
{title || "нет заголовка"}
</Typography>
<IconButton
onClick={() => {
updateOpenBranchingPanel(false)
updateEditSomeQuestion(content.id)
}} }}
> >
<Pencil /> <Typography
</IconButton> sx={{
{content.rule.parentId && <CheckedIcon />} width: "100%",
</Button> 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>
</Box> </Box>
); );
} };

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

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

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

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

@ -1,9 +1,9 @@
import { import {
Box, Box,
Tooltip, Tooltip,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { setQuestionInnerName, updateQuestion } from "@root/questions/actions"; import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
@ -12,165 +12,166 @@ import { useDebouncedCallback } from "use-debounce";
import InfoIcon from "../../../assets/icons/InfoIcon"; import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant"; import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
interface Props { interface Props {
question: QuizQuestionVariant; question: QuizQuestionVariant;
} }
export default function ResponseSettings({ question }: Props) { export default function ResponseSettings({ question }: Props) {
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(900)); const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990)); const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790)); const isMobile = useMediaQuery(theme.breakpoints.down(790));
const updateQuestionInnerName = useDebouncedCallback((value) => { const updateQuestionInnerName = useDebouncedCallback((value) => {
setQuestionInnerName(question.id, value); setQuestionInnerName(question.id, value);
}, 200); }, 200);
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
flexDirection: isTablet ? "column" : "none", flexDirection: isTablet ? "column" : "none",
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px", 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={{ </Typography>
pt: isMobile ? "25px" : "20px", {/* <CustomCheckbox
pb: isMobile ? "25px" : "20px", sx={{ mr: isMobile ? "0px" : "16px" }}
pl: "20px", label={"Длинный текстовый ответ"}
display: "flex", checked={question.content.largeCheck}
flexDirection: "column", handleChange={({ target }) => {
gap: "14px", updateQuestion(question.id, (question) => {
width: "100%", 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 <Box>
sx={{ <InfoIcon />
height: isMobile ? "18px" : "auto", </Box>
fontWeight: "500", </Tooltip>
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> </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 <CustomTextField
placeholder={"Текст консультанта"} placeholder={"Текст консультанта"}
text={question.content.hint.text} text={question.content.hint.text}
onChange={({ target }) => updateQuestionHint(target.value)} onChange={({ target }) => updateQuestionHint(target.value || " ")}
/> />
</> </>
) : ( ) : (

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

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

@ -1,76 +1,111 @@
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
type Props = { import { ResultSettings } from "./ResultSettings"
text: string; import { createFrontResult } from "@root/questions/actions";
text2: string; import { useQuestionsStore } from "@root/questions/store";
image: string; 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 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 ( return (
<Box <>
sx={{ <Box
flexGrow: 1, sx={{
backgroundColor: "white", flexGrow: 1,
p: "20px", backgroundColor: "white",
marginTop: "50px", p: "20px",
borderRadius: "12px", marginTop: "50px",
display: isSmallMonitor ? "block" : "flex", borderRadius: "12px",
flexDirection: isSmallMonitor ? "column" : "row", display: isSmallMonitor ? "block" : "flex",
gap: "20px", flexDirection: isSmallMonitor ? "column" : "row",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24), gap: "20px",
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525), 0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066), 0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12), 0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343), 0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`, 0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
}}
>
<Box
sx={{
mr: !isSmallMonitor ? "104px" : 0,
marginBottom: "20px",
position: "relative",
}} }}
> >
<Typography variant="h5" sx={{ marginBottom: "20px" }}>
Результаты квиза в зависимости от ответов
</Typography>
<Box <Box
sx={{ sx={{
display: "flex", mr: !isSmallMonitor ? "104px" : 0,
flexDirection: "column", marginBottom: isSmallMonitor ? "20px" : 0,
justifyContent: "space-between", position: "relative",
height: "100%", height: "100%"
maxHeight: isSmallMonitor ? "none" : "220px",
gap: "25px",
}} }}
> >
<Typography sx={{ color: "#4D4D4D", width: "95%" }}> <Typography variant="h5" sx={{ marginBottom: "20px" }}>
{text} Результаты квиза в зависимости от ответов
</Typography> </Typography>
<Typography <Box
sx={{ sx={{
color: "#9A9AAF", display: "flex",
width: "100%", flexDirection: "column",
justifyContent: "space-between",
height: "100%",
gap: "25px",
}} }}
> >
{text2} <Typography sx={{ color: "#4D4D4D", width: "95%" }}>
</Typography> Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке.
</Typography>
<Typography
sx={{
color: "#9A9AAF",
width: "100%",
}}
>
Этот шаг - необязательный, квиз будет работать и без автоматических результатов.
</Typography>
</Box>
</Box> </Box>
<img
src={image}
alt="quiz creation"
style={{
display: "block",
width: isSmallMonitor ? "100%" : "auto",
maxHeight: isSmallMonitor ? "none" : "270px",
}}
/>
</Box> </Box>
<img <Button
src={image} onClick={create}
alt="quiz creation" variant="contained"
style={{ sx={{
display: "block", backgroundColor: "#7E2AEA",
width: isSmallMonitor ? "100%" : "auto", fontSize: "18px",
maxHeight: isSmallMonitor ? "none" : "270px", 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 { useCurrentQuiz } from "@root/quizes/hooks";
import image from "../../assets/Rectangle 110.png"; import image from "../../assets/Rectangle 110.png";
import Info from "../../assets/icons/Info"; import Info from "../../assets/icons/Info";
import CreationFullCard from "./FirstEntry"; // import CreationFullCard from "./FirstEntry";
export const Result = () => { export const Result = () => {
@ -13,11 +13,11 @@ export const Result = () => {
return ( return (
<Box component="section"> <Box component="section">
<CreationFullCard {/* <CreationFullCard
text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке." text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке."
text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов." text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов."
image={image} image={image}
/> /> */}
<Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}> <Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}>
<Button <Button
variant="contained" variant="contained"

@ -1,19 +1,15 @@
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import FirstEntry from "./FirstEntry" import { FirstEntry } from "./FirstEntry"
import { ResultSettings } from "./ResultSettings"
export const ResultPage = () => {
export default function ResultPage() {
const { questions } = useQuestionsStore(); const { questions } = useQuestionsStore();
//ищём хотя бы один result //ищём хотя бы один result
const haveResult = questions.some((question) => { const haveResult = questions.some((question) => question.type === "result")
question.type === "result"
})
return ( 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 { interface Props {
text: string; text: string;
icon: 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 ( return (
<Box <Box
sx={{ sx={{
@ -24,7 +25,7 @@ export const SwitchSetting = ({ text, icon, onClick }: Props) => {
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "left", maxWidth: "756px", width: "100%" }}> <Box sx={{ display: "flex", alignItems: "center", justifyContent: "left", maxWidth: "756px", width: "100%" }}>
<img src={icon} alt="icon" /> <img src={icon} alt="icon" />
<FormControlLabel <FormControlLabel
value="start" checked={value}
control={<CustomizedSwitch />} control={<CustomizedSwitch />}
label={text} label={text}
labelPlacement="start" 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 { useState, useEffect } from "react";
import { Box, Typography, Button, useTheme } from "@mui/material"; import { Box, Button, useTheme } from "@mui/material";
import { useQuizViewStore } from "@root/quizView"; import { useQuizViewStore } from "@root/quizView";
@ -11,43 +11,92 @@ import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
type FooterProps = { type FooterProps = {
questions: AnyTypedQuizQuestion[];
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void; setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
question: QuizQuestionBase; question: AnyTypedQuizQuestion;
}; };
export const Footer = ({ setCurrentQuestion, question }: FooterProps) => { export const Footer = ({
const [disabledQuestionsId, setDisabledQuestionsId] = useState<Set<string>>( setCurrentQuestion,
new Set() questions,
); question,
}: FooterProps) => {
const [disablePreviousButton, setDisablePreviousButton] = const [disablePreviousButton, setDisablePreviousButton] =
useState<boolean>(false); useState<boolean>(false);
const [disableNextButton, setDisableNextButton] = useState<boolean>(false); const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const linear = !questions.find(
({ content }) => content.rule.parentId === "root"
);
useEffect(() => { useEffect(() => {
// Логика для аргумента disabled у кнопки "Назад" // Логика для аргумента disabled у кнопки "Назад"
if (question?.content.rule.parentId === "root") { if (linear) {
setDisablePreviousButton(true); const questionIndex = questions.findIndex(({ id }) => id === question.id);
const previousQuestion = questions[questionIndex - 1];
if (previousQuestion) {
setDisablePreviousButton(false);
} else {
setDisablePreviousButton(true);
}
} else { } else {
setDisablePreviousButton(false); if (question?.content.rule.parentId === "root") {
setDisablePreviousButton(true);
} else {
setDisablePreviousButton(false);
}
} }
// Логика для аргумента disabled у кнопки "Далее" // Логика для аргумента disabled у кнопки "Далее"
const nextQuestionId = getNextQuestionId(); const answer = answers.find(
if (nextQuestionId) { ({ questionId }) => questionId === question.content.id
);
if ("required" in question.content && question.content.required && answer) {
setDisableNextButton(false); setDisableNextButton(false);
} else {
const nextQuestion = getQuestionByContentId( return;
question.content.rule.default }
);
if (nextQuestion?.type) { 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); setDisableNextButton(false);
} else { } else {
setDisableNextButton(true); 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 = () => { const getNextQuestionId = () => {
if (answers.length) { if (answers.length) {
@ -57,28 +106,56 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
let readyBeNextQuestion = ""; let readyBeNextQuestion = "";
question.content.rule.main.forEach(({ next, rules }) => { (question as QuizQuestionBase).content.rule.main.forEach(
let longerArray = Math.max( ({ next, rules }) => {
rules[0].answers.length, let longerArray = Math.max(
[answer?.answer].length rules[0].answers.length,
); answer?.answer && Array.isArray(answer?.answer)
? answer?.answer.length
: [answer?.answer].length
);
for ( for (
var i = 0; var i = 0;
i < longerArray; i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива i++ // Цикл по всем эле­мен­там бОльшего массива
) { ) {
if (rules[0].answers[i] === answer?.answer) { if (Array.isArray(answer?.answer)) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны 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; return readyBeNextQuestion;
} }
}; };
const followPreviousStep = () => { 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") { if (question?.content.rule.parentId !== "root") {
const parent = getQuestionByContentId(question?.content.rule.parentId); const parent = getQuestionByContentId(question?.content.rule.parentId);
if (parent?.type) { if (parent?.type) {
@ -92,6 +169,18 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
}; };
const followNextStep = () => { 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(); const nextQuestionId = getNextQuestionId();
if (nextQuestionId) { if (nextQuestionId) {

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

@ -1,162 +1,298 @@
import { useParams } from "react-router-dom";
import { import {
Box, Box,
Button, Button,
ButtonBase,
Link,
Paper,
Typography, Typography,
useTheme,
useMediaQuery, useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import useSWR from "swr"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { isAxiosError } from "axios"; import YoutubeEmbedIframe from "../../ui_kit/StartPagePreview/YoutubeEmbedIframe";
import { enqueueSnackbar } from "notistack"; import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { devlog } from "@frontend/kitui"; 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"; export const StartPageViewPublication = ({setVisualStartPage}:Props) => {
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();
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630)); const quiz = useCurrentQuiz();
const quiz = quizes.find(({ backendId }) => quizId === backendId); const { isMobileDevice } = useUADevice();
const isMediaFileExist =
quiz?.config.startpage.background.desktop ||
quiz?.config.startpage.background.video;
useSWR("quizes", () => quizApi.getList(), { if (!quiz) return null;
onSuccess: setQuizes,
onError: (error: unknown) => {
const message = isAxiosError<string>(error)
? error.response?.data ?? ""
: "";
devlog("Error getting quiz list", error); const handleCopyNumber = () => {
enqueueSnackbar(`Не удалось получить квизы. ${message}`); 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 ( return (
<Box <Paper className="quiz-preview-draghandle"
sx={{ sx={{
height: "100vh", height: "100vh",
display: "flex", background: quiz.config.startpageType === "expanded" ?
flexDirection: quiz.config.startpage.position === "left" ? "linear-gradient(90deg,#272626,transparent)" :
quiz?.config.startpage.position === "left" ? "row" : "row-reverse", quiz.config.startpage.position === "center" ? "linear-gradient(180deg,transparent,#272626)" :
flexGrow: 1, "linear-gradient(270deg,#272626,transparent)"
"&::-webkit-scrollbar": { width: 0 }, : "",
}} color: quiz.config.startpageType === "expanded" ? "white" : "black"
>
<Box
sx={{ }}>
width: isMediaFileExist && !isTablet ? "40%" : "100%", <QuizPreviewLayoutByType
padding: "16px", quizHeaderBlock={<Box
display: "flex", p={quiz.config.startpageType === "standard" ? "" : "16px"}
flexDirection: "column", >
alignItems: isMediaFileExist && !isTablet ? "flex-start" : "center", <Box sx={{
}}
>
<Box
sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "20px", gap: "20px",
}} mb: "7px"
> }}>
{quiz?.config.startpage.background.mobile && ( {quiz.config.startpage.logo && (
<img <img
src={quiz.config.startpage.background.mobile} src={quiz.config.startpage.logo}
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
style={{ style={{
width: "100%", height: "37px",
height: "100%", maxWidth: "43px",
objectFit: "cover", objectFit: "cover",
}} }}
alt=""
/> />
)} )}
</Box> <Typography sx={{ fontSize: "14px" }}>
)} {quiz.config.info.orgname}
</Box> </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 { Box } from "@mui/material";
import { StartPageViewPublication } from "./StartPageViewPublication"; import { StartPageViewPublication } from "./StartPageViewPublication";
import { Question } from "./Question"; import { Question } from "./Question";
import { useQuestions } from "@root/questions/hooks"; import { useQuestions } from "@root/questions/hooks";
import { useCurrentQuiz } from "@root/quizes/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 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 = () => { export const ViewPage = () => {
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const { questions } = useQuestions(); const { editQuizId } = useQuizStore();
const [visualStartPage, setVisualStartPage] = useState<boolean>(!quiz?.config.noStartPage); 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( const [visualStartPage, setVisualStartPage] = useState<boolean>();
({ type }) => type
) as AnyTypedQuizQuestion[];
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 ( return (
<Box> <Box>
{visualStartPage ? ( {!visualStartPage ? (
<StartPageViewPublication <StartPageViewPublication setVisualStartPage={setVisualStartPage} />
setVisualStartPage={setVisualStartPage}
showNextButton={!!filteredQuestions.length}
/>
) : ( ) : (
<Question <Question questions={filteredQuestions} />
questions={filteredQuestions}
/>
)} )}
</Box> </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 { Box, Typography } from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView"; import { useQuizViewStore, updateAnswer } from "@root/quizView";
import "react-datepicker/dist/react-datepicker.css";
import type { QuizQuestionDate } from "../../../model/questionTypes/date"; import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon";
type DateProps = { type DateProps = {
currentQuestion: QuizQuestionDate; currentQuestion: QuizQuestionDate;
@ -13,10 +13,10 @@ type DateProps = {
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const answer = answers.find(
answers.find( ({ questionId }) => questionId === currentQuestion.content.id
({ questionId }) => questionId === currentQuestion.content.id )?.answer as string;
) ?? {}; const [day, month, year] = answer?.split(".") || [];
return ( return (
<Box> <Box>
@ -30,19 +30,54 @@ export const Date = ({ currentQuestion }: DateProps) => {
}} }}
> >
<DatePicker <DatePicker
selected={answer ? new window.Date(answer) : new window.Date()} slots={{
onChange={(date) => openPickerIcon: () => <CalendarIcon />,
}}
value={dayjs(
answer
? new window.Date(`${month}.${day}.${year}`)
: new window.Date()
)}
onChange={(date) => {
if (!date) {
return;
}
updateAnswer( updateAnswer(
currentQuestion.content.id, currentQuestion.content.id,
String( String(
date?.toLocaleDateString("ru-RU", { new window.Date(date.toDate()).toLocaleDateString("ru-RU", {
year: "numeric", year: "numeric",
month: "2-digit", month: "2-digit",
day: "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>
</Box> </Box>

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

@ -1,28 +1,56 @@
import { Box, Typography, ButtonBase } from "@mui/material"; import {
Box,
import UploadBox from "@ui_kit/UploadBox"; Typography,
ButtonBase,
useTheme,
IconButton,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView"; import { useQuizViewStore, updateAnswer } from "@root/quizView";
import { UPLOAD_FILE_TYPES_MAP } from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File"; import { UPLOAD_FILE_TYPES_MAP } from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File";
import UploadIcon from "@icons/UploadIcon"; import UploadIcon from "@icons/UploadIcon";
import CloseBold from "@icons/CloseBold";
import type { ChangeEvent } from "react"; import type { ChangeEvent } from "react";
import type { QuizQuestionFile } from "../../../model/questionTypes/file"; import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import type { DragEvent } from "react";
import type { UploadFileType } from "@model/questionTypes/file";
type FileProps = { type FileProps = {
currentQuestion: QuizQuestionFile; 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) => { export const File = ({ currentQuestion }: FileProps) => {
const { answers } = useQuizViewStore(); 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 uploadFile = ({ target }: ChangeEvent<HTMLInputElement>) => {
const file = target.files?.[0]; const file = target.files?.[0];
if (file) { 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", flexDirection: "column",
width: "100%", width: "100%",
marginTop: "20px", 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] && ( {answer?.split("|")[0] && (
<Typography sx={{ marginTop: "15px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
{answer?.split("|")[0]} <Typography>Вы загрузили:</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>
</Box> </Box>

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

@ -1,7 +1,9 @@
import { useEffect } from "react"; import { useState, useEffect } from "react";
import { Box, Typography, Slider, useTheme } from "@mui/material"; import { Box, Typography, Slider, useTheme } from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { CustomSlider } from "@ui_kit/CustomSlider";
import { useQuizViewStore, updateAnswer } from "@root/quizView"; import { useQuizViewStore, updateAnswer } from "@root/quizView";
@ -12,16 +14,47 @@ type NumberProps = {
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100");
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); 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 min = window.Number(currentQuestion.content.range.split("—")[0]);
const max = window.Number(currentQuestion.content.range.split("—")[1]); const max = window.Number(currentQuestion.content.range.split("—")[1]);
const sliderValue = answer || currentQuestion.content.start + "—" + max;
useEffect(() => { useEffect(() => {
if (answer) {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
}
if (!answer) { 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", flexDirection: "column",
width: "100%", width: "100%",
marginTop: "20px", marginTop: "20px",
gap: "30px",
}} }}
> >
<CustomTextField <CustomSlider
placeholder="0" value={
value={answer || ""} currentQuestion.content.chooseRange
onChange={({ target }) => { ? sliderValue.split("—").length || 0 > 1
updateAnswer( ? sliderValue.split("—").map((item) => window.Number(item))
currentQuestion.content.id, : [min, min + 1]
window.Number(target.value) > max : window.Number(sliderValue.split("—")[0])
? String(max) }
: window.Number(target.value) < min
? String(min)
: target.value
);
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
}}
/>
<Slider
value={window.Number(answer || 1)}
min={min} min={min}
max={max} max={max}
step={currentQuestion.content.step || 1} step={currentQuestion.content.step || 1}
sx={{
color: theme.palette.brightPurple.main,
padding: "0",
marginTop: "25px",
}}
onChange={(_, value) => { 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>
</Box> </Box>
); );

@ -7,6 +7,12 @@ import {
import { useQuizViewStore, updateAnswer } from "@root/quizView"; 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 StarIconMini from "@icons/questionsPage/StarIconMini";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating"; import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
@ -15,53 +21,82 @@ type RatingProps = {
currentQuestion: QuizQuestionRating; 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) => { export const Rating = ({ currentQuestion }: RatingProps) => {
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); 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 ( return (
<Box> <Box>
<Typography variant="h5">{currentQuestion.title}</Typography> <Typography variant="h5">{currentQuestion.title}</Typography>
<Box <Box
sx={{ sx={{
display: "flex", display: "inline-flex",
flexDirection: "column", alignItems: "center",
width: "100%", gap: "20px",
marginTop: "20px", marginTop: "20px",
}} }}
> >
<RatingComponent <Typography sx={{ color: theme.palette.grey2.main }}>
value={Number(answer || 0)} {currentQuestion.content.ratingNegativeDescription}
onChange={(_, value) => updateAnswer(currentQuestion.content.id, String(value))} </Typography>
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)" }}
/>
}
/>
<Box <Box
sx={{ sx={{
display: "flex", display: "inline-block",
justifyContent: "space-between", width: "100%",
maxWidth: `${currentQuestion.content.steps * 50}px`,
color: theme.palette.grey2.main,
}} }}
> >
<Typography>{currentQuestion.content.ratingNegativeDescription}</Typography> <RatingComponent
<Typography>{currentQuestion.content.ratingPositiveDescription}</Typography> 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> </Box>
<Typography sx={{ color: theme.palette.grey2.main }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box> </Box>
</Box> </Box>
); );

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

@ -3,31 +3,57 @@ import {
Box, Box,
Typography, Typography,
RadioGroup, RadioGroup,
FormGroup,
FormControlLabel, FormControlLabel,
Radio, Radio,
Checkbox,
TextField,
useTheme, useTheme,
} from "@mui/material"; } 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 RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { CheckboxIcon } from "@icons/Checkbox";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant"; import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
type VariantProps = { type VariantProps = {
stepNumber: number; stepNumber: number;
currentQuestion: QuizQuestionVariant; currentQuestion: QuizQuestionVariant;
}; };
type VariantItemProps = {
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
};
export const Variant = ({ currentQuestion }: VariantProps) => { export const Variant = ({ currentQuestion }: VariantProps) => {
const { answers } = useQuizViewStore(); const { answers, ownVariants } = useQuizViewStore();
const theme = useTheme(); const { answer } =
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; 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(() => { useEffect(() => {
if (!answer) { if (!ownVariant) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id); updateOwnVariant(currentQuestion.content.id, "");
} }
}, []); }, []);
@ -35,15 +61,11 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
<Box> <Box>
<Typography variant="h5">{currentQuestion.title}</Typography> <Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}> <Box sx={{ display: "flex" }}>
<RadioGroup <Group
name={currentQuestion.id} name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)} value={currentQuestion.content.variants.findIndex(
onChange={({ target }) => ({ id }) => answer === id
updateAnswer( )}
currentQuestion.content.id,
currentQuestion.content.variants[Number(target.value)].id
)
}
sx={{ sx={{
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
@ -53,30 +75,35 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
marginTop: "20px", marginTop: "20px",
}} }}
> >
<Box sx={{ display: "flex", flexDirection: "row", flexWrap: "wrap", width: "100%", gap: "20px", }}> <Box
{currentQuestion.content.variants.map(({ id, answer }, index) => ( sx={{
<FormControlLabel display: "flex",
key={id} flexDirection: "row",
sx={{ flexWrap: "wrap",
margin: "0", width: "100%",
borderRadius: "12px", gap: "20px",
padding: "15px", }}
border: `1px solid ${theme.palette.grey2.main}`, >
display: "flex", {currentQuestion.content.variants.map((variant, index) => (
maxWidth: "685px", <VariantItem
justifyContent: "space-between", key={variant.id}
width: "100%" currentQuestion={currentQuestion}
}} variant={variant}
value={index} answer={answer}
labelPlacement="start" index={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={answer}
/> />
))} ))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
/>
)}
</Box> </Box>
</RadioGroup> </Group>
{currentQuestion.content.back && ( {currentQuestion.content.back && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}> <Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img <img
@ -90,3 +117,70 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
</Box> </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 { import {
Box, Box,
Typography, Typography,
@ -8,7 +7,9 @@ import {
useTheme, useTheme,
} from "@mui/material"; } 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 RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
@ -22,67 +23,97 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => { export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; const { answer } =
const variant = currentQuestion.content.variants.find(({ id }) => answer === id); answers.find(
({ questionId }) => questionId === currentQuestion.content.id
) ?? {};
const variant = currentQuestion.content.variants.find(
({ id }) => answer === id
);
useEffect(() => { console.log(currentQuestion)
if (!answer) {
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
}
}, []);
return ( return (
<Box> <Box>
<Typography variant="h5">{currentQuestion.title}</Typography> <Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}> <Box sx={{ display: "flex", marginTop: "20px" }}>
<RadioGroup <RadioGroup
name={currentQuestion.id} name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)} value={currentQuestion.content.variants.findIndex(
onChange={({ target }) => ({ id }) => answer === id
updateAnswer( )}
currentQuestion.content.id,
currentQuestion.content.variants[Number(target.value)].id
)
}
sx={{ sx={{
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
flexBasis: "100%", flexBasis: "100%",
marginTop: "20px",
}} }}
> >
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}> <Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
{currentQuestion.content.variants.map(({ id, answer }, index) => ( {currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel <FormControlLabel
key={id} key={variant.id}
sx={{ sx={{
marginBottom: "15px", marginBottom: "15px",
borderRadius: "5px", borderRadius: "5px",
padding: "15px", padding: "15px",
color: theme.palette.grey2.main, color: "#4D4D4D",
border: `1px solid ${theme.palette.grey2.main}`, border: `1px solid ${theme.palette.grey2.main}`,
display: "flex", display: "flex",
}} }}
value={index} 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={ control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} /> <Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
} }
label={answer} label={variant.answer}
/> />
))} ))}
</Box> </Box>
</RadioGroup> </RadioGroup>
{(variant?.extendedText || currentQuestion.content.back) && ( {/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}> <Box
<img sx={{
src={answer ? variant?.extendedText : currentQuestion.content.back} 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" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt="" alt=""
/> />
:
(variant?.extendedText || "Выберите вариант ответа слева")
}
</Box> </Box>
)} {/* )} */}
</Box> </Box>
</Box> </Box>
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

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

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

@ -1,5 +1,3 @@
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import { import {
Box, Box,
Button, Button,
@ -9,17 +7,14 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { createQuiz } from "@root/quizes/actions";
import { useQuizes } from "@root/quizes/hooks";
import SectionWrapper from "@ui_kit/SectionWrapper"; import SectionWrapper from "@ui_kit/SectionWrapper";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import useSWR from "swr";
import ComplexNavText from "./ComplexNavText"; import ComplexNavText from "./ComplexNavText";
import FirstQuiz from "./FirstQuiz"; import FirstQuiz from "./FirstQuiz";
import QuizCard from "./QuizCard"; import QuizCard from "./QuizCard";
import { setQuizes, createQuiz } from "@root/quizes/actions";
import { useQuizStore } from "@root/quizes/store";
interface Props { interface Props {
@ -31,23 +26,14 @@ export default function MyQuizzesFull({
outerContainerSx: sx, outerContainerSx: sx,
children, children,
}: Props) { }: Props) {
useSWR("quizes", () => quizApi.getList(), { const { quizes } = useQuizes();
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 navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
return ( return (
<> <>
{quizArray.length === 0 ? ( {quizes.length === 0 ? (
<FirstQuiz /> <FirstQuiz />
) : ( ) : (
<SectionWrapper maxWidth="lg"> <SectionWrapper maxWidth="lg">
@ -83,7 +69,7 @@ export default function MyQuizzesFull({
mb: "60px", mb: "60px",
}} }}
> >
{quizArray.map(quiz => ( {quizes.map(quiz => (
<QuizCard <QuizCard
key={quiz.id} key={quiz.id}
quiz={quiz} quiz={quiz}

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

@ -29,30 +29,40 @@ import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import useSWR from "swr"; import useSWR from "swr";
import { SidebarMobile } from "./Sidebar/SidebarMobile"; import { SidebarMobile } from "./Sidebar/SidebarMobile";
import {cleanQuestions, updateOpenBranchingPanel} from "@root/questions/actions"; import { cleanQuestions } from "@root/questions/actions";
import {BranchingPanel} from "../Questions/BranchingPanel"; import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store"; 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 theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const editQuizId = useQuizStore(state => state.editQuizId);
const quiz = useCurrentQuiz();
const currentStep = useQuizStore(state => state.currentStep); const currentStep = useQuizStore(state => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660)); const isMobile = useMediaQuery(theme.breakpoints.down(660));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false); const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const {openBranchingPanel} = useQuestionsStore.getState()
const quizConfig = quiz?.config; const quizConfig = quiz?.config;
useEffect(() => { useEffect(() => {
@ -64,6 +74,7 @@ export default function StartPage() {
cleanQuestions(); cleanQuestions();
}, []); }, []);
return ( return (
<> <>
{/*хедер*/} {/*хедер*/}
@ -208,12 +219,13 @@ export default function StartPage() {
sx={{ sx={{
background: theme.palette.background.default, background: theme.palette.background.default,
width: "100%", width: "100%",
padding: isMobile ? "16px" : "25px", padding: isMobile ? "16px 16px 140px 16px" : "25px",
height: "calc(100vh - 80px)", height: "calc(100vh - 80px)",
overflow: "auto", overflow: "auto",
boxSizing: "border-box", boxSizing: "border-box",
}} }}
> >
{/* Выбор текущей страницы редактирования чего-либо находится здесь */}
{quizConfig && {quizConfig &&
<> <>
<Stepper activeStep={currentStep} /> <Stepper activeStep={currentStep} />
@ -226,7 +238,7 @@ export default function StartPage() {
</> </>
} }
</Box> </Box>
{isTablet && [1, 2, 3].includes(currentStep) && ( {isTablet &&
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
@ -240,75 +252,74 @@ export default function StartPage() {
background: "#FFF", background: "#FFF",
}} }}
> >
<Box {[1, 2].includes(currentStep) && !openBranchingPanel && (
sx={{ <Box
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)
}}
sx={{ sx={{
width: 50, display: "flex",
height: 30, alignItems: "center",
padding: 0, gap: "15px",
"& .MuiSwitch-switchBase": { padding: "18px",
padding: 0, background: "#fff",
margin: "2px", borderRadius: "12px",
transitionDuration: "300ms", boxShadow: "0px 10px 30px #e7e7e7",
"&.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> <Switch
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}> checked={openBranchingPanel}
Логика ветвления onChange={
</Typography> (e) => updateOpenBranchingPanel(e.target.checked)
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}> }
Настройте связи между вопросами sx={{
</Typography> 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>
</Box> )}
<Button <Button
variant="contained" variant="contained"
sx={{ sx={{
@ -320,7 +331,8 @@ export default function StartPage() {
Опубликовать Опубликовать
</Button> </Button>
</Box> </Box>
)} }
</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 AlignLeftIcon from "@icons/AlignLeftIcon";
import AlignRightIcon from "@icons/AlignRightIcon"; import AlignRightIcon from "@icons/AlignRightIcon";
import ArrowDown from "@icons/ArrowDownIcon"; import ArrowDown from "@icons/ArrowDownIcon";
import InfoIcon from "@icons/InfoIcon";
import LayoutCenteredIcon from "@icons/LayoutCenteredIcon"; import LayoutCenteredIcon from "@icons/LayoutCenteredIcon";
import LayoutExpandedIcon from "@icons/LayoutExpandedIcon"; import LayoutExpandedIcon from "@icons/LayoutExpandedIcon";
import LayoutStandartIcon from "@icons/LayoutStandartIcon"; import LayoutStandartIcon from "@icons/LayoutStandartIcon";
@ -11,16 +10,14 @@ import { QuizStartpageType } from "@model/quizSettings";
import { import {
Box, Box,
Button, Button,
ButtonBase,
Checkbox, Checkbox,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
MenuItem, MenuItem,
Select, Select,
Tooltip,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme
} from "@mui/material"; } from "@mui/material";
import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions"; import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
@ -28,15 +25,14 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import SelectableButton from "@ui_kit/SelectableButton"; import SelectableButton from "@ui_kit/SelectableButton";
import { StartPagePreview } from "@ui_kit/StartPagePreview"; import { StartPagePreview } from "@ui_kit/StartPagePreview";
import UploadBox from "@ui_kit/UploadBox"; import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
import { useState } from "react"; import { useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import UploadIcon from "../../assets/icons/UploadIcon"; import FaviconDropZone from "./FaviconDropZone";
import ModalSizeImage from "./ModalSizeImage"; import ModalSizeImage from "./ModalSizeImage";
import SelectableIconButton from "./SelectableIconButton"; import SelectableIconButton from "./SelectableIconButton";
import { DropZone } from "./dropZone"; import { DropZone } from "./dropZone";
import Extra from "./extra"; import Extra from "./extra";
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
const designTypes = [ const designTypes = [
@ -65,40 +61,20 @@ export default function StartPageSettings() {
const isTablet = useMediaQuery(theme.breakpoints.down(950)); const isTablet = useMediaQuery(theme.breakpoints.down(950));
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const [formState, setFormState] = useState<"design" | "content">("design"); 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); const [mobileVersion, setMobileVersion] = useState(false);
if (!quiz) return null;
const MobileVersionHC = (bool: boolean) => { const MobileVersionHC = (bool: boolean) => {
setMobileVersion(bool); setMobileVersion(bool);
}; };
if (!quiz) return null; // TODO throw and catch with error boundary const designType = quiz?.config?.startpageType;
const favIconDropZoneElement = ( const favIconDropZoneElement = (
<DropZone <FaviconDropZone
sx={{ height: "48px", width: "48px" }}
deleteIconSx={{ right: -40, top: -10 }}
imageUrl={quiz.config.startpage.favIcon} imageUrl={quiz.config.startpage.favIcon}
originalImageUrl={quiz.config.startpage.originalFavIcon}
onImageUploadClick={async file => { 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); const resizedImage = await resizeFavIcon(file);
uploadQuizImage(quiz.id, resizedImage, (quiz, url) => { uploadQuizImage(quiz.id, resizedImage, (quiz, url) => {
quiz.config.startpage.favIcon = url; quiz.config.startpage.favIcon = url;
@ -355,213 +331,70 @@ export default function StartPageSettings() {
<ModalSizeImage /> <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>
<Box <Box
sx={{ sx={{
display: quiz.config.startpage.background.type === "image" ? "none" : "flex", display: quiz.config.startpage.background.type === "image" ? "none" : "flex",
flexDirection: "column", 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", mt: "20px",
mb: "14px",
}} }}
> >
Расположение элементов <CustomTextField
</Typography> placeholder="URL видео"
<Box text={quiz.config.startpage.background.video ?? ""}
sx={{ onChange={e => updateQuiz(quiz.id, quiz => {
display: "flex", quiz.config.startpage.background.video = e.target.value;
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 === "centered" ? "flex" : "none" }}
/>
<SelectableIconButton
onClick={() => updateQuiz(quiz.id, quiz => {
quiz.config.startpage.position = "right";
})}
isActive={quiz.config.startpage.position === "right"}
Icon={AlignRightIcon}
/> />
</Box> </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) && ( {(isTablet || !isSmallMonitor) && (
<> <>
<Box <Box
@ -613,25 +446,7 @@ export default function StartPageSettings() {
> >
Favicon Favicon
</Typography> </Typography>
<Box {favIconDropZoneElement}
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>
</> </>
)} )}
</Box> </Box>
@ -695,25 +510,7 @@ export default function StartPageSettings() {
> >
Favicon Favicon
</Typography> </Typography>
<Box {favIconDropZoneElement}
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>
</> </>
)} )}
{(!isSmallMonitor || (isSmallMonitor && formState === "content")) && ( {(!isSmallMonitor || (isSmallMonitor && formState === "content")) && (

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

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

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

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

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

@ -5,20 +5,10 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = { export type QuestionsStore = {
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[]; questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
openedModalSettingsId: string | null;
dragQuestionContentId: string | null;
openBranchingPanel: boolean;
desireToOpenABranchingModal: string | null;
editSomeQuestion: string | null;
}; };
const initialState: QuestionsStore = { const initialState: QuestionsStore = {
questions: [], 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 { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
import type { QuestionVariant } from "../model/questionTypes/shared";
type Answer = { type Answer = {
questionId: string; questionId: string;
answer: string; answer: string | string[];
};
type OwnVariant = {
contentId: string;
variant: QuestionVariant;
}; };
interface QuizViewStore { interface QuizViewStore {
answers: Answer[]; answers: Answer[];
ownVariants: OwnVariant[];
} }
export const useQuizViewStore = create<QuizViewStore>()( export const useQuizViewStore = create<QuizViewStore>()(
devtools( devtools(
(set, get) => ({ (set, get) => ({
answers: [], answers: [],
ownVariants: [],
}), }),
{ {
name: "quizView", 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 answers = [...useQuizViewStore.getState().answers];
const answerIndex = answers.findIndex((answer) => questionId === answer.questionId); const answerIndex = answers.findIndex(
(answer) => questionId === answer.questionId
);
if (answerIndex < 0) { if (answerIndex < 0) {
answers.push({ questionId, answer }); answers.push({ questionId, answer });
@ -33,3 +44,53 @@ export const updateAnswer = (questionId: string, answer: string) => {
useQuizViewStore.setState({ answers }); 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