Merge branch 'dev' into 'main'
хелпер не учитывает результ и безтиповые вопросы, CSкомпонент не получает... See merge request frontend/squiz!73
19013
package-lock.json
generated
Normal file
@ -35,7 +35,6 @@
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-cytoscapejs": "^2.0.0",
|
||||
"react-datepicker": "^4.24.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
@ -78,7 +77,6 @@
|
||||
"@types/cytoscape-popper": "^2.0.4",
|
||||
"@types/react-beautiful-dnd": "^13.1.4",
|
||||
"@types/react-cytoscapejs": "^1.2.4",
|
||||
"@types/react-datepicker": "^4.19.3",
|
||||
"craco-alias": "^3.0.1",
|
||||
"cypress": "^13.4.0"
|
||||
}
|
||||
|
35
src/App.tsx
@ -5,20 +5,21 @@ import "dayjs/locale/ru";
|
||||
import SigninDialog from "./pages/auth/Signin";
|
||||
import SignupDialog from "./pages/auth/Signup";
|
||||
import { ViewPage } from "./pages/ViewPublicationPage";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { BrowserRouter, Route, Routes, useLocation, useNavigate, Navigate } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import ContactFormPage from "./pages/ContactFormPage/ContactFormPage";
|
||||
import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
|
||||
import Landing from "./pages/Landing/Landing";
|
||||
import QuestionsPage from "./pages/Questions/QuestionsPage";
|
||||
import { Result } from "./pages/ResultPage/Result";
|
||||
import { Setting } from "./pages/ResultPage/Setting";
|
||||
import { ResultSettings } from "./pages/ResultPage/ResultSettings";
|
||||
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
|
||||
import Main from "./pages/main";
|
||||
import StartPage from "./pages/startPage/StartPage";
|
||||
import EditPage from "./pages/startPage/EditPage";
|
||||
import { clearAuthToken, getMessageFromFetchError, useUserFetcher } from "@frontend/kitui";
|
||||
import { clearUserData, setUser, useUserStore } from "@root/user";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import PrivateRoute from "@ui_kit/PrivateRoute";
|
||||
|
||||
|
||||
dayjs.locale("ru");
|
||||
@ -28,12 +29,14 @@ const routeslink = [
|
||||
{ path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true, },
|
||||
{ path: "/contacts", page: <ContactFormPage />, header: true, sidebar: true },
|
||||
{ path: "/result", page: <Result />, header: true, sidebar: true },
|
||||
{ path: "/settings", page: <Setting />, header: true, sidebar: true },
|
||||
{ path: "/settings", page: <ResultSettings />, header: true, sidebar: true },
|
||||
{ path: "/install", page: <InstallQuiz />, header: true, sidebar: true },
|
||||
] as const;
|
||||
|
||||
export default function App() {
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useUserFetcher({
|
||||
url: `https://hub.pena.digital/user/${userId}`,
|
||||
@ -48,23 +51,31 @@ export default function App() {
|
||||
}
|
||||
},
|
||||
});
|
||||
if (location.state?.redirectTo)
|
||||
return <Navigate to={location.state.redirectTo} replace state={{ backgroundLocation: location }} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactFormModal />
|
||||
<BrowserRouter>
|
||||
{location.state?.backgroundLocation && (
|
||||
<Routes>
|
||||
<Route path="/signin" element={<SigninDialog />} />
|
||||
<Route path="/signup" element={<SignupDialog />} />
|
||||
</Routes>
|
||||
)}
|
||||
<Routes location={location.state?.backgroundLocation || location}>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/signin" element={<Navigate to="/" replace state={{ redirectTo: "/signin" }} />} />
|
||||
<Route path="/signup" element={<Navigate to="/" replace state={{ redirectTo: "/signup" }} />} />
|
||||
<Route element={<PrivateRoute />}>
|
||||
{routeslink.map((e, i) => (
|
||||
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
|
||||
))}
|
||||
<Route path="edit" element={<StartPage />} />
|
||||
<Route path="edit" element={<EditPage />} />
|
||||
<Route path="crop" element={<ImageCrop />} />
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/signin" element={<SigninDialog />} />
|
||||
<Route path="/signup" element={<SignupDialog />} />
|
||||
<Route path="/view" element={<ViewPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</Route>
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ function createQuestion(body: CreateQuestionRequest) {
|
||||
}
|
||||
|
||||
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
|
||||
console.log("body" , body)
|
||||
if (!body?.quiz_id) return null;
|
||||
|
||||
const response = await makeRequest<GetQuestionListRequest, GetQuestionListResponse>({
|
||||
|
@ -173,9 +173,9 @@ export default function Notebook({ color }: Props) {
|
||||
width="0.998971"
|
||||
height="1.57978"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
@ -194,9 +194,9 @@ export default function Notebook({ color }: Props) {
|
||||
width="2.51719"
|
||||
height="2.51694"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
@ -215,9 +215,9 @@ export default function Notebook({ color }: Props) {
|
||||
width="1.11616"
|
||||
height="1.57978"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
@ -236,9 +236,9 @@ export default function Notebook({ color }: Props) {
|
||||
width="1.22643"
|
||||
height="0.993766"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
@ -257,9 +257,9 @@ export default function Notebook({ color }: Props) {
|
||||
width="279"
|
||||
height="12.0981"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
|
@ -38,9 +38,9 @@ export default function YoutobeIcon({ color, width }: Props) {
|
||||
width="155.738"
|
||||
height="115.205"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 823 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
43
src/assets/icons/Checkbox.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
86
src/assets/icons/CloseBold.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
37
src/assets/icons/ExpandLessIconBG.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useTheme, SxProps, Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
export default function ExpandIcon({ sx }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
...sx,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 30 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="30" height="30" rx="6" fill="#EEE4FC" />
|
||||
<path
|
||||
d="M22.5 11.25L15 18.75L7.5 11.25"
|
||||
stroke="#7E2AEA"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,13 +1,20 @@
|
||||
import { IconButton } from "@mui/material";
|
||||
import { IconButton, SxProps } from "@mui/material";
|
||||
|
||||
type InfoProps = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sx?: SxProps;
|
||||
onClick?: any;
|
||||
className?: string
|
||||
};
|
||||
|
||||
export default function Info({ width = 20, height = 20 }: InfoProps) {
|
||||
export default function Info({ width = 20, height = 20, sx, onClick, className }: InfoProps) {
|
||||
return (
|
||||
<IconButton>
|
||||
<IconButton
|
||||
sx={sx}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 -2.62268e-07C21.3137 -1.17422e-07 24 2.68629 24 6L24 18C24 21.3137 21.3137 24 18 24L6 24C2.68629 24 -9.31652e-07 21.3137 -7.86805e-07 18L-5.24537e-07 12L-2.62268e-07 6C-1.17422e-07 2.68629 2.68629 -9.31652e-07 6 -7.86805e-07L18 -2.62268e-07Z" fill="#9A9AAF" fill-opacity="0.7"/>
|
||||
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 493 B |
@ -12,7 +12,7 @@ export default function StarIconMini({ color, width = 30, sx }: Props) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "30px",
|
||||
height: "50px",
|
||||
width: width + "px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@ -20,7 +20,7 @@ export default function StarIconMini({ color, width = 30, sx }: Props) {
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width={width} height={width} viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.551 21.8375L20.851 25.8375C21.6635 26.35 22.6635 25.5875 22.426 24.65L20.601 17.475C20.5516 17.2762 20.5595 17.0674 20.6236 16.8728C20.6877 16.6781 20.8056 16.5056 20.9635 16.375L26.6135 11.6625C27.351 11.05 26.976 9.81253 26.0135 9.75003L18.6385 9.27503C18.4372 9.26332 18.2438 9.19325 18.0817 9.07338C17.9197 8.95351 17.7961 8.78902 17.726 8.60003L14.976 1.67503C14.9032 1.47491 14.7706 1.30204 14.5961 1.17988C14.4217 1.05772 14.2139 0.992188 14.001 0.992188C13.788 0.992188 13.5802 1.05772 13.4058 1.17988C13.2314 1.30204 13.0988 1.47491 13.026 1.67503L10.276 8.60003C10.2059 8.78902 10.0823 8.95351 9.92021 9.07338C9.75816 9.19325 9.5647 9.26332 9.36347 9.27503L1.98847 9.75003C1.02597 9.81253 0.650971 11.05 1.38847 11.6625L7.03847 16.375C7.19639 16.5056 7.3142 16.6781 7.37834 16.8728C7.44247 17.0674 7.45032 17.2762 7.40097 17.475L5.71347 24.125C5.42597 25.25 6.62597 26.1625 7.58847 25.55L13.451 21.8375C13.6154 21.733 13.8062 21.6775 14.001 21.6775C14.1958 21.6775 14.3866 21.733 14.551 21.8375Z"
|
||||
fill={color}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
color: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export default function SettingIcon({ color }: Props) {
|
||||
|
64
src/assets/icons/trash.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useTheme, SxProps, Box } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
export default function Trash({ sx }: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
...sx,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.25 5.73438H3.75"
|
||||
stroke="#4D4D4D"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.75 10.2344V16.2344"
|
||||
stroke="#4D4D4D"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 10.2344V16.2344"
|
||||
stroke="#4D4D4D"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M18.75 5.73438V19.9844C18.75 20.1833 18.671 20.3741 18.5303 20.5147C18.3897 20.6554 18.1989 20.7344 18 20.7344H6C5.80109 20.7344 5.61032 20.6554 5.46967 20.5147C5.32902 20.3741 5.25 20.1833 5.25 19.9844V5.73438"
|
||||
stroke="#4D4D4D"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.75 5.73438V4.23438C15.75 3.83655 15.592 3.45502 15.3107 3.17371C15.0294 2.89241 14.6478 2.73438 14.25 2.73438H9.75C9.35218 2.73438 8.97064 2.89241 8.68934 3.17371C8.40804 3.45502 8.25 3.83655 8.25 4.23438V5.73438"
|
||||
stroke="#4D4D4D"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -18,6 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
||||
video: "",
|
||||
},
|
||||
rule: {
|
||||
children: [],
|
||||
main: [] as QuestionBranchingRuleMain[],
|
||||
parentId: "",
|
||||
default: ""
|
||||
|
@ -10,6 +10,6 @@ export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id" | "backendId"> = {
|
||||
required: false,
|
||||
innerNameCheck: false,
|
||||
innerName: "",
|
||||
type: "all",
|
||||
type: "picture",
|
||||
},
|
||||
};
|
||||
|
@ -8,19 +8,10 @@ export const QUIZ_QUESTION_RESULT: Omit<QuizQuestionResult, "id" | "backendId">
|
||||
type: "result",
|
||||
content: {
|
||||
...QUIZ_QUESTION_BASE.content,
|
||||
multi: false,
|
||||
own: false,
|
||||
innerNameCheck: false,
|
||||
video: "",
|
||||
innerName: "",
|
||||
required: false,
|
||||
variants: [
|
||||
{
|
||||
id: nanoid(),
|
||||
answer: "",
|
||||
extendedText: "",
|
||||
hints: "",
|
||||
originalImageUrl: "",
|
||||
},
|
||||
],
|
||||
text: "",
|
||||
price: [0],
|
||||
useImage: true
|
||||
},
|
||||
};
|
||||
|
@ -10,4 +10,20 @@ body {
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
@keyframes blinking {
|
||||
0% {
|
||||
opacity: 100;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 100;
|
||||
}
|
||||
}
|
||||
|
||||
.blink {
|
||||
animation: blinking 2s infinite ;
|
||||
}
|
@ -12,6 +12,8 @@ import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import lightTheme from "./utils/themes/light";
|
||||
import { SWRConfig } from "swr";
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
dayjs.locale("ru");
|
||||
@ -28,13 +30,16 @@ root.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ru" localeText={localeText}>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<SnackbarProvider
|
||||
preventDuplicate={true}
|
||||
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
||||
>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
<BrowserRouter>
|
||||
|
||||
<SnackbarProvider
|
||||
preventDuplicate={true}
|
||||
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
||||
>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</LocalizationProvider>
|
||||
</DndProvider>
|
||||
|
@ -1,29 +1,22 @@
|
||||
import type {
|
||||
QuizQuestionBase,
|
||||
QuestionVariant,
|
||||
QuestionBranchingRule,
|
||||
QuestionHint,
|
||||
PreviewRule,
|
||||
} from "./shared";
|
||||
|
||||
export interface QuizQuestionResult extends QuizQuestionBase {
|
||||
type: "result";
|
||||
content: {
|
||||
id: string;
|
||||
/** Чекбокс "Можно несколько" */
|
||||
multi: boolean;
|
||||
/** Чекбокс "Вариант "свой ответ"" */
|
||||
own: boolean;
|
||||
/** Чекбокс "Внутреннее название вопроса" */
|
||||
innerNameCheck: boolean;
|
||||
/** Поле "Внутреннее название вопроса" */
|
||||
innerName: string;
|
||||
/** Чекбокс "Необязательный вопрос" */
|
||||
required: boolean;
|
||||
variants: QuestionVariant[];
|
||||
hint: QuestionHint;
|
||||
rule: PreviewRule;
|
||||
back: string;
|
||||
originalBack: string;
|
||||
video: string;
|
||||
innerName: string;
|
||||
text: string;
|
||||
price: [number] | [number, number];
|
||||
useImage: boolean;
|
||||
rule: QuestionBranchingRule,
|
||||
hint: QuestionHint;
|
||||
autofill: boolean;
|
||||
};
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import type { QuizQuestionSelect } from "./select";
|
||||
import type { QuizQuestionText } from "./text";
|
||||
import type { QuizQuestionVariant } from "./variant";
|
||||
import type { QuizQuestionVarImg } from "./varimg";
|
||||
import type { QuizQuestionResult } from "./result";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export interface QuestionBranchingRuleMain {
|
||||
@ -22,6 +23,7 @@ export interface QuestionBranchingRuleMain {
|
||||
}
|
||||
export interface QuestionBranchingRule {
|
||||
|
||||
children: string[],
|
||||
//список условий
|
||||
main: QuestionBranchingRuleMain[];
|
||||
parentId: string | null | "root";
|
||||
@ -58,7 +60,6 @@ export interface QuizQuestionBase {
|
||||
type?: QuestionType | null;
|
||||
expanded: boolean;
|
||||
openedModalSettings: boolean;
|
||||
required: boolean;
|
||||
deleted: boolean;
|
||||
deleteTimeoutId: number;
|
||||
content: {
|
||||
@ -92,7 +93,8 @@ export type AnyTypedQuizQuestion =
|
||||
| QuizQuestionNumber
|
||||
| QuizQuestionFile
|
||||
| QuizQuestionPage
|
||||
| QuizQuestionRating;
|
||||
| QuizQuestionRating
|
||||
| QuizQuestionResult;
|
||||
|
||||
type FilterQuestionsWithVariants<T> = T extends {
|
||||
content: { variants: QuestionVariant[]; };
|
||||
|
@ -33,12 +33,19 @@ export interface QuizConfig {
|
||||
startpageType: QuizStartpageType;
|
||||
results: QuizResultsType;
|
||||
haveRoot: string | null;
|
||||
resultInfo: {
|
||||
when: 'before' | 'after' | 'email',
|
||||
share: true | false,
|
||||
replay: true | false,
|
||||
theme: string,
|
||||
reply: string,
|
||||
replname: string,
|
||||
}
|
||||
startpage: {
|
||||
description: string;
|
||||
button: string;
|
||||
position: QuizStartpageAlignType;
|
||||
favIcon: string | null;
|
||||
originalFavIcon: string | null;
|
||||
logo: string | null;
|
||||
originalLogo: string | null;
|
||||
background: {
|
||||
@ -67,12 +74,19 @@ export const defaultQuizConfig: QuizConfig = {
|
||||
startpageType: null,
|
||||
results: null,
|
||||
haveRoot: null,
|
||||
resultInfo: {
|
||||
when: 'after',
|
||||
share: false,
|
||||
replay: false,
|
||||
theme: "",
|
||||
reply: "",
|
||||
replname: "",
|
||||
},
|
||||
startpage: {
|
||||
description: "",
|
||||
button: "",
|
||||
position: "left",
|
||||
favIcon: null,
|
||||
originalFavIcon: null,
|
||||
logo: null,
|
||||
originalLogo: null,
|
||||
background: {
|
||||
|
@ -1,72 +1,87 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import SectionStyled from './SectionStyled';
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import SectionStyled from "./SectionStyled";
|
||||
import NavMenuItem from "@ui_kit/Header/NavMenuItem";
|
||||
import QuizLogo from "./images/icons/QuizLogo";
|
||||
import { useMediaQuery, useTheme } from "@mui/material";
|
||||
import { setIsContactFormOpen } from "../../stores/contactForm";
|
||||
import { useUserStore } from "@root/user";
|
||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||
|
||||
const buttonMenu = ['Меню 1', 'Меню 2', 'Меню 3', 'Меню 4', 'Меню 5', 'Меню 1', 'Меню 2']
|
||||
const buttonMenu = ["Меню 1", "Меню 2", "Меню 3", "Меню 4", "Меню 5", "Меню 1", "Меню 2"];
|
||||
|
||||
export default function Component() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
|
||||
const [select, setSelect] = React.useState(0)
|
||||
return (
|
||||
<SectionStyled tag={'header'} bg={'#F2F3F7'} mwidth={'1160px'}
|
||||
sxsect={{
|
||||
minHeight: '80px',
|
||||
borderBottom: '1px solid #E3E3E3',
|
||||
position: "relative",
|
||||
padding: isMobile ? "0 16px" : isTablet ? "0 40px" : 0,
|
||||
zIndex: 3
|
||||
}}
|
||||
sxcont={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 0
|
||||
}}>
|
||||
<QuizLogo width={isMobile ? 100 : 150} />
|
||||
{/*<Box*/}
|
||||
{/* sx={{*/}
|
||||
{/* maxWidth: '595px',*/}
|
||||
{/* width: '100%',*/}
|
||||
{/* display: 'flex',*/}
|
||||
{/* justifyContent: 'space-between',*/}
|
||||
{/* flexWrap: 'wrap',*/}
|
||||
{/* marginRight: "138px",*/}
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const [select, setSelect] = React.useState(0);
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation()
|
||||
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* {buttonMenu.map( (element, index) => (*/}
|
||||
{/* <NavMenuItem*/}
|
||||
{/* text={element}*/}
|
||||
{/* // component={Link}*/}
|
||||
{/* // to={url}*/}
|
||||
{/* key={index}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* setSelect(index);*/}
|
||||
{/* }}*/}
|
||||
{/* isActive={select === index}*/}
|
||||
{/* />*/}
|
||||
{/* ))}*/}
|
||||
{/*</Box>*/}
|
||||
<Button variant="outlined"
|
||||
onClick={() => setIsContactFormOpen(true)}
|
||||
sx={{
|
||||
color: 'black',
|
||||
border: '1px solid black',
|
||||
textTransform: 'none',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '24px',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 17px',
|
||||
}}
|
||||
>Предрегистрация</Button>
|
||||
</SectionStyled>
|
||||
)
|
||||
}
|
||||
const onClick = () => (userId ? navigate("/list") : navigate("/signin"));
|
||||
|
||||
return (
|
||||
<SectionStyled
|
||||
tag={"header"}
|
||||
bg={"#F2F3F7"}
|
||||
mwidth={"1160px"}
|
||||
sxsect={{
|
||||
minHeight: "80px",
|
||||
borderBottom: "1px solid #E3E3E3",
|
||||
position: "relative",
|
||||
padding: isMobile ? "0 16px" : isTablet ? "0 40px" : 0,
|
||||
zIndex: 3,
|
||||
}}
|
||||
sxcont={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<QuizLogo width={isMobile ? 100 : 150} />
|
||||
{/*<Box*/}
|
||||
{/* sx={{*/}
|
||||
{/* maxWidth: '595px',*/}
|
||||
{/* width: '100%',*/}
|
||||
{/* display: 'flex',*/}
|
||||
{/* justifyContent: 'space-between',*/}
|
||||
{/* flexWrap: 'wrap',*/}
|
||||
{/* marginRight: "138px",*/}
|
||||
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* {buttonMenu.map( (element, index) => (*/}
|
||||
{/* <NavMenuItem*/}
|
||||
{/* text={element}*/}
|
||||
{/* // component={Link}*/}
|
||||
{/* // to={url}*/}
|
||||
{/* key={index}*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* setSelect(index);*/}
|
||||
{/* }}*/}
|
||||
{/* isActive={select === index}*/}
|
||||
{/* />*/}
|
||||
{/* ))}*/}
|
||||
{/*</Box>*/}
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
color: "black",
|
||||
border: "1px solid black",
|
||||
textTransform: "none",
|
||||
fontWeight: "400",
|
||||
fontSize: "18px",
|
||||
lineHeight: "24px",
|
||||
borderRadius: "8px",
|
||||
padding: "8px 17px",
|
||||
}}
|
||||
>
|
||||
{userId ? "Войти" : " Регистрация / Войти"}
|
||||
</Button>
|
||||
</SectionStyled>
|
||||
);
|
||||
}
|
||||
|
@ -1,75 +1,82 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import {Typography, useMediaQuery, useTheme} from "@mui/material";
|
||||
import abstraction from '../../assets/Quiz-main.png'
|
||||
import SectionStyled from './SectionStyled';
|
||||
import { Link, redirect } from 'react-router-dom';
|
||||
import {setIsContactFormOpen} from "@root/contactForm";
|
||||
import React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import abstraction from "../../assets/Quiz-main.png";
|
||||
import SectionStyled from "./SectionStyled";
|
||||
import { Link, redirect, useNavigate } from "react-router-dom";
|
||||
import { setIsContactFormOpen } from "@root/contactForm";
|
||||
import { useUserStore } from "@root/user";
|
||||
|
||||
export default function Component() {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000))
|
||||
return(
|
||||
<SectionStyled tag={'section'} bg={'#f2f3f7'} mwidth={'1160px'}
|
||||
sxsect={{
|
||||
height: isMobile ? '702px' : (isTablet ? '986px' : '660px'),
|
||||
}}
|
||||
sxcont={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: isMobile ? "300px 16px 0 16px" : (isTablet ? "494px 40px 0 40px" : 0),
|
||||
marginBottom: isMobile ? '55px' : (isTablet ? 0 : "55px"),
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '30px',
|
||||
height: '100%',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: "flex-start",
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Typography variant='h2'>
|
||||
Pena Quiz
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: isTablet ? '715px' : '420px',
|
||||
minHeight: '64px',
|
||||
}}>
|
||||
<Typography variant='body1'>
|
||||
Помогаем посетителю оставить заявку. <br style={{display: isTablet ? 'flex' : "none"}}/> Готовые шаблоны квизов с легкой установкой на любой сайт и социальные сети
|
||||
</Typography>
|
||||
</Box>
|
||||
{/*<Link */}
|
||||
{/* to="/list"*/}
|
||||
{/* style={{textDecoration: "none"}}>*/}
|
||||
<Button variant="contained"
|
||||
onClick={() => setIsContactFormOpen(true)}
|
||||
>
|
||||
Попробуйте бесплатно
|
||||
|
||||
</Button>
|
||||
{/*</Link>*/}
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const userId = useUserStore((state) => state.userId);
|
||||
const navigate = useNavigate();
|
||||
|
||||
<Box
|
||||
component={"img"}
|
||||
src={abstraction}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: isMobile ? undefined : (isTablet? "138px" : "-291px"),
|
||||
maxWidth: isMobile ? "403px" : "810px",
|
||||
width: isMobile ? "100%" : undefined,
|
||||
left: isMobile ? "-20px" : (isTablet? "54px" : "401px"),
|
||||
top: isMobile ? "-345px" : undefined
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</SectionStyled>
|
||||
)
|
||||
}
|
||||
const tryItForFreeonClick = () => (userId ? navigate("/list") : setIsContactFormOpen(true));
|
||||
|
||||
return (
|
||||
<SectionStyled
|
||||
tag={"section"}
|
||||
bg={"#f2f3f7"}
|
||||
mwidth={"1160px"}
|
||||
sxsect={{
|
||||
height: isMobile ? "702px" : isTablet ? "986px" : "660px",
|
||||
}}
|
||||
sxcont={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: isMobile ? "300px 16px 0 16px" : isTablet ? "494px 40px 0 40px" : 0,
|
||||
marginBottom: isMobile ? "55px" : isTablet ? 0 : "55px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "30px",
|
||||
height: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2">Pena Quiz</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: isTablet ? "715px" : "420px",
|
||||
minHeight: "64px",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
Помогаем посетителю оставить заявку. <br style={{ display: isTablet ? "flex" : "none" }} /> Готовые шаблоны
|
||||
квизов с легкой установкой на любой сайт и социальные сети
|
||||
</Typography>
|
||||
</Box>
|
||||
{/*<Link */}
|
||||
{/* to="/list"*/}
|
||||
{/* style={{textDecoration: "none"}}>*/}
|
||||
<Button variant="contained" onClick={tryItForFreeonClick}>
|
||||
Попробуйте бесплатно
|
||||
</Button>
|
||||
{/*</Link>*/}
|
||||
|
||||
<Box
|
||||
component={"img"}
|
||||
src={abstraction}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: isMobile ? undefined : isTablet ? "138px" : "-291px",
|
||||
maxWidth: isMobile ? "403px" : "810px",
|
||||
width: isMobile ? "100%" : undefined,
|
||||
left: isMobile ? "-20px" : isTablet ? "54px" : "401px",
|
||||
top: isMobile ? "-345px" : undefined,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</SectionStyled>
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import Blog from './Blog';
|
||||
import HowItWorks from './HowItWorks';
|
||||
import BusinessPluses from './BusinessPluses';
|
||||
import HowToUse from './HowToUse';
|
||||
import WhatTheySay from './WhatTheySay';
|
||||
|
||||
import StartWithTemplates from './StartWithTemplates';
|
||||
import WhatTheFeatures from './WhatTheFeatures';
|
||||
import FullScreenDialog from "./headerMobileLanding";
|
||||
@ -18,13 +18,14 @@ import Collaboration from "./Collaboration";
|
||||
export default function Landing() {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<Header/>
|
||||
<Hero/>
|
||||
<Counter/>
|
||||
<Collaboration/>
|
||||
{/* <Collaboration/> */}
|
||||
<HowItWorks/>
|
||||
<BusinessPluses/>
|
||||
<HowToUse/>
|
||||
|
@ -77,7 +77,7 @@ export const AnswerItem = ({
|
||||
placeholder={"Добавьте ответ"}
|
||||
multiline={largeCheck}
|
||||
onChange={({ target }) => {
|
||||
setQuestionVariantAnswer(target.value);
|
||||
setQuestionVariantAnswer(target.value || " ");
|
||||
}}
|
||||
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.code === "Enter" && !largeCheck) {
|
||||
@ -124,7 +124,7 @@ export const AnswerItem = ({
|
||||
style={{ margin: "10px" }}
|
||||
placeholder="Подсказка для этого ответа"
|
||||
value={variant.hints}
|
||||
onChange={e => setQuestionVariantAnswer(e.target.value)}
|
||||
onChange={e => setQuestionVariantAnswer(e.target.value || " ")}
|
||||
onKeyDown={(
|
||||
event: KeyboardEvent<HTMLTextAreaElement>
|
||||
) => event.stopPropagation()}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import Cytoscape from "cytoscape";
|
||||
import { Button } from "@mui/material";
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
import popper from "cytoscape-popper";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { updateRootContentId } from "@root/quizes/actions"
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { cleardragQuestionContentId, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
|
||||
import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
|
||||
import { updateOpenedModalSettingsId, } from "@root/uiTools/actions";
|
||||
import { cleardragQuestionContentId } from "@root/uiTools/actions";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
|
||||
import { storeToNodes } from "./helper";
|
||||
@ -20,9 +23,8 @@ import type {
|
||||
AbstractEventObject,
|
||||
ElementDefinition,
|
||||
} from "cytoscape";
|
||||
import { QuestionsList } from "../SwitchBranchingPanel/QuestionsList";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
type PopperItem = {
|
||||
id: () => string;
|
||||
@ -113,16 +115,18 @@ interface Props {
|
||||
}
|
||||
|
||||
|
||||
function CsComponent ({
|
||||
function CsComponent({
|
||||
modalQuestionParentContentId,
|
||||
modalQuestionTargetContentId,
|
||||
setOpenedModalQuestions,
|
||||
setModalQuestionParentContentId,
|
||||
setModalQuestionTargetContentId
|
||||
}: Props) {
|
||||
}: Props) {
|
||||
const quiz = useCurrentQuiz();
|
||||
|
||||
const { dragQuestionContentId, questions, desireToOpenABranchingModal } = useQuestionsStore()
|
||||
const { dragQuestionContentId, desireToOpenABranchingModal } = useUiTools()
|
||||
const trashQuestions = useQuestionsStore().questions
|
||||
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
|
||||
const [startCreate, setStartCreate] = useState("");
|
||||
const [startRemove, setStartRemove] = useState("");
|
||||
|
||||
@ -144,8 +148,8 @@ function CsComponent ({
|
||||
}, [desireToOpenABranchingModal])
|
||||
useLayoutEffect(() => {
|
||||
updateOpenedModalSettingsId()
|
||||
// updateRootContentId(quiz.id, "")
|
||||
// clearRuleForAll()
|
||||
// updateRootContentId(quiz.id, "")
|
||||
// clearRuleForAll()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
|
||||
@ -156,6 +160,12 @@ function CsComponent ({
|
||||
}, [modalQuestionTargetContentId])
|
||||
|
||||
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
|
||||
|
||||
|
||||
//запрещаем работу родителя-ребенка если это один и тот же вопрос
|
||||
if (parentNodeContentId === targetNodeContentId) return
|
||||
|
||||
|
||||
const cy = cyRef?.current
|
||||
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
|
||||
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
|
||||
@ -164,11 +174,11 @@ function CsComponent ({
|
||||
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
|
||||
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
|
||||
cy?.data('changed', true)
|
||||
cy?.add([
|
||||
const es = cy?.add([
|
||||
{
|
||||
data: {
|
||||
id: targetQuestion.content.id,
|
||||
label: targetQuestion.title || "noname"
|
||||
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -179,28 +189,45 @@ function CsComponent ({
|
||||
}
|
||||
])
|
||||
cy?.layout(lyopts).run()
|
||||
cy?.fit(es, 200)
|
||||
} else {
|
||||
enqueueSnackbar("Добавляемый вопрос не найден")
|
||||
}
|
||||
}
|
||||
|
||||
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
|
||||
|
||||
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
|
||||
|
||||
|
||||
//смотрим не добавлен ли родителю result. Если да - убираем его. Веточкам result не нужен
|
||||
trashQuestions.forEach((targetQuestion) => {
|
||||
if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) {
|
||||
deleteQuestion(targetQuestion.id);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//предупреждаем добавленный вопрос о том, кто его родитель
|
||||
updateQuestion(targetQuestion.content.id, question => {
|
||||
question.content.rule.parentId = parentNodeContentId
|
||||
question.content.rule.main = []
|
||||
})
|
||||
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
|
||||
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, question => {
|
||||
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]
|
||||
})
|
||||
|
||||
|
||||
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
|
||||
if (parentNodeChildren >= 1) {
|
||||
if (parentQuestion.content.rule.children >= 1) {
|
||||
updateOpenedModalSettingsId(targetQuestion.content.id)
|
||||
} else {
|
||||
//Если ребёнок первый - добавляем его родителю как дефолтный
|
||||
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
|
||||
console.log("старт удаление")
|
||||
const deleteNodes = [] as string[]
|
||||
const deleteEdges: any = []
|
||||
const cy = cyRef?.current
|
||||
@ -230,10 +257,18 @@ function CsComponent ({
|
||||
updateQuestion(targetNodeContentId, question => {
|
||||
question.content.rule.parentId = ""
|
||||
question.content.rule.main = []
|
||||
question.content.rule.children = []
|
||||
question.content.rule.default = ""
|
||||
})
|
||||
trashQuestions.forEach(q => {
|
||||
if (q.type === "result") {
|
||||
deleteQuestion(q.id);
|
||||
}
|
||||
});
|
||||
clearRuleForAll()
|
||||
|
||||
} else {
|
||||
|
||||
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
|
||||
if (targetNodeContentId && parentQuestionContentId) {
|
||||
|
||||
@ -243,7 +278,7 @@ function CsComponent ({
|
||||
|
||||
}
|
||||
|
||||
//После всех манипуляций удаляем грани из CS и ноды из бекенда
|
||||
//После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке
|
||||
|
||||
deleteNodes.forEach((nodeId) => {//Ноды
|
||||
cy?.remove(cy?.$("#" + nodeId))
|
||||
@ -252,7 +287,10 @@ function CsComponent ({
|
||||
question.content.rule.parentId = ""
|
||||
question.content.rule.main = []
|
||||
question.content.rule.default = ""
|
||||
question.content.rule.children = []
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
|
||||
deleteEdges.forEach((edge: any) => {//Грани
|
||||
@ -262,32 +300,47 @@ function CsComponent ({
|
||||
removeButtons(targetNodeContentId)
|
||||
cy?.data('changed', true)
|
||||
cy?.layout(lyopts).run()
|
||||
|
||||
//удаляем result всех потомков
|
||||
trashQuestions.forEach((qr) => {
|
||||
if (qr.type === "result") {
|
||||
if (deleteNodes.includes(qr.content.rule.parentId) || qr.content.rule.parentId === targetQuestion.content.id) {
|
||||
deleteQuestion(qr.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
|
||||
|
||||
|
||||
|
||||
updateQuestion(targetQuestionContentId, question => {
|
||||
question.content.rule.parentId = ""
|
||||
question.content.rule.children = []
|
||||
question.content.rule.main = []
|
||||
question.content.rule.default = ""
|
||||
})
|
||||
|
||||
|
||||
//чистим rule родителя
|
||||
const parentQuestion = getQuestionByContentId(parentQuestionContentId)
|
||||
const newRule = {}
|
||||
const newChildren = [...parentQuestion.content.rule.children]
|
||||
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
|
||||
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== targetQuestionContentId) //удаляем условия перехода от родителя к этому вопросу
|
||||
newRule.parentId = parentQuestion.content.rule.parentId
|
||||
newRule.default = questions.filter((q) => {
|
||||
return q.content.rule.parentId === parentQuestionContentId && q.content.id !== targetQuestionContentId
|
||||
})[0]?.content.id || ""
|
||||
//Если этот вопрос был дефолтным у родителя - чистим дефолт
|
||||
//Смотрим можем ли мы заменить id на один из main
|
||||
newRule.default = parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default
|
||||
newRule.children = newChildren
|
||||
|
||||
console.log(newRule)
|
||||
updateQuestion(parentQuestionContentId, (PQ) => {
|
||||
PQ.content.rule = newRule
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (startCreate) {
|
||||
addNode({ parentNodeContentId: startCreate });
|
||||
@ -330,7 +383,7 @@ function CsComponent ({
|
||||
positions: (e) => {
|
||||
if (!e.cy().data('changed')) {
|
||||
return e.data('oldPos')
|
||||
} else { e.removeData('oldPos') }
|
||||
}
|
||||
const id = e.id()
|
||||
const incomming = e.cy().edges(`[target="${id}"]`)
|
||||
const layer = 0
|
||||
@ -361,7 +414,7 @@ function CsComponent ({
|
||||
while (queue.length) {
|
||||
const task = queue.pop()
|
||||
if (task.children.length === 0) {
|
||||
task.parent.data('subtreeWidth', task.parent.height())
|
||||
task.parent.data('subtreeWidth', task.parent.height() + 50)
|
||||
continue
|
||||
}
|
||||
const unprocessed = task?.children.filter(e => {
|
||||
@ -377,31 +430,30 @@ function CsComponent ({
|
||||
|
||||
task?.parent.data('subtreeWidth', task.children.reduce((p, n) => p + n.data('subtreeWidth'), 0))
|
||||
}
|
||||
|
||||
const pos = { x: 0, y: 0 }
|
||||
e.data('oldPos', pos)
|
||||
|
||||
queue.push({ task: children, parent: e })
|
||||
while (queue.length) {
|
||||
const task = queue.pop()
|
||||
const oldPos = task.parent.data('oldPos')
|
||||
let yoffset = oldPos.y - task.parent.data('subtreeWidth') / 2
|
||||
task.task.forEach(n => {
|
||||
const width = n.data('subtreeWidth')
|
||||
|
||||
n.data('oldPos', { x: 250 * n.data('layer'), y: yoffset + width / 2 })
|
||||
yoffset += width
|
||||
queue.push({ task: n.cy().edges(`[source="${n.id()}"]`).targets(), parent: n })
|
||||
})
|
||||
}
|
||||
e.cy().data('changed', false)
|
||||
return pos
|
||||
} else {
|
||||
if (e.cy().data('firstNode') !== 'root') {
|
||||
e.cy().data('firstNode', 'nonroot')
|
||||
return { x: 0, y: 0 }
|
||||
}
|
||||
if (e.cy().data('firstNode') === undefined)
|
||||
e.cy().data('firstNode', 'nonroot')
|
||||
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
|
||||
const wing = (parent.data('children') === 1) ? 0 : parent.data('subtreeWidth') / 2 + 50
|
||||
const lastOffset = parent.data('lastChild')
|
||||
const step = wing * 2 / (parent.data('children') - 1)
|
||||
//e.removeData('subtreeWidth')
|
||||
if (lastOffset !== undefined) {
|
||||
parent.data('lastChild', lastOffset + step)
|
||||
const pos = { x: 250 * e.data('layer'), y: (lastOffset + step) }
|
||||
e.data('oldPos', pos)
|
||||
return pos
|
||||
} else {
|
||||
parent.data('lastChild', parent.position().y - wing)
|
||||
const pos = { x: 250 * e.data('layer'), y: (parent.position().y - wing) }
|
||||
e.data('oldPos', pos)
|
||||
return pos
|
||||
|
||||
const opos = e.data('oldPos')
|
||||
if (opos) {
|
||||
return opos
|
||||
}
|
||||
}
|
||||
}, // map of (node id) => (position obj); or function(node){ return somPos; }
|
||||
@ -412,7 +464,7 @@ function CsComponent ({
|
||||
animate: false, // whether to transition the node positions
|
||||
animationDuration: 500, // duration of animation in ms if enabled
|
||||
animationEasing: undefined, // easing of animation if enabled
|
||||
animateFilter: function (node, i) { return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||
animateFilter: function (node, i) { return false; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||
ready: readyLO, // callback on layoutready
|
||||
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
|
||||
}
|
||||
@ -420,7 +472,7 @@ function CsComponent ({
|
||||
useEffect(() => {
|
||||
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
|
||||
const cy = cyRef.current;
|
||||
const eles = cy?.add(storeToNodes(questions))
|
||||
const eles = cy?.add(storeToNodes(questions.filter((question: AnyTypedQuizQuestion) => (question.type !== "result" && question.type !== null))))
|
||||
cy.data('changed', true)
|
||||
// cy.data('changed', true)
|
||||
const elecs = eles.layout(lyopts).run()
|
||||
@ -586,17 +638,15 @@ function CsComponent ({
|
||||
return crossElement;
|
||||
},
|
||||
});
|
||||
|
||||
const gearsPopper = node.popper({
|
||||
let gearsPopper = null
|
||||
if (node.data().root !== true) {
|
||||
gearsPopper = node.popper({
|
||||
popper: {
|
||||
placement: "left",
|
||||
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||
},
|
||||
content: ([item]) => {
|
||||
const itemId = item.id();
|
||||
if (item.cy().edges(`[target="${itemId}"]`).sources().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemElement = gearsContainer.current?.querySelector(
|
||||
`.popper-gear[data-id='${itemId}']`
|
||||
@ -611,24 +661,25 @@ function CsComponent ({
|
||||
gearElement.style.zIndex = "1"
|
||||
gearsContainer.current?.appendChild(gearElement);
|
||||
gearElement.addEventListener("mouseup", (e) => {
|
||||
console.log("up")
|
||||
updateOpenedModalSettingsId(item.id())
|
||||
});
|
||||
|
||||
return gearElement;
|
||||
},
|
||||
});
|
||||
|
||||
}
|
||||
const update = async () => {
|
||||
await plusesPopper.update();
|
||||
await crossesPopper.update();
|
||||
await gearsPopper.update();
|
||||
await gearsPopper?.update();
|
||||
await layoutsPopper.update();
|
||||
};
|
||||
|
||||
const onZoom = (event: AbstractEventObject) => {
|
||||
const zoom = event.cy.zoom();
|
||||
|
||||
update();
|
||||
//update();
|
||||
|
||||
crossesPopper.setOptions({
|
||||
modifiers: [
|
||||
@ -643,6 +694,18 @@ function CsComponent ({
|
||||
{ name: "offset", options: { offset: [0, -130 * zoom] } },
|
||||
],
|
||||
});
|
||||
plusesPopper.setOptions({
|
||||
modifiers: [
|
||||
{ name: "flip", options: { boundary: node } },
|
||||
{ name: "offset", options: { offset: [0, 0 * zoom] } },
|
||||
],
|
||||
});
|
||||
gearsPopper?.setOptions({
|
||||
modifiers: [
|
||||
{ name: "flip", options: { boundary: node } },
|
||||
{ name: "offset", options: { offset: [0, 0] } },
|
||||
],
|
||||
});
|
||||
|
||||
layoutsContainer.current
|
||||
?.querySelectorAll("#popper-layouts > .popper-layout")
|
||||
@ -672,7 +735,7 @@ function CsComponent ({
|
||||
element.style.borderRadius = `${6 * zoom}px`;
|
||||
});
|
||||
|
||||
gearsContainer.current
|
||||
gearsContainer?.current
|
||||
?.querySelectorAll("#popper-gears > .popper-gear")
|
||||
.forEach((item) => {
|
||||
const element = item as HTMLDivElement;
|
||||
@ -681,13 +744,63 @@ function CsComponent ({
|
||||
});
|
||||
};
|
||||
|
||||
node?.on("position", update);
|
||||
cy?.on("pan zoom resize render", onZoom);
|
||||
//node?.on("position", update);
|
||||
let pressed = false
|
||||
let hide = false
|
||||
cy?.on('mousedown', () => { pressed = true })
|
||||
cy?.on('mouseup', () => {
|
||||
pressed = false
|
||||
hide = false
|
||||
|
||||
|
||||
const gc = gearsContainer.current
|
||||
if (gc) gc.style.display = 'block'
|
||||
const pc = plusesContainer.current
|
||||
const xc = crossesContainer.current
|
||||
const lc = layoutsContainer.current
|
||||
if (pc) pc.style.display = 'block'
|
||||
if (xc) xc.style.display = 'block'
|
||||
if (lc) lc.style.display = 'block'
|
||||
update()
|
||||
})
|
||||
|
||||
cy?.on('mousemove', () => {
|
||||
if (pressed && !hide) {
|
||||
hide = true
|
||||
const gc = gearsContainer.current
|
||||
if (gc) gc.style.display = 'none'
|
||||
const pc = plusesContainer.current
|
||||
const xc = crossesContainer.current
|
||||
const lc = layoutsContainer.current
|
||||
if (pc) pc.style.display = 'none'
|
||||
if (xc) xc.style.display = 'none'
|
||||
if (lc) lc.style.display = 'block'
|
||||
}
|
||||
});
|
||||
|
||||
cy?.on("zoom render", onZoom);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
sx={{
|
||||
mb: "20px",
|
||||
height: "27px",
|
||||
color: "#7E2AEA",
|
||||
textDecoration: "underline",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
|
||||
cyRef.current?.fit()
|
||||
|
||||
}}
|
||||
>
|
||||
Выровнять
|
||||
</Button>
|
||||
<CytoscapeComponent
|
||||
wheelSensitivity={0.1}
|
||||
elements={[]}
|
||||
@ -698,9 +811,9 @@ function CsComponent ({
|
||||
cy={(cy) => {
|
||||
cyRef.current = cy;
|
||||
}}
|
||||
// autolock
|
||||
autoungrabify={true}
|
||||
/>
|
||||
<button onClick={() => {
|
||||
{/* <button onClick={() => {
|
||||
console.log("NODES____________________________")
|
||||
cyRef.current?.elements().forEach((ele: any) => {
|
||||
console.log(ele.data())
|
||||
@ -709,23 +822,23 @@ function CsComponent ({
|
||||
<button onClick={() => {
|
||||
console.log("ELEMENTS____________________________")
|
||||
console.log(questions)
|
||||
}}>elements</button>
|
||||
}}>elements</button> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function Clear () {
|
||||
const quiz = useCurrentQuiz();
|
||||
updateRootContentId(quiz.id, "")
|
||||
clearRuleForAll()
|
||||
return <></>
|
||||
function Clear() {
|
||||
const quiz = useCurrentQuiz();
|
||||
updateRootContentId(quiz.id, "")
|
||||
clearRuleForAll()
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default withErrorBoundary(CsComponent, {
|
||||
fallback: <Clear/>,
|
||||
fallback: <Clear />,
|
||||
onError: (error, info) => {
|
||||
enqueueSnackbar("Дерево порвалось")
|
||||
console.log(info)
|
||||
console.log(error)
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { Box } from "@mui/material"
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
|
||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
||||
import { deleteQuestion, clearRuleForAll, updateQuestion } from "@root/questions/actions"
|
||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions"
|
||||
import { updateRootContentId } from "@root/quizes/actions"
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
||||
import { useQuestionsStore } from "@root/questions/store"
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
interface Props {
|
||||
setOpenedModalQuestions: (open: boolean) => void;
|
||||
@ -12,7 +14,18 @@ interface Props {
|
||||
}
|
||||
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
|
||||
const quiz = useCurrentQuiz();
|
||||
const { dragQuestionContentId, questions } = useQuestionsStore()
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateOpenedModalSettingsId()
|
||||
console.log("first render firstComponent")
|
||||
updateRootContentId(quiz.id, "")
|
||||
clearRuleForAll()
|
||||
}, [])
|
||||
|
||||
|
||||
const { questions } = useQuestionsStore()
|
||||
const { dragQuestionContentId } = useUiTools()
|
||||
const Container = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const modalOpen = () => setOpenedModalQuestions(true)
|
||||
@ -22,6 +35,11 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
|
||||
if (dragQuestionContentId) {
|
||||
updateRootContentId(quiz?.id, dragQuestionContentId)
|
||||
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
|
||||
//если были результаты - удалить
|
||||
questions.forEach((q) => {
|
||||
if (q.type === 'result') deleteQuestion(q.id)
|
||||
})
|
||||
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
||||
@ -44,6 +62,10 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
|
||||
if (modalQuestionTargetContentId) {
|
||||
updateRootContentId(quiz?.id, modalQuestionTargetContentId)
|
||||
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
|
||||
//если были результаты - удалить
|
||||
questions.forEach((q) => {
|
||||
if (q.type === 'result') deleteQuestion(q.id)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar("Нет информации о взятом опроснике")
|
||||
|
@ -21,7 +21,7 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
||||
if (question.content.rule.parentId) {
|
||||
nodes.push({data: {
|
||||
id: question.content.id,
|
||||
label: question.title ? question.title : "noname"
|
||||
label: question.title === "" || question.title === " " ? "noname" : question.title
|
||||
}})
|
||||
// nodes.push({
|
||||
// data: {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { FirstNodeField } from "./FirstNodeField";
|
||||
import CsComponent from "./CsComponent";
|
||||
import { useQuestionsStore } from "@root/questions/store"
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { useState } from "react";
|
||||
import {BranchingQuestionsModal} from "../BranchingQuestionsModal"
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
export const BranchingMap = () => {
|
||||
const quiz = useCurrentQuiz();
|
||||
const { dragQuestionContentId } = useQuestionsStore()
|
||||
const { dragQuestionContentId } = useUiTools()
|
||||
const [modalQuestionParentContentId, setModalQuestionParentContentId] = useState<string>("")
|
||||
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
|
||||
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
|
||||
@ -26,7 +26,7 @@ export const BranchingMap = () => {
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||
marginBottom: "40px",
|
||||
height: "521px",
|
||||
height: "568px",
|
||||
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed"
|
||||
}}
|
||||
>
|
||||
|
@ -22,15 +22,16 @@ import InfoIcon from "@icons/Info";
|
||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||
|
||||
import { TypeSwitch, BlockRule } from "./Settings";
|
||||
import { getQuestionById, getQuestionByContentId, updateOpenedModalSettingsId, updateQuestion } from "@root/questions/actions";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { getQuestionById, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
|
||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
|
||||
export default function BranchingQuestions() {
|
||||
const theme = useTheme();
|
||||
|
||||
const { openedModalSettingsId } = useQuestionsStore();
|
||||
const { openedModalSettingsId } = useUiTools();
|
||||
const [targetQuestion, setTargetQuestion] = useState<AnyTypedQuizQuestion | null>(getQuestionById(openedModalSettingsId) || getQuestionByContentId(openedModalSettingsId))
|
||||
const [parentQuestion, setParentQuestion] = useState<AnyTypedQuizQuestion | null>(getQuestionByContentId(targetQuestion?.content.rule.parentId))
|
||||
|
||||
@ -165,7 +166,7 @@ export default function BranchingQuestions() {
|
||||
|
||||
onClick={() => {
|
||||
let mutate = JSON.parse(JSON.stringify(parentQuestion))
|
||||
mutate.content.rule.default = targetQuestion.id
|
||||
mutate.content.rule.default = parentQuestion.content.rule.default === targetQuestion.content.id ? "" : targetQuestion.content.id
|
||||
setParentQuestion(mutate)
|
||||
}}
|
||||
/>} label="Следующий вопрос по-умолчанию" />
|
||||
|
@ -6,15 +6,17 @@ import { useState, useRef, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { updateQuestion, getQuestionById } from "@root/questions/actions";
|
||||
import { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import * as dayjs from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
|
||||
import InfoIcon from "@icons/Info";
|
||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||
|
||||
import type { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared"
|
||||
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"
|
||||
|
||||
const CONDITIONS = [
|
||||
"Все условия обязательны",
|
||||
"Обязательно хотя бы одно условие",
|
||||
@ -289,12 +291,12 @@ const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQu
|
||||
</Typography>
|
||||
}
|
||||
<DatePicker
|
||||
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||
defaultValue={dayjs(new Date(parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]).toLocaleDateString())}
|
||||
onChange={(dateString) => {
|
||||
const date = dateString?.$d?.toLocaleDateString("ru-RU", { year: "numeric", month: "2-digit", day: "2-digit" });
|
||||
const date = dateString?.toDate().toLocaleDateString("ru-RU", { year: "numeric", month: "2-digit", day: "2-digit" });
|
||||
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
|
||||
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [date]
|
||||
// setParentQuestion(newParentQuestion)
|
||||
setParentQuestion(newParentQuestion)
|
||||
}}
|
||||
slots={{
|
||||
openPickerIcon: () => <CalendarIcon />,
|
||||
@ -514,47 +516,34 @@ const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParent
|
||||
(Укажите один или несколько вариантов)
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
placeholder="от"
|
||||
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||
|
||||
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
|
||||
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
|
||||
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = 0
|
||||
setParentQuestion(newParentQuestion)
|
||||
|
||||
}}
|
||||
/>
|
||||
{parentQuestion.content.chooseRange &&
|
||||
<Box>
|
||||
<TextField
|
||||
placeholder="до"
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]}
|
||||
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||
|
||||
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
|
||||
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
|
||||
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = 0
|
||||
sx={{ marginTop: "20px", width: "100%" }}
|
||||
placeholder="от"
|
||||
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[0]}
|
||||
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
|
||||
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
|
||||
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] =(parentQuestion as QuizQuestionNumber).content.chooseRange ? previousValue ? `${target.value}—${previousValue.split("—")[1] || 0}` : `${target.value}—0` : target.value;
|
||||
setParentQuestion(newParentQuestion)
|
||||
|
||||
}}
|
||||
/>
|
||||
{(parentQuestion as QuizQuestionNumber).content.chooseRange &&
|
||||
<TextField
|
||||
placeholder="до"
|
||||
sx={{ marginTop: "20px", width: "100%" }}
|
||||
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[1]}
|
||||
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
|
||||
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
|
||||
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = previousValue ? `${previousValue.split("—")[0] || 0}—${target.value}` : `0—${target.value}`
|
||||
setParentQuestion(newParentQuestion)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
</Box>
|
||||
</Box >
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import {Box, Typography, Switch, useTheme, Button, useMediaQuery, SxProps, Theme} from "@mui/material";
|
||||
|
||||
import { QuestionsList } from "./QuestionsList";
|
||||
import { updateOpenBranchingPanel } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
|
||||
import {useQuestionsStore} from "@root/questions/store";
|
||||
import {useRef} from "react";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
export const BranchingPanel = (sx?: SxProps<Theme>) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||
const {openBranchingPanel} = useQuestionsStore.getState()
|
||||
const {openBranchingPanel} = useUiTools()
|
||||
const ref = useRef()
|
||||
return (
|
||||
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
|
||||
@ -26,10 +27,10 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={openBranchingPanel}
|
||||
onChange={(_, value) => {
|
||||
updateOpenBranchingPanel(value)
|
||||
}}
|
||||
checked={openBranchingPanel}
|
||||
onChange={
|
||||
(e) => updateOpenBranchingPanel(e.target.checked)
|
||||
}
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 30,
|
||||
@ -76,7 +77,7 @@ export const BranchingPanel = (sx?: SxProps<Theme>) => {
|
||||
/>
|
||||
<Box>
|
||||
<Typography ref={ref} sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
|
||||
Логика ветвления
|
||||
Логика ветвления
|
||||
</Typography>
|
||||
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
|
||||
Настройте связи между вопросами
|
||||
|
@ -13,18 +13,22 @@ export const BranchingQuestionsModal = ({
|
||||
openedModalQuestions,
|
||||
setOpenedModalQuestions,
|
||||
setModalQuestionTargetContentId,
|
||||
setModalQuestionParentContentId
|
||||
setModalQuestionParentContentId,
|
||||
}: Props) => {
|
||||
const { questions } = useQuestionsStore();
|
||||
const trashQuestions = useQuestionsStore().questions;
|
||||
const questions = trashQuestions.filter(
|
||||
(question) => question.type !== "result"
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpenedModalQuestions(false);
|
||||
};
|
||||
|
||||
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
|
||||
(question) => question.type && !question.content.rule.parentId
|
||||
(question) => question.type && !question.content.rule.parentId && question.type !== "result"
|
||||
) as AnyTypedQuizQuestion[];
|
||||
|
||||
if (typedQuestions.length === 0) return <></>
|
||||
return (
|
||||
<Modal open={openedModalQuestions} onClose={handleClose}>
|
||||
<Box
|
||||
@ -61,7 +65,7 @@ export const BranchingQuestionsModal = ({
|
||||
borderRadius: "8px",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' strokeWidth='2' stroke-dasharray='8 8' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e");
|
||||
border-radius: 8px;`,
|
||||
"&:last-child": { marginBottom: 0 },
|
||||
}}
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { copyQuestion, deleteQuestion, updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/questions/actions";
|
||||
import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout, clearRuleForAll, updateQuestion, getQuestionByContentId } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal, } from "@root/uiTools/actions";
|
||||
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
|
||||
import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
|
||||
import Branching from "../../assets/icons/questionsPage/branching";
|
||||
@ -22,7 +23,9 @@ import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
|
||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||||
import { updateRootContentId } from "@root/quizes/actions";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
interface Props {
|
||||
switchState: string;
|
||||
@ -40,8 +43,13 @@ export default function ButtonsOptions({
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
|
||||
const quiz = useCurrentQuiz();
|
||||
const { questions } = useQuestionsStore.getState();
|
||||
|
||||
const openedModal = () => {
|
||||
updateOpenBranchingPanel(true);
|
||||
updateDesireToOpenABranchingModal(question.content.id);
|
||||
};
|
||||
|
||||
const { openBranchingPanel } = useQuestionsStore.getState()
|
||||
|
||||
const buttonSetting: {
|
||||
icon: JSX.Element;
|
||||
@ -60,15 +68,15 @@ export default function ButtonsOptions({
|
||||
title: "Настройки",
|
||||
value: "setting",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<Clue
|
||||
color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
|
||||
/>
|
||||
),
|
||||
title: "Подсказка",
|
||||
value: "help",
|
||||
},
|
||||
// {
|
||||
// icon: (
|
||||
// <Clue
|
||||
// color={switchState === "help" ? "#ffffff" : theme.palette.grey3.main}
|
||||
// />
|
||||
// ),
|
||||
// title: "Подсказка",
|
||||
// value: "help",
|
||||
// },
|
||||
{
|
||||
icon: (
|
||||
<Branching
|
||||
@ -80,8 +88,9 @@ export default function ButtonsOptions({
|
||||
title: "Ветвление",
|
||||
value: "branching",
|
||||
myFunc: (question) => {
|
||||
updateOpenBranchingPanel(true)
|
||||
updateDesireToOpenABranchingModal(question.content.id)
|
||||
console.log("buttons opiums")
|
||||
updateOpenBranchingPanel(true);
|
||||
updateDesireToOpenABranchingModal(question.content.id);
|
||||
}
|
||||
},
|
||||
];
|
||||
@ -157,8 +166,9 @@ export default function ButtonsOptions({
|
||||
<MiniButtonSetting
|
||||
key={title}
|
||||
onClick={() => {
|
||||
SSHC(value);
|
||||
myFunc(question);
|
||||
openedModal();
|
||||
// SSHC(value);
|
||||
// myFunc(question);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
@ -264,24 +274,67 @@ export default function ButtonsOptions({
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||
onClick={() => { // TODO
|
||||
// const removedId = question.id;
|
||||
// if (question.deleteTimeoutId) {
|
||||
// clearTimeout(question.deleteTimeoutId);
|
||||
// }
|
||||
onClick={() => {
|
||||
const deleteFn = () => {
|
||||
if (question.type !== null) {
|
||||
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
|
||||
updateRootContentId(quiz.id, "");
|
||||
clearRuleForAll();
|
||||
questions.forEach(q => {
|
||||
if (q.type === "result") {
|
||||
deleteQuestion(q.id);
|
||||
}
|
||||
});
|
||||
deleteQuestion(question.id);
|
||||
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
|
||||
const clearQuestions = [] as string[];
|
||||
|
||||
// removeQuestion(quizId, totalIndex);
|
||||
//записываем потомков , а их результаты удаляем
|
||||
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
|
||||
questions.forEach((targetQuestion) => {
|
||||
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
|
||||
if (targetQuestion.type === "result") {
|
||||
deleteQuestion(targetQuestion.id);
|
||||
} else {
|
||||
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
|
||||
getChildren(targetQuestion); //и ищем его потомков
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
getChildren(question);
|
||||
//чистим потомков от инфы ветвления
|
||||
clearQuestions.forEach((id) => {
|
||||
updateQuestion(id, question => {
|
||||
question.content.rule.parentId = "";
|
||||
question.content.rule.main = [];
|
||||
question.content.rule.default = "";
|
||||
});
|
||||
});
|
||||
|
||||
// const newTimeoutId = window.setTimeout(() => {
|
||||
// removeQuestionForce(quizId, removedId);
|
||||
// }, 5000);
|
||||
//чистим rule родителя
|
||||
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
|
||||
const newRule = {};
|
||||
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
|
||||
newRule.parentId = parentQuestion.content.rule.parentId;
|
||||
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
|
||||
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
|
||||
|
||||
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
||||
// ...question,
|
||||
// deleteTimeoutId: newTimeoutId,
|
||||
// });
|
||||
updateQuestion(question.content.rule.parentId, (PQ) => {
|
||||
PQ.content.rule = newRule;
|
||||
});
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
|
||||
deleteQuestion(question.id, quiz.id);
|
||||
deleteQuestion(question.id);
|
||||
|
||||
} else {
|
||||
console.log("удаляю безтипогово");
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
};
|
||||
|
||||
deleteQuestionWithTimeout(question.id, deleteFn);
|
||||
}}
|
||||
data-cy="delete-question"
|
||||
>
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { copyQuestion, deleteQuestion, updateQuestion } from "@root/questions/actions";
|
||||
import { copyQuestion, deleteQuestion, updateQuestion, clearRuleForAll, getQuestionByContentId, deleteQuestionWithTimeout } from "@root/questions/actions";
|
||||
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
|
||||
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
|
||||
import { useEffect, useState } from "react";
|
||||
@ -23,10 +23,11 @@ import ImgIcon from "../../assets/icons/questionsPage/imgIcon";
|
||||
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
|
||||
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||
import { updateOpenedModalSettingsId } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { updateRootContentId } from "@root/quizes/actions";
|
||||
|
||||
|
||||
interface Props {
|
||||
@ -46,7 +47,7 @@ export default function ButtonsOptionsAndPict({
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
|
||||
const { openBranchingPanel } = useQuestionsStore.getState()
|
||||
const { questions } = useQuestionsStore.getState();
|
||||
const quiz = useCurrentQuiz();
|
||||
|
||||
useEffect(() => {
|
||||
@ -190,8 +191,9 @@ export default function ButtonsOptionsAndPict({
|
||||
onMouseEnter={() => setButtonHover("branching")}
|
||||
onMouseLeave={() => setButtonHover("")}
|
||||
onClick={() => {
|
||||
updateOpenBranchingPanel(true)
|
||||
updateDesireToOpenABranchingModal(question.content.id)
|
||||
console.log("buttonsOptions")
|
||||
updateOpenBranchingPanel(true);
|
||||
updateDesireToOpenABranchingModal(question.content.id);
|
||||
}}
|
||||
sx={{
|
||||
height: "30px",
|
||||
@ -306,24 +308,67 @@ export default function ButtonsOptionsAndPict({
|
||||
</IconButton>
|
||||
<IconButton
|
||||
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||
onClick={() => { // TODO
|
||||
// const removedId = question.id;
|
||||
// if (question.deleteTimeoutId) {
|
||||
// clearTimeout(question.deleteTimeoutId);
|
||||
// }
|
||||
onClick={() => {
|
||||
const deleteFn = () => {
|
||||
if (question.type !== null) {
|
||||
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
|
||||
updateRootContentId(quiz.id, "");
|
||||
clearRuleForAll();
|
||||
questions.forEach(q => {
|
||||
if (q.type === "result") {
|
||||
deleteQuestion(q.id);
|
||||
}
|
||||
});
|
||||
deleteQuestion(question.id);
|
||||
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
|
||||
const clearQuestions = [] as string[];
|
||||
|
||||
// removeQuestion(quizId, totalIndex);
|
||||
//записываем потомков , а их результаты удаляем
|
||||
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
|
||||
questions.forEach((targetQuestion) => {
|
||||
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
|
||||
if (targetQuestion.type === "result") {
|
||||
deleteQuestion(targetQuestion.id);
|
||||
} else {
|
||||
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
|
||||
getChildren(targetQuestion); //и ищем его потомков
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
getChildren(question);
|
||||
//чистим потомков от инфы ветвления
|
||||
clearQuestions.forEach((id) => {
|
||||
updateQuestion(id, question => {
|
||||
question.content.rule.parentId = "";
|
||||
question.content.rule.main = [];
|
||||
question.content.rule.default = "";
|
||||
});
|
||||
});
|
||||
|
||||
// const newTimeoutId = window.setTimeout(() => {
|
||||
// removeQuestionForce(quizId, removedId);
|
||||
// }, 5000);
|
||||
//чистим rule родителя
|
||||
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
|
||||
const newRule = {};
|
||||
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
|
||||
newRule.parentId = parentQuestion.content.rule.parentId;
|
||||
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
|
||||
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
|
||||
|
||||
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
||||
// ...question,
|
||||
// deleteTimeoutId: newTimeoutId,
|
||||
// });
|
||||
updateQuestion(question.content.rule.parentId, (PQ) => {
|
||||
PQ.content.rule = newRule;
|
||||
});
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
|
||||
deleteQuestion(question.id, quiz?.id);
|
||||
deleteQuestion(question.id);
|
||||
|
||||
} else {
|
||||
console.log("удаляю безтипогово");
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
};
|
||||
|
||||
deleteQuestionWithTimeout(question.id, deleteFn);
|
||||
}}
|
||||
data-cy="delete-question"
|
||||
>
|
||||
|
@ -29,7 +29,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
|
||||
flexDirection: isWrappColumn ? "column" : null,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
pt: "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
@ -67,7 +67,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "0px" : "20px",
|
||||
@ -86,10 +86,10 @@ export default function SettingsData({ question }: SettingsDataProps) {
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.required = !target.checked;
|
||||
updateQuestion<QuizQuestionDate>(question.id, question => {
|
||||
question.content.required = !target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@ -109,7 +109,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionDate>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||
});
|
||||
@ -125,7 +125,7 @@ export default function SettingsData({ question }: SettingsDataProps) {
|
||||
<CustomTextField
|
||||
placeholder={"Развёрнутое описание вопроса"}
|
||||
text={question.content.innerName}
|
||||
onChange={({ target }) => setInnerName(target.value)}
|
||||
onChange={({ target }) => setInnerName(target.value || " ")}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -3,8 +3,10 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
|
||||
import { memo, useEffect } from "react";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import QuestionsPageCard from "./QuestionPageCard";
|
||||
import { updateEditSomeQuestion } from "@root/questions/actions"
|
||||
import { useQuestionsStore } from "@root/questions/store"
|
||||
import { cancelQuestionDeletion } from "@root/questions/actions";
|
||||
import { updateEditSomeQuestion } from "@root/uiTools/actions";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
type Props = {
|
||||
@ -15,23 +17,21 @@ type Props = {
|
||||
|
||||
function DraggableListItem({ question, isDragging, index }: Props) {
|
||||
const theme = useTheme();
|
||||
const { editSomeQuestion } = useQuestionsStore()
|
||||
const { editSomeQuestion } = useUiTools();
|
||||
|
||||
useEffect(() => {
|
||||
if (editSomeQuestion !== null) {
|
||||
const setI = setInterval(() => {
|
||||
let comp = document.getElementById(editSomeQuestion)
|
||||
console.log(comp)
|
||||
if(comp !== null) {
|
||||
clearInterval(setI)
|
||||
comp.scrollIntoView({behavior: 'instant'})
|
||||
updateEditSomeQuestion()
|
||||
}
|
||||
}, 200)
|
||||
let comp = document.getElementById(editSomeQuestion);
|
||||
if (comp !== null) {
|
||||
clearInterval(setI);
|
||||
comp.scrollIntoView({ behavior: 'instant' });
|
||||
updateEditSomeQuestion();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
}
|
||||
console.log(editSomeQuestion)
|
||||
}, [editSomeQuestion])
|
||||
}, [editSomeQuestion]);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={question.id.toString()} index={index}>
|
||||
@ -62,11 +62,8 @@ function DraggableListItem({ question, isDragging, index }: Props) {
|
||||
Вопрос удалён.
|
||||
</Typography>
|
||||
<Typography
|
||||
onClick={() => { // TODO
|
||||
// updateQuestionsList<QuizQuestionBase>(quizId, index, {
|
||||
// ...questionData,
|
||||
// deleted: false,
|
||||
// });
|
||||
onClick={() => {
|
||||
cancelQuestionDeletion(question.id);
|
||||
}}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
|
@ -29,7 +29,8 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { copyQuestion, createUntypedQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
|
||||
import { copyQuestion, createUntypedQuestion, deleteQuestion, clearRuleForAll, toggleExpandQuestion, updateQuestion, updateUntypedQuestion, getQuestionByContentId, deleteQuestionWithTimeout } from "@root/questions/actions";
|
||||
import { updateRootContentId } from "@root/quizes/actions";
|
||||
import { useRef, useState } from "react";
|
||||
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
@ -39,7 +40,8 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
||||
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
||||
import TypeQuestions from "../TypeQuestions";
|
||||
import { QuestionType } from "@model/question/question";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
|
||||
interface Props {
|
||||
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
|
||||
@ -49,6 +51,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
|
||||
const { questions } = useQuestionsStore();
|
||||
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
@ -68,7 +71,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
||||
return (
|
||||
<>
|
||||
<Paper
|
||||
id={question.id}
|
||||
id={question.id}
|
||||
data-cy="quiz-question-card"
|
||||
sx={{
|
||||
maxWidth: "796px",
|
||||
@ -99,7 +102,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
||||
<TextField
|
||||
defaultValue={question.title}
|
||||
placeholder={"Заголовок вопроса"}
|
||||
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value)}
|
||||
onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value || " ")}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<Box>
|
||||
@ -238,24 +241,67 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
||||
padding: "0",
|
||||
margin: "0 5px 0 10px",
|
||||
}}
|
||||
onClick={() => { // TODO
|
||||
const removedId = question.id;
|
||||
// if (question.deleteTimeoutId) {
|
||||
// clearTimeout(question.deleteTimeoutId);
|
||||
// }
|
||||
onClick={() => {
|
||||
const deleteFn = () => {
|
||||
if (question.type !== null) {
|
||||
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
|
||||
updateRootContentId(quiz.id, "");
|
||||
clearRuleForAll();
|
||||
deleteQuestion(question.id);
|
||||
questions.forEach(q => {
|
||||
if (q.type === "result") {
|
||||
deleteQuestion(q.id);
|
||||
}
|
||||
});
|
||||
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
|
||||
const clearQuestions = [] as string[];
|
||||
|
||||
// removeQuestion(quizId, totalIndex);
|
||||
//записываем потомков , а их результаты удаляем
|
||||
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
|
||||
questions.forEach((targetQuestion) => {
|
||||
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
|
||||
if (targetQuestion.type === "result") {
|
||||
deleteQuestion(targetQuestion.id);
|
||||
} else {
|
||||
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
|
||||
getChildren(targetQuestion); //и ищем его потомков
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
getChildren(question);
|
||||
//чистим потомков от инфы ветвления
|
||||
clearQuestions.forEach((id) => {
|
||||
updateQuestion(id, question => {
|
||||
question.content.rule.parentId = "";
|
||||
question.content.rule.main = [];
|
||||
question.content.rule.default = "";
|
||||
});
|
||||
});
|
||||
|
||||
// const newTimeoutId = window.setTimeout(() => {
|
||||
// removeQuestionForce(quizId, removedId);
|
||||
// }, 5000);
|
||||
//чистим rule родителя
|
||||
const parentQuestion = getQuestionByContentId(question.content.rule.parentId);
|
||||
const newRule = {};
|
||||
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id); //удаляем условия перехода от родителя к этому вопросу
|
||||
newRule.parentId = parentQuestion.content.rule.parentId;
|
||||
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId;
|
||||
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
|
||||
|
||||
// updateQuestionsList<AnyTypedQuizQuestion>(quizId, totalIndex, {
|
||||
// ...question,
|
||||
// deleteTimeoutId: newTimeoutId,
|
||||
// });
|
||||
updateQuestion(question.content.rule.parentId, (PQ) => {
|
||||
PQ.content.rule = newRule;
|
||||
});
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
|
||||
deleteQuestion(question.id, quiz.id);
|
||||
|
||||
deleteQuestion(question.id);
|
||||
} else {
|
||||
console.log("удаляю безтипогово");
|
||||
deleteQuestion(question.id);
|
||||
}
|
||||
};
|
||||
|
||||
deleteQuestionWithTimeout(question.id, deleteFn);
|
||||
}}
|
||||
data-cy="delete-question"
|
||||
>
|
||||
@ -265,26 +311,28 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
marginLeft: "3px",
|
||||
borderRadius: "50%",
|
||||
fontSize: "16px",
|
||||
color: question.expanded
|
||||
? theme.palette.brightPurple.main
|
||||
: "#FFF",
|
||||
background: question.expanded
|
||||
? "#EEE4FC"
|
||||
: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</Box>
|
||||
{question.type !== null &&
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
marginLeft: "3px",
|
||||
borderRadius: "50%",
|
||||
fontSize: "16px",
|
||||
color: question.expanded
|
||||
? theme.palette.brightPurple.main
|
||||
: "#FFF",
|
||||
background: question.expanded
|
||||
? "#EEE4FC"
|
||||
: theme.palette.brightPurple.main,
|
||||
}}
|
||||
>
|
||||
{question.page + 1}
|
||||
</Box>
|
||||
}
|
||||
<IconButton
|
||||
disableRipple
|
||||
sx={{
|
||||
@ -327,7 +375,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={() => createUntypedQuestion(question.quizId)}
|
||||
onClick={() => createUntypedQuestion(question.quizId, question.id)}
|
||||
sx={{
|
||||
display: plusVisible && !isDragging ? "flex" : "none",
|
||||
width: "100%",
|
||||
|
@ -1,26 +1,24 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { reorderQuestions } from "@root/questions/actions";
|
||||
import { useQuestions } from "@root/questions/hooks";
|
||||
import type { DropResult } from "react-beautiful-dnd";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import DraggableListItem from "./DraggableListItem";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
|
||||
|
||||
export const DraggableList = () => {
|
||||
const { questions, isLoading } = useQuestions();
|
||||
|
||||
const { questions } = useQuestionsStore()
|
||||
const filteredQuestions = questions.filter((question) => question.type !== "result")
|
||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||
if (destination) reorderQuestions(source.index, destination.index);
|
||||
};
|
||||
|
||||
if (isLoading && !questions) return <Box>Загрузка вопросов...</Box>;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable-list">
|
||||
{(provided, snapshot) => (
|
||||
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{questions.map((question, index) => (
|
||||
{filteredQuestions.map((question, index) => (
|
||||
<DraggableListItem
|
||||
key={question.id}
|
||||
question={question}
|
||||
|
@ -58,7 +58,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
|
||||
maxWidth: isFigmaTablte ? "297px" : "360px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
{/* <Typography
|
||||
sx={{
|
||||
height: isMobile ? "18px" : "auto",
|
||||
fontWeight: "500",
|
||||
@ -79,7 +79,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
|
||||
question.content.multi = target.checked;
|
||||
})
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<Box
|
||||
sx={{
|
||||
display: isMobile ? "none" : "block",
|
||||
@ -128,10 +128,10 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={(e) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.required = !e.target.checked;
|
||||
updateQuestion<QuizQuestionSelect>(question.id, question => {
|
||||
question.content.required = !e.target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@ -141,7 +141,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionSelect>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||
});
|
||||
@ -184,7 +184,7 @@ export default function SettingDropDown({ question }: SettingDropDownProps) {
|
||||
<CustomTextField
|
||||
placeholder={"Развёрнутое описание вопроса"}
|
||||
text={question.content.innerName}
|
||||
onChange={({ target }) => debounced(target.value)}
|
||||
onChange={({ target }) => debounced(target.value || " ")}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -30,7 +30,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
|
||||
flexDirection: isWrappColumn ? "column" : "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
@ -66,12 +66,12 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
|
||||
question.content.own = target.checked;
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "0px" : "20px",
|
||||
pb: "20px",
|
||||
pl: isTablet ? "20px" : "",
|
||||
pl: "20px",
|
||||
pr: isFigmaTablte ? "30px" : "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
@ -85,11 +85,11 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
handleChange={(e) => updateQuestion(question.id, question => {
|
||||
checked={!question.content.required}
|
||||
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
|
||||
if (question.type !== "emoji") return;
|
||||
|
||||
question.content.required = !e.target.checked;
|
||||
question.content.required = !target.checked;
|
||||
})}
|
||||
/>
|
||||
<Box
|
||||
@ -107,7 +107,7 @@ export default function SettingEmoji({ question }: SettingEmojiProps) {
|
||||
}}
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => updateQuestion(question.id, question => {
|
||||
handleChange={({ target }) => updateQuestion<QuizQuestionEmoji>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||
})}
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { reorderQuestions } from "@root/questions/actions";
|
||||
import { useQuestions } from "@root/questions/hooks";
|
||||
import type { DropResult } from "react-beautiful-dnd";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import FormDraggableListItem from "./FormDraggableListItem";
|
||||
import { useQuestions } from "@root/questions/hooks";
|
||||
|
||||
|
||||
export const FormDraggableList = () => {
|
||||
const { questions } = useQuestions();
|
||||
|
||||
const { questions } = useQuestions()
|
||||
|
||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
||||
if (destination) reorderQuestions(source.index, destination.index);
|
||||
};
|
||||
@ -18,7 +19,7 @@ export const FormDraggableList = () => {
|
||||
<Droppable droppableId="droppable-list">
|
||||
{(provided) => (
|
||||
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{questions.map((question, index) => (
|
||||
{questions?.map((question, index) => (
|
||||
<FormDraggableListItem
|
||||
key={question.id}
|
||||
question={question}
|
||||
|
@ -18,74 +18,74 @@ import type {
|
||||
|
||||
|
||||
type ButtonTypeQuestion = {
|
||||
icon: JSX.Element;
|
||||
title: string;
|
||||
value: QuestionType;
|
||||
icon: JSX.Element;
|
||||
title: string;
|
||||
value: QuestionType;
|
||||
};
|
||||
|
||||
const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [
|
||||
{
|
||||
icon: <Answer color="#9A9AAF" />,
|
||||
title: "Варианты ответов",
|
||||
value: "variant",
|
||||
},
|
||||
{
|
||||
icon: <Input color="#9A9AAF" />,
|
||||
title: "Своё поле для ввода",
|
||||
value: "text",
|
||||
},
|
||||
{
|
||||
icon: <DropDown color="#9A9AAF" />,
|
||||
title: "Выпадающий список",
|
||||
value: "select",
|
||||
},
|
||||
{
|
||||
icon: <Date color="#9A9AAF" />,
|
||||
title: "Дата",
|
||||
value: "date",
|
||||
},
|
||||
{
|
||||
icon: <Slider color="#9A9AAF" />,
|
||||
title: "Ползунок",
|
||||
value: "number",
|
||||
},
|
||||
{
|
||||
icon: <Download color="#9A9AAF" />,
|
||||
title: "Загрузка файла",
|
||||
value: "file",
|
||||
},
|
||||
{
|
||||
icon: <Answer color="#9A9AAF" />,
|
||||
title: "Варианты ответов",
|
||||
value: "variant",
|
||||
},
|
||||
{
|
||||
icon: <Input color="#9A9AAF" />,
|
||||
title: "Своё поле для ввода",
|
||||
value: "text",
|
||||
},
|
||||
{
|
||||
icon: <DropDown color="#9A9AAF" />,
|
||||
title: "Выпадающий список",
|
||||
value: "select",
|
||||
},
|
||||
{
|
||||
icon: <Date color="#9A9AAF" />,
|
||||
title: "Дата",
|
||||
value: "date",
|
||||
},
|
||||
{
|
||||
icon: <Slider color="#9A9AAF" />,
|
||||
title: "Ползунок",
|
||||
value: "number",
|
||||
},
|
||||
{
|
||||
icon: <Download color="#9A9AAF" />,
|
||||
title: "Загрузка файла",
|
||||
value: "file",
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
question: UntypedQuizQuestion;
|
||||
question: UntypedQuizQuestion;
|
||||
}
|
||||
|
||||
export default function FormTypeQuestions({ question }: Props) {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "20px",
|
||||
margin: "20px",
|
||||
}}
|
||||
>
|
||||
{(true /* TODO только первый вопрос */
|
||||
? BUTTON_TYPE_QUESTIONS
|
||||
: BUTTON_TYPE_SHORT_QUESTIONS
|
||||
).map(({ icon, title, value: questionType }) => (
|
||||
<QuestionsMiniButton
|
||||
key={title}
|
||||
onClick={() => {
|
||||
createTypedQuestion(question.id, questionType);
|
||||
}}
|
||||
icon={icon}
|
||||
text={title}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "20px",
|
||||
margin: "20px",
|
||||
}}
|
||||
>
|
||||
{(("page" in question) && question.page === 0
|
||||
? BUTTON_TYPE_QUESTIONS
|
||||
: BUTTON_TYPE_SHORT_QUESTIONS
|
||||
).map(({ icon, title, value: questionType }) => (
|
||||
<QuestionsMiniButton
|
||||
key={title}
|
||||
onClick={() => {
|
||||
createTypedQuestion(question.id, questionType);
|
||||
}}
|
||||
icon={icon}
|
||||
text={title}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -111,11 +111,11 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={question.content.required}
|
||||
handleChange={({ target }) => updateQuestion(question.id, question => {
|
||||
checked={!question.content.required}
|
||||
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
|
||||
if (question.type !== "varimg") return;
|
||||
|
||||
question.content.required = target.checked;
|
||||
question.content.required = !target.checked;
|
||||
})}
|
||||
/>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
@ -126,7 +126,7 @@ export default function SettingOptionsAndPict({ question }: SettingOptionsAndPic
|
||||
}}
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => updateQuestion(question.id, question => {
|
||||
handleChange={({ target }) => updateQuestion<QuizQuestionVarImg>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = "";
|
||||
})}
|
||||
|
@ -64,7 +64,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
|
||||
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
@ -148,7 +148,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
@ -161,7 +161,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
marginBottom: "5px",
|
||||
opacity: question.content.xy !== "1:1" ? 1 : 0,
|
||||
@ -202,7 +202,7 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
|
||||
isActive={question.content.format === "masonry"}
|
||||
Icon={FormatIcon1}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
<Typography
|
||||
sx={{ fontWeight: "500", fontSize: "18px", color: " #4D4D4D" }}
|
||||
>
|
||||
@ -211,11 +211,11 @@ export default function SettingOpytionsPict({ question }: SettingOpytionsPictPro
|
||||
<CustomCheckbox
|
||||
sx={{ alignItems: isMobile ? "flex-start" : "" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={question.content.required}
|
||||
handleChange={({ target }) => updateQuestion(question.id, question => {
|
||||
checked={!question.content.required}
|
||||
handleChange={({ target }) => updateQuestion<QuizQuestionImages>(question.id, question => {
|
||||
if (question.type !== "images") return;
|
||||
|
||||
question.content.required = target.checked;
|
||||
question.content.required = !target.checked;
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
@ -54,7 +54,7 @@ export default function SettingTextField({
|
||||
marginRight: isFigmaTablte ? "0px" : "32px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{/* <Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
@ -126,7 +126,7 @@ export default function SettingTextField({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "0px" : "20px",
|
||||
@ -148,7 +148,7 @@ export default function SettingTextField({
|
||||
>
|
||||
Настройки вопросов
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
{/* <CustomCheckbox
|
||||
sx={{
|
||||
display: isMobile ? "flex" : "block",
|
||||
mr: isMobile ? "0px" : "16px",
|
||||
@ -161,7 +161,7 @@ export default function SettingTextField({
|
||||
question.content.autofill = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
<CustomCheckbox
|
||||
sx={{
|
||||
display: isMobile ? "flex" : "block",
|
||||
@ -169,10 +169,10 @@ export default function SettingTextField({
|
||||
alignItems: isMobile ? "flex-end" : "center",
|
||||
}}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={(e) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.required = !e.target.checked;
|
||||
updateQuestion<QuizQuestionText>(question.id, question => {
|
||||
question.content.required = !e.target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@ -193,7 +193,7 @@ export default function SettingTextField({
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionText>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked
|
||||
? question.content.innerName
|
||||
|
@ -1,18 +1,23 @@
|
||||
|
||||
import {
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
Box, useMediaQuery, useTheme,
|
||||
} from "@mui/material";
|
||||
import { DraggableList } from "./DraggableList";
|
||||
import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
|
||||
import { BranchingMap } from "./BranchingMap";
|
||||
import {useQuestionsStore} from "@root/questions/store";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
export const QuestionSwitchWindowTool = () => {
|
||||
const {openBranchingPanel} = useQuestionsStore.getState()
|
||||
console.log(openBranchingPanel)
|
||||
const {questions} = useQuestionsStore.getState()
|
||||
const {openBranchingPanel} = useUiTools()
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(600));
|
||||
console.log("questions ", questions)
|
||||
console.log("rules ", questions.filter((q) => q.type !== null).map((q) => ({id: q.content.id, rule: q.content.rule})))
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap", marginBottom: isMobile ? "20px" : undefined }}>
|
||||
<Box sx={{ flexBasis: "796px" }}>
|
||||
{openBranchingPanel? <BranchingMap /> : <DraggableList />}
|
||||
</Box>
|
||||
|
@ -17,15 +17,14 @@ import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
||||
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal"
|
||||
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/questions/actions";
|
||||
|
||||
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
export default function QuestionsPage() {
|
||||
const theme = useTheme();
|
||||
const { openedModalSettingsId, openBranchingPanel } = useQuestionsStore();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||
const { openedModalSettingsId, openBranchingPanel } = useUiTools();
|
||||
const isMobile = false//useMediaQuery(theme.breakpoints.down(660));
|
||||
const quiz = useCurrentQuiz();
|
||||
console.log(quiz)
|
||||
useLayoutEffect(() => {
|
||||
updateOpenBranchingPanel(false)
|
||||
updateEditSomeQuestion()
|
||||
@ -48,7 +47,8 @@ export default function QuestionsPage() {
|
||||
margin: "60px 0 40px 0",
|
||||
}}
|
||||
>
|
||||
<Typography variant={"h5"}>Заголовок квиза</Typography>
|
||||
<Typography variant={"h5"}>{
|
||||
quiz.name ? quiz.name : "Заголовок квиза" }</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
display: openBranchingPanel ? "none" : "flex",
|
||||
@ -115,4 +115,4 @@ export default function QuestionsPage() {
|
||||
{openedModalSettingsId !== null && <BranchingQuestions/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -148,12 +148,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
|
||||
<CustomCheckbox
|
||||
sx={{ display: "block", mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={(e) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionRating>(question.id, question => {
|
||||
if (question.type !== "rating") return;
|
||||
|
||||
question.required = !e.target.checked;
|
||||
question.content.required = !e.target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
Select as MuiSelect,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
@ -16,6 +17,7 @@ type SelectProps = {
|
||||
empty?: boolean;
|
||||
onChange?: (item: string, num: number) => void;
|
||||
sx?: SxProps;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const Select = ({
|
||||
@ -24,6 +26,7 @@ export const Select = ({
|
||||
empty,
|
||||
onChange,
|
||||
sx,
|
||||
placeholder = "",
|
||||
}: SelectProps) => {
|
||||
const [activeItem, setActiveItem] = useState<number>(
|
||||
empty ? -1 : activeItemIndex
|
||||
@ -35,9 +38,17 @@ export const Select = ({
|
||||
}, [activeItemIndex]);
|
||||
|
||||
const handleChange = (event: SelectChangeEvent) => {
|
||||
const activeItemIndex = Number(event.target.value);
|
||||
setActiveItem(activeItemIndex);
|
||||
onChange?.(items[activeItemIndex], activeItemIndex);
|
||||
const newItemIndex = Number(event.target.value);
|
||||
|
||||
if (newItemIndex === activeItem) {
|
||||
setActiveItem(-1);
|
||||
onChange?.("", -1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveItem(newItemIndex);
|
||||
onChange?.(items[newItemIndex], newItemIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -47,10 +58,19 @@ export const Select = ({
|
||||
sx={{ width: "100%", height: "48px", ...sx }}
|
||||
>
|
||||
<MuiSelect
|
||||
displayEmpty
|
||||
renderValue={(value) =>
|
||||
value ? (
|
||||
items[Number(value)]
|
||||
) : (
|
||||
<Typography sx={{ color: theme.palette.grey2.main }}>
|
||||
{placeholder}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
id="display-select"
|
||||
variant="outlined"
|
||||
value={activeItem === -1 ? "" : String(activeItem)}
|
||||
displayEmpty
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
width: "100%",
|
||||
|
@ -49,7 +49,7 @@ export default function SettingSlider({ question }: SettingSliderProps) {
|
||||
height: isMobile ? "100%" : "auto",
|
||||
alignItems: isMobile ? "flex-start" : "center",
|
||||
}}
|
||||
label={"Выбор диапозона (два ползунка)"}
|
||||
label={"Выбор диапазона (два ползунка)"}
|
||||
checked={question.content.chooseRange}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
@ -78,12 +78,12 @@ export default function SettingSlider({ question }: SettingSliderProps) {
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px", alignItems: isMobile ? "flex-end" : "center" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={(e) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionNumber>(question.id, question => {
|
||||
if (question.type !== "number") return;
|
||||
|
||||
question.required = !e.target.checked;
|
||||
question.content.required = !e.target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@ -2,12 +2,18 @@ import { useParams } from "react-router-dom";
|
||||
import { Box, Button, IconButton, Typography } from "@mui/material";
|
||||
import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { updateDragQuestionContentId } from "@root/questions/actions";
|
||||
import { useEffect } from "react";
|
||||
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import {
|
||||
AnyTypedQuizQuestion,
|
||||
UntypedQuizQuestion,
|
||||
} from "@model/questionTypes/shared";
|
||||
import { Pencil } from "../../startPage/Sidebar/icons/Pencil";
|
||||
import {updateOpenBranchingPanel, updateEditSomeQuestion} from "@root/questions/actions"
|
||||
|
||||
import {
|
||||
updateOpenBranchingPanel,
|
||||
updateEditSomeQuestion,
|
||||
updateDragQuestionContentId,
|
||||
} from "@root/uiTools/actions";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
const getItemStyle = (isDragging: any, draggableStyle: any) => ({
|
||||
// some basic styles to make the items look a bit nicer
|
||||
@ -19,12 +25,16 @@ const getItemStyle = (isDragging: any, draggableStyle: any) => ({
|
||||
background: isDragging ? "lightgreen" : "grey",
|
||||
|
||||
// styles we need to apply on draggables
|
||||
...draggableStyle
|
||||
...draggableStyle,
|
||||
});
|
||||
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
|
||||
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion;
|
||||
|
||||
export const QuestionsList = () => {
|
||||
const { questions, desireToOpenABranchingModal } = useQuestionsStore()
|
||||
const { desireToOpenABranchingModal } = useUiTools();
|
||||
const trashQuestions = useQuestionsStore().questions;
|
||||
const questions = trashQuestions.filter(
|
||||
(question) => question.type !== "result"
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ padding: "15px" }}>
|
||||
@ -53,47 +63,56 @@ export const QuestionsList = () => {
|
||||
}}
|
||||
>
|
||||
{/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/}
|
||||
{questions.filter((q: AnyQuestion) => q.type).map(({ title, content }, index) => (
|
||||
<Button
|
||||
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
|
||||
if (!content.rule.parentId) updateDragQuestionContentId(content.id)
|
||||
}}
|
||||
key={index}
|
||||
sx={{
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px",
|
||||
background: "#FFFFFF",
|
||||
borderRadius: "8px",
|
||||
border: desireToOpenABranchingModal === content.id ? "4px solid #7e2aea" : "none",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||
{questions
|
||||
.filter((q: AnyQuestion) => q.type)
|
||||
.map(({ title, content }, index) => (
|
||||
<Button
|
||||
onMouseDown={() => {
|
||||
//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
|
||||
if (!content.rule.parentId)
|
||||
updateDragQuestionContentId(content.id);
|
||||
}}
|
||||
key={index}
|
||||
sx={{
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px",
|
||||
background: "#FFFFFF",
|
||||
borderRadius: "8px",
|
||||
border:
|
||||
desireToOpenABranchingModal === content.id
|
||||
? "4px solid #7e2aea"
|
||||
: "none",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' strokeWidth='2' stroke-dasharray='8 8' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e");
|
||||
border-radius: 8px;`,
|
||||
"&:last-child": { marginBottom: 0 },
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ width: "100%", color: content.rule.parentId ? "#9A9AAF" : "#000" }}>
|
||||
{title || "нет заголовка"}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
updateOpenBranchingPanel(false)
|
||||
updateEditSomeQuestion(content.id)
|
||||
"&:last-child": { marginBottom: 0 },
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
</IconButton>
|
||||
{content.rule.parentId && <CheckedIcon />}
|
||||
</Button>
|
||||
|
||||
|
||||
))}
|
||||
<Typography
|
||||
sx={{
|
||||
width: "100%",
|
||||
color: content.rule.parentId ? "#9A9AAF" : "#000",
|
||||
}}
|
||||
>
|
||||
{title || "нет заголовка"}
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
updateOpenBranchingPanel(false);
|
||||
updateEditSomeQuestion(content.id);
|
||||
}}
|
||||
>
|
||||
<Pencil style={{ color: "#7e2aea" }} />
|
||||
</IconButton>
|
||||
{content.rule.parentId && <CheckedIcon />}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -1,18 +1,23 @@
|
||||
|
||||
|
||||
import {Box, Typography, Switch, useTheme, Button, useMediaQuery} from "@mui/material";
|
||||
|
||||
import { QuestionsList } from "./QuestionsList";
|
||||
import { updateOpenBranchingPanel } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
|
||||
import {useQuestionsStore} from "@root/questions/store";
|
||||
import {useRef} from "react";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
export const SwitchBranchingPanel = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||
const {openBranchingPanel} = useQuestionsStore.getState()
|
||||
console.log(openBranchingPanel)
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
|
||||
const {openBranchingPanel} = useUiTools()
|
||||
const ref = useRef()
|
||||
return (
|
||||
|
||||
return ( !isTablet || openBranchingPanel ?
|
||||
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
@ -26,11 +31,10 @@ export const SwitchBranchingPanel = () => {
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={openBranchingPanel}
|
||||
onChange={(_, value) => {
|
||||
console.log("меняю на " + value)
|
||||
updateOpenBranchingPanel(value)
|
||||
}}
|
||||
checked={openBranchingPanel}
|
||||
onChange={
|
||||
(e) => updateOpenBranchingPanel(e.target.checked)
|
||||
}
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 30,
|
||||
@ -87,5 +91,7 @@ export const SwitchBranchingPanel = () => {
|
||||
</Box>
|
||||
{ openBranchingPanel && <QuestionsList /> }
|
||||
</Box>
|
||||
:
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ type DesignItem = {
|
||||
};
|
||||
|
||||
const DESIGN_TYPES: DesignItem[] = [
|
||||
{ name: "Все типы файлов", value: "all" },
|
||||
// { name: "Все типы файлов", value: "all" },
|
||||
{ name: "Изображения", value: "picture" },
|
||||
{ name: "Видео", value: "video" },
|
||||
{ name: "Аудио", value: "audio" },
|
||||
|
@ -40,7 +40,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
|
||||
}}
|
||||
>
|
||||
<Typography>Настройки вопроса</Typography>
|
||||
<CustomCheckbox
|
||||
{/* <CustomCheckbox
|
||||
sx={{
|
||||
display: isMobile ? "flex" : "block",
|
||||
mr: isMobile ? "0px" : "16px",
|
||||
@ -52,17 +52,17 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
|
||||
question.content.autofill = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
<CustomCheckbox
|
||||
sx={{
|
||||
display: isMobile ? "flex" : "block",
|
||||
mr: isMobile ? "0px" : "16px",
|
||||
}}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
checked={!question.content.required}
|
||||
handleChange={(e) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.required = !e.target.checked;
|
||||
updateQuestion<QuizQuestionFile>(question.id, question => {
|
||||
question.content.required = !e.target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@ -82,7 +82,7 @@ export default function SettingsUpload({ question }: SettingsUploadProps) {
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
updateQuestion<QuizQuestionFile>(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||
});
|
||||
|
@ -99,7 +99,7 @@ export const UploadVideoModal = ({
|
||||
<CustomTextField
|
||||
placeholder={"http://example.com"}
|
||||
text={video}
|
||||
onChange={({ target }) => onUpload(target.value)}
|
||||
onChange={({ target }) => onUpload(target.value || " ")}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
@ -114,7 +114,7 @@ export const UploadVideoModal = ({
|
||||
<input
|
||||
onChange={({ target }) => {
|
||||
if (target.files?.length) {
|
||||
onUpload(URL.createObjectURL(target.files[0]));
|
||||
onUpload(URL.createObjectURL(target.files[0] || " "));
|
||||
}
|
||||
}}
|
||||
hidden
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {
|
||||
Box,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
Box,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { setQuestionInnerName, updateQuestion } from "@root/questions/actions";
|
||||
import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||
@ -12,165 +12,166 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
import InfoIcon from "../../../assets/icons/InfoIcon";
|
||||
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||
|
||||
|
||||
interface Props {
|
||||
question: QuizQuestionVariant;
|
||||
question: QuizQuestionVariant;
|
||||
}
|
||||
|
||||
export default function ResponseSettings({ question }: Props) {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(900));
|
||||
|
||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
|
||||
const updateQuestionInnerName = useDebouncedCallback((value) => {
|
||||
setQuestionInnerName(question.id, value);
|
||||
}, 200);
|
||||
const updateQuestionInnerName = useDebouncedCallback((value) => {
|
||||
setQuestionInnerName(question.id, value);
|
||||
}, 200);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: isTablet ? "column" : "none",
|
||||
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
|
||||
}}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: isTablet ? "column" : "none",
|
||||
marginRight: isFigmaTablte ? (isMobile ? "0" : "0px") : "30px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
pl: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
height: isMobile ? "18px" : "auto",
|
||||
fontWeight: "500",
|
||||
fontSize: "18px",
|
||||
color: " #4D4D4D",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pt: isMobile ? "25px" : "20px",
|
||||
pb: isMobile ? "25px" : "20px",
|
||||
pl: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
width: "100%",
|
||||
}}
|
||||
Настройки ответов
|
||||
</Typography>
|
||||
{/* <CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Длинный текстовый ответ"}
|
||||
checked={question.content.largeCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, (question) => {
|
||||
if (!("largeCheck" in question.content)) return;
|
||||
|
||||
question.content.largeCheck = target.checked;
|
||||
});
|
||||
}}
|
||||
/> */}
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Можно несколько"}
|
||||
checked={question.content.multi}
|
||||
dataCy="multiple-answers-checkbox"
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, (question) => {
|
||||
if (!("multi" in question.content)) return;
|
||||
|
||||
question.content.multi = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/* <CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={'Вариант "свой ответ"'}
|
||||
checked={question.content.own}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, (question) => {
|
||||
if (!("own" in question.content)) return;
|
||||
|
||||
question.content.own = target.checked;
|
||||
});
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
pt: isMobile ? "0px" : "20px",
|
||||
pb: "20px",
|
||||
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
|
||||
pr: isFigmaTablte ? "19px" : "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
height: isMobile ? "18px" : "auto",
|
||||
fontWeight: "500",
|
||||
fontSize: "18px",
|
||||
color: " #4D4D4D",
|
||||
}}
|
||||
>
|
||||
Настройки вопросов
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.content.required}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
|
||||
question.content.required = !target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "90%" : "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<CustomCheckbox
|
||||
sx={{
|
||||
mr: isMobile ? "0px" : "9px",
|
||||
height: isMobile ? "42px" : "26px",
|
||||
alignItems: "start",
|
||||
}}
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion<QuizQuestionVariant>(question.id, (question) => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked
|
||||
? question.content.innerName
|
||||
: "";
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isMobile && (
|
||||
<Tooltip
|
||||
title="Будет отображаться как заголовок вопроса в приходящих заявках."
|
||||
placement="top"
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
height: isMobile ? "18px" : "auto",
|
||||
fontWeight: "500",
|
||||
fontSize: "18px",
|
||||
color: " #4D4D4D",
|
||||
}}
|
||||
>
|
||||
Настройки ответов
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Длинный текстовый ответ"}
|
||||
checked={question.content.largeCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
if (!("largeCheck" in question.content)) return;
|
||||
|
||||
question.content.largeCheck = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Можно несколько"}
|
||||
checked={question.content.multi}
|
||||
dataCy="multiple-answers-checkbox"
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
if (!("multi" in question.content)) return;
|
||||
|
||||
question.content.multi = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={'Вариант "свой ответ"'}
|
||||
checked={question.content.own}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
if (!("own" in question.content)) return;
|
||||
|
||||
question.content.own = target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
pt: isMobile ? "0px" : "20px",
|
||||
pb: "20px",
|
||||
pl: isFigmaTablte ? (isTablet ? "20px" : "34px") : "28px",
|
||||
pr: isFigmaTablte ? "19px" : "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "14px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
height: isMobile ? "18px" : "auto",
|
||||
fontWeight: "500",
|
||||
fontSize: "18px",
|
||||
color: " #4D4D4D",
|
||||
}}
|
||||
>
|
||||
Настройки вопросов
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
label={"Необязательный вопрос"}
|
||||
checked={!question.required}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.required = !target.checked;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "90%" : "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<CustomCheckbox
|
||||
sx={{
|
||||
mr: isMobile ? "0px" : "9px",
|
||||
height: isMobile ? "42px" : "26px",
|
||||
alignItems: "start",
|
||||
}}
|
||||
label={"Внутреннее название вопроса"}
|
||||
checked={question.content.innerNameCheck}
|
||||
handleChange={({ target }) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.content.innerNameCheck = target.checked;
|
||||
question.content.innerName = target.checked ? question.content.innerName : "";
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isMobile && (
|
||||
<Tooltip
|
||||
title="Будет отображаться как заголовок вопроса в приходящих заявках."
|
||||
placement="top"
|
||||
>
|
||||
<Box>
|
||||
<InfoIcon />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
{question.content.innerNameCheck && (
|
||||
<CustomTextField
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
placeholder={"Развёрнутое описание вопроса"}
|
||||
text={question.content.innerName}
|
||||
onChange={({ target }) => updateQuestionInnerName(target.value)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<InfoIcon />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
{question.content.innerNameCheck && (
|
||||
<CustomTextField
|
||||
sx={{ mr: isMobile ? "0px" : "16px" }}
|
||||
placeholder={"Развёрнутое описание вопроса"}
|
||||
text={question.content.innerName}
|
||||
onChange={({ target }) => updateQuestionInnerName(target.value || " ")}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export default function HelpQuestions({ question }: HelpQuestionsProps) {
|
||||
<CustomTextField
|
||||
placeholder={"Текст консультанта"}
|
||||
text={question.content.hint.text}
|
||||
onChange={({ target }) => updateQuestionHint(target.value)}
|
||||
onChange={({ target }) => updateQuestionHint(target.value || " ")}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -9,7 +9,7 @@ export default function ImageAndVideoButtons() {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "12px", mt: "20px", mb: "20px" }}>
|
||||
<AddImage onClick={undefined/* TODO () => openCropModal("", "") */} />
|
||||
<AddImage onClick={undefined} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||
|
||||
const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
|
||||
@ -39,62 +40,34 @@ const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "ƒ",
|
||||
type: "ƒ",
|
||||
sx: {
|
||||
width: "38px",
|
||||
height: "48px",
|
||||
border: "1px solid #9A9AAF",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Скидка",
|
||||
type: "discount",
|
||||
sx: {
|
||||
width: "93px",
|
||||
height: "48px",
|
||||
border: "1px solid #9A9AAF",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
ButtonsActive: (index: number, type: string) => void;
|
||||
priceButtonsActive: number | undefined;
|
||||
resultData: QuizQuestionResult
|
||||
};
|
||||
|
||||
export default function PriceButtons({
|
||||
ButtonsActive,
|
||||
priceButtonsActive,
|
||||
resultData
|
||||
}: Props) {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: "14xp" }}>
|
||||
<Typography component={"h6"} sx={{ weight: "500", fontSize: "18px" }}>
|
||||
Стоимость
|
||||
Заголовок
|
||||
</Typography>
|
||||
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
|
||||
<DeleteIcon style={{ color: "#4D4D4D" }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box
|
||||
component="div"
|
||||
sx={{ display: "flex", flexWrap: "wrap", gap: "8px", mb: "20px" }}
|
||||
>
|
||||
{priceButtonsArray.map(({ title, type, sx }, index) => (
|
||||
<Button
|
||||
onClick={() => ButtonsActive(index, type)}
|
||||
key={title}
|
||||
sx={{
|
||||
bgcolor: priceButtonsActive === index ? "#7E2AEA" : "#F2F3F7",
|
||||
color: priceButtonsActive === index ? "#FFFF" : "#9A9AAF",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
))}
|
||||
<CustomTextField
|
||||
placeholder={"Вы прошли опрос"}
|
||||
sx={{
|
||||
borderRadius: "8px",
|
||||
height: "48px",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,76 +1,111 @@
|
||||
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
text2: string;
|
||||
image: string;
|
||||
};
|
||||
import { ResultSettings } from "./ResultSettings"
|
||||
import { createFrontResult } from "@root/questions/actions";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { Box, Typography, useTheme, useMediaQuery, Button } from "@mui/material";
|
||||
import image from "../../assets/Rectangle 110.png";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
|
||||
export default function CreationFullCard({ text, text2, image }: Props) {
|
||||
export const FirstEntry = () => {
|
||||
const theme = useTheme();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1500));
|
||||
const quiz = useCurrentQuiz();
|
||||
const { questions } = useQuestionsStore();
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1250));
|
||||
|
||||
const create = () => {
|
||||
if (quiz?.config.haveRoot) {
|
||||
questions
|
||||
.filter((question:AnyTypedQuizQuestion) => {
|
||||
return question.type !== null && question.content.rule.parentId.length !== 0 && question.content.rule.children.length === 0
|
||||
})
|
||||
.forEach(question => {
|
||||
createFrontResult(quiz.id, question.content.id)
|
||||
})
|
||||
} else {
|
||||
createFrontResult(quiz.id, "line")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
p: "20px",
|
||||
marginTop: "50px",
|
||||
borderRadius: "12px",
|
||||
display: isSmallMonitor ? "block" : "flex",
|
||||
flexDirection: isSmallMonitor ? "column" : "row",
|
||||
gap: "20px",
|
||||
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: "white",
|
||||
p: "20px",
|
||||
marginTop: "50px",
|
||||
borderRadius: "12px",
|
||||
display: isSmallMonitor ? "block" : "flex",
|
||||
flexDirection: isSmallMonitor ? "column" : "row",
|
||||
gap: "20px",
|
||||
boxShadow: `0px 100px 309px rgba(210, 208, 225, 0.24),
|
||||
0px 41.7776px 129.093px rgba(210, 208, 225, 0.172525),
|
||||
0px 22.3363px 69.0192px rgba(210, 208, 225, 0.143066),
|
||||
0px 12.5216px 38.6916px rgba(210, 208, 225, 0.12),
|
||||
0px 6.6501px 20.5488px rgba(210, 208, 225, 0.0969343),
|
||||
0px 2.76726px 8.55082px rgba(210, 208, 225, 0.0674749)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
mr: !isSmallMonitor ? "104px" : 0,
|
||||
marginBottom: "20px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" sx={{ marginBottom: "20px" }}>
|
||||
Результаты квиза в зависимости от ответов
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
maxHeight: isSmallMonitor ? "none" : "220px",
|
||||
gap: "25px",
|
||||
mr: !isSmallMonitor ? "104px" : 0,
|
||||
marginBottom: isSmallMonitor ? "20px" : 0,
|
||||
position: "relative",
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: "#4D4D4D", width: "95%" }}>
|
||||
{text}
|
||||
<Typography variant="h5" sx={{ marginBottom: "20px" }}>
|
||||
Результаты квиза в зависимости от ответов
|
||||
</Typography>
|
||||
<Typography
|
||||
<Box
|
||||
sx={{
|
||||
color: "#9A9AAF",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
height: "100%",
|
||||
gap: "25px",
|
||||
}}
|
||||
>
|
||||
{text2}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "#4D4D4D", width: "95%" }}>
|
||||
Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке.
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "#9A9AAF",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
Этот шаг - необязательный, квиз будет работать и без автоматических результатов.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<img
|
||||
src={image}
|
||||
alt="quiz creation"
|
||||
style={{
|
||||
display: "block",
|
||||
width: isSmallMonitor ? "100%" : "auto",
|
||||
maxHeight: isSmallMonitor ? "none" : "270px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<img
|
||||
src={image}
|
||||
alt="quiz creation"
|
||||
style={{
|
||||
display: "block",
|
||||
width: isSmallMonitor ? "100%" : "auto",
|
||||
maxHeight: isSmallMonitor ? "none" : "270px",
|
||||
<Button
|
||||
onClick={create}
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: "#7E2AEA",
|
||||
fontSize: "18px",
|
||||
lineHeight: "18px",
|
||||
width: "216px",
|
||||
height: "44px",
|
||||
mt: "30px",
|
||||
p: "10px 20px"
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
>
|
||||
Создать результаты
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { updateQuiz } from "@root/quizes/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import image from "../../assets/Rectangle 110.png";
|
||||
import Info from "../../assets/icons/Info";
|
||||
import CreationFullCard from "./FirstEntry";
|
||||
// import CreationFullCard from "./FirstEntry";
|
||||
|
||||
|
||||
export const Result = () => {
|
||||
@ -13,11 +13,11 @@ export const Result = () => {
|
||||
|
||||
return (
|
||||
<Box component="section">
|
||||
<CreationFullCard
|
||||
{/* <CreationFullCard
|
||||
text="Вы можете показывать разные результаты квиза (добавьте описание, изображение, стоимость и т.п.) разным пользователям, нужно только их создать и проставить условия. Таким образом ваш квиз получится максимально индивидуальным для каждого клиента. Показывайте картинку/видео вместо результата или переадресовывайте пользователя по нужной ссылке."
|
||||
text2="Этот шаг - необязательный, квиз будет работать и без автоматических результатов."
|
||||
image={image}
|
||||
/>
|
||||
/> */}
|
||||
<Box sx={{ display: "flex", mt: "30px", alignItems: "center" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import FirstEntry from "./FirstEntry"
|
||||
import { FirstEntry } from "./FirstEntry"
|
||||
import { ResultSettings } from "./ResultSettings"
|
||||
|
||||
|
||||
export default function ResultPage() {
|
||||
export const ResultPage = () => {
|
||||
const { questions } = useQuestionsStore();
|
||||
//ищём хотя бы один result
|
||||
const haveResult = questions.some((question) => {
|
||||
question.type === "result"
|
||||
})
|
||||
|
||||
|
||||
|
||||
const haveResult = questions.some((question) => question.type === "result")
|
||||
return (
|
||||
<>
|
||||
|
||||
</>
|
||||
haveResult ?
|
||||
<ResultSettings />
|
||||
:
|
||||
<FirstEntry />
|
||||
);
|
||||
}
|
121
src/pages/ResultPage/ResultSettings.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import IconPlus from "@icons/IconPlus";
|
||||
import Info from "@icons/Info";
|
||||
import Plus from "@icons/Plus";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import { Box, Button, Typography, Paper, Modal, TextField } from "@mui/material";
|
||||
import { incrementCurrentStep } from "@root/quizes/actions";
|
||||
import CustomWrapper from "@ui_kit/CustomWrapper";
|
||||
import { DescriptionForm } from "./DescriptionForm/DescriptionForm";
|
||||
import { ResultListForm } from "./ResultListForm";
|
||||
import { SettingForm } from "./SettingForm";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { WhenCard } from "./cards/WhenCard";
|
||||
import { ResultCard, checkEmptyData } from "./cards/ResultCard";
|
||||
import { EmailSettingsCard } from "./cards/EmailSettingsCard";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { createFrontResult, deleteQuestion } from "@root/questions/actions";
|
||||
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||
|
||||
export const ResultSettings = () => {
|
||||
const { questions } = useQuestionsStore()
|
||||
const quiz = useCurrentQuiz()
|
||||
const results = useQuestionsStore().questions.filter((q): q is QuizQuestionResult => q.type === "result")
|
||||
const [quizExpand, setQuizExpand] = useState(true)
|
||||
const [resultContract, setResultContract] = useState(true)
|
||||
const isReadyToLeaveRef = useRef(true);
|
||||
|
||||
useEffect(function calcIsReadyToLeave(){
|
||||
let isReadyToLeave = true;
|
||||
results.forEach((result) => {
|
||||
if (checkEmptyData({ resultData: result })) {
|
||||
isReadyToLeave = false;
|
||||
}
|
||||
});
|
||||
isReadyToLeaveRef.current = isReadyToLeave;
|
||||
}, [results])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isReadyToLeaveRef.current === false) alert("Пожалуйста, проверьте, что вы заполнили все результаты");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: "796px" }}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
margin: "60px 0 40px 0",
|
||||
}}>
|
||||
<Typography variant="h5">
|
||||
Настройки результатов
|
||||
</Typography>
|
||||
<Info />
|
||||
<Button
|
||||
disableRipple
|
||||
sx={{
|
||||
ml: "auto",
|
||||
width: "73px",
|
||||
height: "19px",
|
||||
color: "#7E2AEA",
|
||||
textDecoration: "underline",
|
||||
fontSize: "16px",
|
||||
"&:hover": {
|
||||
background: "none",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
variant="text"
|
||||
onClick={() => setQuizExpand(!quizExpand)}
|
||||
>
|
||||
Свернуть
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
<WhenCard quizExpand={quizExpand} />
|
||||
{quiz.config.resultInfo.when === "email" && <EmailSettingsCard quizExpand={quizExpand} />}
|
||||
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
|
||||
>
|
||||
<Typography variant="p1" sx={{ color: "#4D4D4D", fontSize: "14px" }}>
|
||||
Создайте результат
|
||||
</Typography>
|
||||
<Button
|
||||
disableRipple
|
||||
sx={{
|
||||
ml: "auto",
|
||||
height: "19px",
|
||||
color: "#7E2AEA",
|
||||
textDecoration: "underline",
|
||||
fontSize: "16px",
|
||||
|
||||
"&:hover": {
|
||||
background: "none",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
variant="text"
|
||||
onClick={() => setResultContract(!resultContract)}
|
||||
>
|
||||
Развернуть все
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{
|
||||
results.map((resultQuestion) => <ResultCard resultContract={resultContract} resultData={resultQuestion} key={resultQuestion.id} />)
|
||||
}
|
||||
<Modal
|
||||
open={false}
|
||||
// onClose={handleClose}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<></>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,111 +0,0 @@
|
||||
import IconPlus from "@icons/IconPlus";
|
||||
import Info from "@icons/Info";
|
||||
import Plus from "@icons/Plus";
|
||||
import ArrowLeft from "@icons/questionsPage/arrowLeft";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { incrementCurrentStep } from "@root/quizes/actions";
|
||||
import CustomWrapper from "@ui_kit/CustomWrapper";
|
||||
import { DescriptionForm } from "./DescriptionForm/DescriptionForm";
|
||||
import { ResultListForm } from "./ResultListForm";
|
||||
import { SettingForm } from "./SettingForm";
|
||||
|
||||
export const Setting = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: "796px" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: "40px" }}>
|
||||
<Typography sx={{ pr: "10px" }} variant="h5">
|
||||
Настройки результатов
|
||||
</Typography>
|
||||
<Info />
|
||||
<Button
|
||||
disableRipple
|
||||
sx={{
|
||||
ml: "auto",
|
||||
width: "73px",
|
||||
height: "19px",
|
||||
color: "#7E2AEA",
|
||||
textDecoration: "underline",
|
||||
fontSize: "16px",
|
||||
"&:hover": {
|
||||
background: "none",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
Свернуть
|
||||
</Button>
|
||||
</Box>
|
||||
<CustomWrapper sx={{ mt: "30px" }} text="Показывать результат" />
|
||||
<CustomWrapper sx={{ mt: "30px" }} text="Настройки почты" />
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
|
||||
>
|
||||
<Typography variant="p1" sx={{ color: "#4D4D4D", fontSize: "14px" }}>
|
||||
Создайте результат
|
||||
</Typography>
|
||||
<Button
|
||||
disableRipple
|
||||
sx={{
|
||||
ml: "auto",
|
||||
height: "19px",
|
||||
color: "#7E2AEA",
|
||||
textDecoration: "underline",
|
||||
fontSize: "16px",
|
||||
|
||||
"&:hover": {
|
||||
background: "none",
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
Развернуть все
|
||||
</Button>
|
||||
</Box>
|
||||
<CustomWrapper result={true} text="Показывать результат" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
columnGap: "10px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
width: "100%",
|
||||
height: "1px",
|
||||
backgroundPosition: "bottom",
|
||||
backgroundRepeat: "repeat-x",
|
||||
backgroundSize: "20px 1px",
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
|
||||
}}
|
||||
/>
|
||||
<IconPlus />
|
||||
</Box>
|
||||
<CustomWrapper result={true} text="Настройки почты" />
|
||||
<Box sx={{ pt: "30px", display: "flex", alignItems: "center" }}>
|
||||
<Plus />
|
||||
<Typography component="div" sx={{ ml: "auto" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{ padding: "10px 20px", borderRadius: "8px" }}
|
||||
>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button variant="contained" sx={{ ml: "10px" }} onClick={incrementCurrentStep}>
|
||||
Настроить форму
|
||||
</Button>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<SettingForm />
|
||||
<ResultListForm />
|
||||
<DescriptionForm />
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -5,10 +5,11 @@ import * as React from "react";
|
||||
interface Props {
|
||||
text: string;
|
||||
icon: string;
|
||||
onClick?: () => void;
|
||||
onClick?: (a:any) => void;
|
||||
value: boolean
|
||||
}
|
||||
|
||||
export const SwitchSetting = ({ text, icon, onClick }: Props) => {
|
||||
export const SwitchSetting = ({ text, icon, onClick, value }: Props) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -24,7 +25,7 @@ export const SwitchSetting = ({ text, icon, onClick }: Props) => {
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "left", maxWidth: "756px", width: "100%" }}>
|
||||
<img src={icon} alt="icon" />
|
||||
<FormControlLabel
|
||||
value="start"
|
||||
checked={value}
|
||||
control={<CustomizedSwitch />}
|
||||
label={text}
|
||||
labelPlacement="start"
|
||||
|
247
src/pages/ResultPage/cards/EmailSettingsCard.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
610
src/pages/ResultPage/cards/ResultCard.tsx
Normal file
@ -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 >
|
||||
)
|
||||
}
|
187
src/pages/ResultPage/cards/WhenCard.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { updateQuiz } from "@root/quizes/actions"
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
||||
|
||||
import { SwitchSetting } from "../SwichResult";
|
||||
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Paper,
|
||||
Button,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
import ShareNetwork from "@icons/ShareNetwork.svg";
|
||||
import ArrowCounterClockWise from "@icons/ArrowCounterClockWise.svg";
|
||||
|
||||
const whenValues = [
|
||||
{
|
||||
title: "До формы контактов",
|
||||
value: "before",
|
||||
},
|
||||
{
|
||||
title: "После формы контактов",
|
||||
value: "after",
|
||||
},
|
||||
{
|
||||
title: "Отправить на E-mail",
|
||||
value: "email",
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
quizExpand: boolean
|
||||
}
|
||||
|
||||
export const WhenCard = ({ quizExpand }: Props) => {
|
||||
const quiz = useCurrentQuiz()
|
||||
const theme = useTheme();
|
||||
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1100));
|
||||
|
||||
const [expand, setExpand] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
setExpand(false)
|
||||
}, [quizExpand])
|
||||
|
||||
|
||||
return (
|
||||
<Paper
|
||||
data-cy="quiz-question-card"
|
||||
sx={{
|
||||
maxWidth: "796px",
|
||||
width: "100%",
|
||||
borderRadius: "12px",
|
||||
backgroundColor: expand ? "white" : "#EEE4FC",
|
||||
border: expand ? "none" : "1px solid #9A9AAF",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
m: "20px 0"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: isMobile ? "10px" : "20px",
|
||||
flexDirection: isMobile ? "column" : null,
|
||||
justifyContent: "space-between",
|
||||
minHeight: "40px",
|
||||
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
margin: isMobile ? "10px 0" : 0,
|
||||
color: expand ? "#9A9AAF" : "#000000",
|
||||
}}
|
||||
>
|
||||
Показывать результат
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
width: isMobile ? "100%" : "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
sx={{ padding: "0", margin: "5px" }}
|
||||
disableRipple
|
||||
data-cy="expand-question"
|
||||
onClick={() => setExpand(!expand)}
|
||||
>
|
||||
{expand ? (
|
||||
<ExpandLessIconBG />
|
||||
) : (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
boxSizing: "border-box",
|
||||
fill: theme.palette.brightPurple.main,
|
||||
background: "#FFF",
|
||||
borderRadius: "6px",
|
||||
height: "30px",
|
||||
width: "30px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{expand && (
|
||||
<>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: "33px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: isSmallMonitor ? "column" : "row",
|
||||
justifyContent: "space-between",
|
||||
gap: "20px",
|
||||
mb: "20px",
|
||||
}}
|
||||
>
|
||||
{whenValues.map(({ title, value }, index) => (
|
||||
<Button
|
||||
onClick={() => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.when = value)}
|
||||
key={title}
|
||||
sx={{
|
||||
bgcolor: quiz?.config.resultInfo.when === value ? " #7E2AEA" : "#F2F3F7",
|
||||
color: quiz?.config.resultInfo.when === value ? " white" : "#9A9AAF",
|
||||
minWidth: isSmallMonitor ? "300px" : "auto",
|
||||
borderRadius: "8px",
|
||||
width: "237px",
|
||||
height: "44px",
|
||||
border: quiz?.config.resultInfo.when === value ? "none" : "1px solid #9A9AAF",
|
||||
"&:hover": {
|
||||
backgroundColor: quiz?.config.resultInfo.when === value ? "#581CA7" : "#7E2AEA",
|
||||
color: "white"
|
||||
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
|
||||
{
|
||||
(quiz?.config.resultInfo.when !== "email") && <SwitchSetting
|
||||
icon={ShareNetwork}
|
||||
text="Поделиться результатами"
|
||||
onClick={(event) => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.share = event.target.checked)}
|
||||
value={quiz?.config.resultInfo.share}
|
||||
/>
|
||||
}
|
||||
{
|
||||
quiz?.config.resultInfo.when === "before" && <SwitchSetting
|
||||
icon={ArrowCounterClockWise}
|
||||
text="Кнопка `Пройти тест заново`"
|
||||
onClick={(event) => updateQuiz(quiz.id, (quiz) => quiz.config.resultInfo.replay = event.target.checked)}
|
||||
value={quiz?.config.resultInfo.replay}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Typography, Button, useTheme } from "@mui/material";
|
||||
import { Box, Button, useTheme } from "@mui/material";
|
||||
|
||||
import { useQuizViewStore } from "@root/quizView";
|
||||
|
||||
@ -11,43 +11,92 @@ import { getQuestionByContentId } from "@root/questions/actions";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
type FooterProps = {
|
||||
questions: AnyTypedQuizQuestion[];
|
||||
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
|
||||
question: QuizQuestionBase;
|
||||
question: AnyTypedQuizQuestion;
|
||||
};
|
||||
|
||||
export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
|
||||
const [disabledQuestionsId, setDisabledQuestionsId] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
export const Footer = ({
|
||||
setCurrentQuestion,
|
||||
questions,
|
||||
question,
|
||||
}: FooterProps) => {
|
||||
const [disablePreviousButton, setDisablePreviousButton] =
|
||||
useState<boolean>(false);
|
||||
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const linear = !questions.find(
|
||||
({ content }) => content.rule.parentId === "root"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Логика для аргумента disabled у кнопки "Назад"
|
||||
if (question?.content.rule.parentId === "root") {
|
||||
setDisablePreviousButton(true);
|
||||
if (linear) {
|
||||
const questionIndex = questions.findIndex(({ id }) => id === question.id);
|
||||
|
||||
const previousQuestion = questions[questionIndex - 1];
|
||||
|
||||
if (previousQuestion) {
|
||||
setDisablePreviousButton(false);
|
||||
} else {
|
||||
setDisablePreviousButton(true);
|
||||
}
|
||||
} else {
|
||||
setDisablePreviousButton(false);
|
||||
if (question?.content.rule.parentId === "root") {
|
||||
setDisablePreviousButton(true);
|
||||
} else {
|
||||
setDisablePreviousButton(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Логика для аргумента disabled у кнопки "Далее"
|
||||
const nextQuestionId = getNextQuestionId();
|
||||
if (nextQuestionId) {
|
||||
const answer = answers.find(
|
||||
({ questionId }) => questionId === question.content.id
|
||||
);
|
||||
|
||||
if ("required" in question.content && question.content.required && answer) {
|
||||
setDisableNextButton(false);
|
||||
} else {
|
||||
const nextQuestion = getQuestionByContentId(
|
||||
question.content.rule.default
|
||||
);
|
||||
if (nextQuestion?.type) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
"required" in question.content &&
|
||||
question.content.required &&
|
||||
!answer
|
||||
) {
|
||||
setDisableNextButton(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (linear) {
|
||||
const questionIndex = questions.findIndex(({ id }) => id === question.id);
|
||||
|
||||
const nextQuestion = questions[questionIndex + 1];
|
||||
|
||||
if (nextQuestion) {
|
||||
setDisableNextButton(false);
|
||||
} else {
|
||||
setDisableNextButton(true);
|
||||
}
|
||||
} else {
|
||||
const nextQuestionId = getNextQuestionId();
|
||||
if (nextQuestionId) {
|
||||
setDisableNextButton(false);
|
||||
} else {
|
||||
const nextQuestion = getQuestionByContentId(
|
||||
question.content.rule.default
|
||||
);
|
||||
if (nextQuestion?.type) {
|
||||
setDisableNextButton(false);
|
||||
} else {
|
||||
setDisableNextButton(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [question]);
|
||||
}, [question, answers]);
|
||||
|
||||
const getNextQuestionId = () => {
|
||||
if (answers.length) {
|
||||
@ -57,28 +106,56 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
|
||||
|
||||
let readyBeNextQuestion = "";
|
||||
|
||||
question.content.rule.main.forEach(({ next, rules }) => {
|
||||
let longerArray = Math.max(
|
||||
rules[0].answers.length,
|
||||
[answer?.answer].length
|
||||
);
|
||||
(question as QuizQuestionBase).content.rule.main.forEach(
|
||||
({ next, rules }) => {
|
||||
let longerArray = Math.max(
|
||||
rules[0].answers.length,
|
||||
answer?.answer && Array.isArray(answer?.answer)
|
||||
? answer?.answer.length
|
||||
: [answer?.answer].length
|
||||
);
|
||||
|
||||
for (
|
||||
var i = 0;
|
||||
i < longerArray;
|
||||
i++ // Цикл по всем элементам бОльшего массива
|
||||
) {
|
||||
if (rules[0].answers[i] === answer?.answer) {
|
||||
readyBeNextQuestion = next; // Если хоть один элемент отличается, массивы не равны
|
||||
for (
|
||||
var i = 0;
|
||||
i < longerArray;
|
||||
i++ // Цикл по всем элементам бОльшего массива
|
||||
) {
|
||||
if (Array.isArray(answer?.answer)) {
|
||||
if (
|
||||
answer?.answer.find((item) =>
|
||||
String(item === rules[0].answers[i])
|
||||
)
|
||||
) {
|
||||
readyBeNextQuestion = next; // Если хоть один элемент отличается, массивы не равны
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(rules[0].answers[i]) === answer?.answer) {
|
||||
readyBeNextQuestion = next; // Если хоть один элемент отличается, массивы не равны
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return readyBeNextQuestion;
|
||||
}
|
||||
};
|
||||
|
||||
const followPreviousStep = () => {
|
||||
if (linear) {
|
||||
const questionIndex = questions.findIndex(({ id }) => id === question.id);
|
||||
|
||||
const previousQuestion = questions[questionIndex - 1];
|
||||
|
||||
if (previousQuestion) {
|
||||
setCurrentQuestion(previousQuestion);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (question?.content.rule.parentId !== "root") {
|
||||
const parent = getQuestionByContentId(question?.content.rule.parentId);
|
||||
if (parent?.type) {
|
||||
@ -92,6 +169,18 @@ export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
|
||||
};
|
||||
|
||||
const followNextStep = () => {
|
||||
if (linear) {
|
||||
const questionIndex = questions.findIndex(({ id }) => id === question.id);
|
||||
|
||||
const nextQuestion = questions[questionIndex + 1];
|
||||
|
||||
if (nextQuestion) {
|
||||
setCurrentQuestion(nextQuestion);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const nextQuestionId = getNextQuestionId();
|
||||
|
||||
if (nextQuestionId) {
|
||||
|
@ -13,15 +13,13 @@ import { Page } from "./questions/Page";
|
||||
import { Rating } from "./questions/Rating";
|
||||
import { Footer } from "./Footer";
|
||||
|
||||
import { useState, type FC } from "react";
|
||||
import { useState, type FC, useEffect } from "react";
|
||||
import type { QuestionType } from "../../model/question/question";
|
||||
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { getQuestionByContentId } from "@root/questions/actions";
|
||||
import { getQuestionByContentId } from "@root/questions/actions";
|
||||
|
||||
type QuestionProps = {
|
||||
stepNumber: number;
|
||||
setStepNumber: (step: number) => void;
|
||||
questions: AnyTypedQuizQuestion[];
|
||||
};
|
||||
|
||||
@ -39,12 +37,24 @@ const QUESTIONS_MAP: any = {
|
||||
rating: Rating,
|
||||
};
|
||||
|
||||
export const Question = ({
|
||||
questions,
|
||||
}: QuestionProps) => {
|
||||
export const Question = ({ questions }: QuestionProps) => {
|
||||
const quiz = useCurrentQuiz();
|
||||
const [currentQuestion, setCurrentQuestion] = useState(getQuestionByContentId(quiz?.config.haveRoot || ""))
|
||||
if (!currentQuestion) return <>не смог отобразить вопрос</>
|
||||
const [currentQuestion, setCurrentQuestion] =
|
||||
useState<AnyTypedQuizQuestion>();
|
||||
|
||||
useEffect(() => {
|
||||
const nextQuestion = getQuestionByContentId(quiz?.config.haveRoot || "");
|
||||
|
||||
if (nextQuestion?.type) {
|
||||
setCurrentQuestion(nextQuestion);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentQuestion(questions[0]);
|
||||
}, []);
|
||||
|
||||
if (!currentQuestion) return <>не смог отобразить вопрос</>;
|
||||
|
||||
const QuestionComponent =
|
||||
QUESTIONS_MAP[currentQuestion.type as Exclude<QuestionType, "nonselected">];
|
||||
@ -60,9 +70,10 @@ export const Question = ({
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<QuestionComponent currentQuestion={currentQuestion}/>
|
||||
<QuestionComponent currentQuestion={currentQuestion} />
|
||||
</Box>
|
||||
<Footer
|
||||
questions={questions}
|
||||
question={currentQuestion}
|
||||
setCurrentQuestion={setCurrentQuestion}
|
||||
/>
|
||||
|
@ -1,162 +1,298 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Link,
|
||||
Paper,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { isAxiosError } from "axios";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import YoutubeEmbedIframe from "../../ui_kit/StartPagePreview/YoutubeEmbedIframe";
|
||||
import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
|
||||
import { notReachable } from "../../utils/notReachable";
|
||||
import { useUADevice } from "../../utils/hooks/useUADevice";
|
||||
|
||||
import { quizApi } from "@api/quiz";
|
||||
interface Props {
|
||||
setVisualStartPage: (a:boolean) => void
|
||||
}
|
||||
|
||||
import { useQuizStore } from "@root/quizes/store";
|
||||
import { useQuestions } from "@root/questions/hooks";
|
||||
import { setQuizes } from "@root/quizes/actions";
|
||||
|
||||
type StartPageViewPublicationProps = {
|
||||
setVisualStartPage: (bool: boolean) => void;
|
||||
showNextButton:boolean
|
||||
};
|
||||
|
||||
export const StartPageViewPublication = ({
|
||||
setVisualStartPage,
|
||||
showNextButton
|
||||
}: StartPageViewPublicationProps) => {
|
||||
const quizId = Number(useParams().quizId);
|
||||
const { quizes } = useQuizStore();
|
||||
const { questions } = useQuestions();
|
||||
export const StartPageViewPublication = ({setVisualStartPage}:Props) => {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(630));
|
||||
const quiz = quizes.find(({ backendId }) => quizId === backendId);
|
||||
const isMediaFileExist =
|
||||
quiz?.config.startpage.background.desktop ||
|
||||
quiz?.config.startpage.background.video;
|
||||
const quiz = useCurrentQuiz();
|
||||
const { isMobileDevice } = useUADevice();
|
||||
|
||||
useSWR("quizes", () => quizApi.getList(), {
|
||||
onSuccess: setQuizes,
|
||||
onError: (error: unknown) => {
|
||||
const message = isAxiosError<string>(error)
|
||||
? error.response?.data ?? ""
|
||||
: "";
|
||||
if (!quiz) return null;
|
||||
|
||||
devlog("Error getting quiz list", error);
|
||||
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
|
||||
},
|
||||
});
|
||||
const handleCopyNumber = () => {
|
||||
navigator.clipboard.writeText(quiz.config.info.phonenumber);
|
||||
};
|
||||
|
||||
const background = quiz.config.startpage.background.type === "image"
|
||||
? quiz.config.startpage.background.desktop
|
||||
? (
|
||||
<img
|
||||
src={quiz.config.startpage.background.desktop}
|
||||
alt=""
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
: quiz.config.startpage.background.type === "video"
|
||||
? quiz.config.startpage.background.video
|
||||
? (
|
||||
<YoutubeEmbedIframe videoUrl={quiz.config.startpage.background.video}
|
||||
containerSX={{
|
||||
width: quiz.config.startpageType === "centered" ? "550px" : quiz.config.startpageType === "expanded" ? "100vw" : "100%",
|
||||
height: quiz.config.startpageType === "centered" ? "275px" : quiz.config.startpageType === "expanded" ? "100vh" : "100%",
|
||||
borderRadius: quiz.config.startpageType === "centered" ? "10px" : "0",
|
||||
overflow: "hidden",
|
||||
"& iframe": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transform: quiz.config.startpageType === "centered" ? "" : quiz.config.startpageType === "expanded" ? "scale(1.5)" : "scale(2.4)",
|
||||
|
||||
}
|
||||
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Paper className="quiz-preview-draghandle"
|
||||
sx={{
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
flexDirection:
|
||||
quiz?.config.startpage.position === "left" ? "row" : "row-reverse",
|
||||
flexGrow: 1,
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMediaFileExist && !isTablet ? "40%" : "100%",
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: isMediaFileExist && !isTablet ? "flex-start" : "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
background: quiz.config.startpageType === "expanded" ?
|
||||
quiz.config.startpage.position === "left" ? "linear-gradient(90deg,#272626,transparent)" :
|
||||
quiz.config.startpage.position === "center" ? "linear-gradient(180deg,transparent,#272626)" :
|
||||
"linear-gradient(270deg,#272626,transparent)"
|
||||
: "",
|
||||
color: quiz.config.startpageType === "expanded" ? "white" : "black"
|
||||
|
||||
|
||||
}}>
|
||||
<QuizPreviewLayoutByType
|
||||
quizHeaderBlock={<Box
|
||||
p={quiz.config.startpageType === "standard" ? "" : "16px"}
|
||||
>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
{quiz?.config.startpage.background.mobile && (
|
||||
<img
|
||||
src={quiz.config.startpage.background.mobile}
|
||||
style={{
|
||||
height: "50px",
|
||||
maxWidth: "100px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
<Typography sx={{ fontSize: "18px" }}>
|
||||
{quiz?.config.info.orgname}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontWeight: "bold", fontSize: "20px" }}>
|
||||
{quiz?.name}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: "16px" }}>
|
||||
{quiz?.config.startpage.description}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ fontSize: "16px", padding: "10px 15px" }}
|
||||
// disabled={!questions.length}
|
||||
onClick={() => setVisualStartPage(false)}
|
||||
>
|
||||
{quiz?.config.startpage.button
|
||||
? quiz?.config.startpage.button
|
||||
: "Пройти тест"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}
|
||||
>
|
||||
{quiz?.config.info.phonenumber}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: "12px" }}>
|
||||
{quiz?.config.info.law}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
{!isTablet && isMediaFileExist && (
|
||||
<Box sx={{ width: "60%" }}>
|
||||
{quiz?.config.startpage.background.mobile && (
|
||||
<img
|
||||
src={quiz.config.startpage.background.mobile}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{quiz.config.startpage.background.type === "video" &&
|
||||
quiz.config.startpage.background.video && (
|
||||
<video
|
||||
src={quiz.config.startpage.background.video}
|
||||
controls
|
||||
mb: "7px"
|
||||
}}>
|
||||
{quiz.config.startpage.logo && (
|
||||
<img
|
||||
src={quiz.config.startpage.logo}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
height: "37px",
|
||||
maxWidth: "43px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: "14px" }}>
|
||||
{quiz.config.info.orgname}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Link mb="16px" href={quiz.config.info.site}>
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
|
||||
{quiz.config.info.site}
|
||||
</Typography>
|
||||
</Link>
|
||||
</Box>}
|
||||
quizMainBlock={<>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: quiz.config.startpageType === "centered" ? "center" :
|
||||
quiz.config.startpageType === "expanded"
|
||||
? quiz.config.startpage.position === "center" ?
|
||||
"center"
|
||||
: "start": "start",
|
||||
mt: "28px",
|
||||
width: "100%"
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "26px",
|
||||
fontStyle: "normal",
|
||||
fontStretch: "normal",
|
||||
lineHeight: "1.2",
|
||||
}}>{quiz.name}</Typography>
|
||||
<Typography sx={{
|
||||
fontSize: "16px",
|
||||
m: "16px 0"
|
||||
}}>
|
||||
{quiz.config.startpage.description}
|
||||
</Typography>
|
||||
<Box width={ quiz.config.startpageType === "standard" ? "100%" : "auto"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
padding: "10px 15px",
|
||||
width: quiz.config.startpageType === "standard" ? "100%" : "auto"
|
||||
}}
|
||||
onClick={() => setVisualStartPage(true)}
|
||||
>
|
||||
{quiz.config.startpage.button.trim() ? quiz.config.startpage.button : "Пройти тест"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
mt: "46px"
|
||||
}}
|
||||
>
|
||||
{quiz.config.info.clickable ? (
|
||||
isMobileDevice ? (
|
||||
<Link href={`tel:${quiz.config.info.phonenumber}`}>
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
|
||||
{quiz.config.info.phonenumber}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<ButtonBase onClick={handleCopyNumber}>
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
|
||||
{quiz.config.info.phonenumber}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
)
|
||||
) : (
|
||||
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
|
||||
{quiz.config.info.phonenumber}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography sx={{ fontSize: "12px", textAlign: "end" }}>
|
||||
{quiz.config.info.law}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>}
|
||||
backgroundBlock={background}
|
||||
startpageType={quiz.config.startpageType}
|
||||
alignType={quiz.config.startpage.position}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlock, startpageType, alignType }: {
|
||||
quizHeaderBlock: JSX.Element;
|
||||
quizMainBlock: JSX.Element;
|
||||
backgroundBlock: JSX.Element | null;
|
||||
startpageType: QuizStartpageType;
|
||||
alignType: QuizStartpageAlignType;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(630));
|
||||
|
||||
switch (startpageType) {
|
||||
case null:
|
||||
case "standard": {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: alignType === "left" ? "row" : "row-reverse",
|
||||
flexGrow: 1,
|
||||
height: "100vh",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: !isTablet ? "40%" : "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: !isTablet ? "flex-start" : "center",
|
||||
p: "25px"
|
||||
}}>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
<Box sx={{
|
||||
width: "60%",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
case "expanded": {
|
||||
return (
|
||||
<Box sx={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: startpageAlignTypeToJustifyContent[alignType],
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
}}>
|
||||
<Box sx={{
|
||||
width: "40%",
|
||||
position: "relative",
|
||||
padding: "16px",
|
||||
zIndex: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
alignItems: alignType === "center" ? "center" : "start",
|
||||
}}>
|
||||
{quizHeaderBlock}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
case "centered": {
|
||||
return (
|
||||
<Box sx={{
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
"&::-webkit-scrollbar": { width: 0 },
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
{quizHeaderBlock}
|
||||
{backgroundBlock &&
|
||||
<Box>
|
||||
{backgroundBlock}
|
||||
</Box>
|
||||
}
|
||||
{quizMainBlock}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
default: notReachable(startpageType);
|
||||
}
|
||||
}
|
||||
|
||||
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
|
||||
left: "start",
|
||||
center: "center",
|
||||
right: "end",
|
||||
};
|
||||
|
@ -1,34 +1,65 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
import { StartPageViewPublication } from "./StartPageViewPublication";
|
||||
import { Question } from "./Question";
|
||||
import { useQuestions } from "@root/questions/hooks";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import useSWR from "swr";
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { setQuizes } from "@root/quizes/actions";
|
||||
import { isAxiosError } from "axios";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import { useQuizStore } from "@root/quizes/store";
|
||||
|
||||
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { setQuestions } from "@root/questions/actions";
|
||||
import { questionApi } from "@api/question";
|
||||
|
||||
export const ViewPage = () => {
|
||||
const quiz = useCurrentQuiz();
|
||||
const { questions } = useQuestions();
|
||||
const [visualStartPage, setVisualStartPage] = useState<boolean>(!quiz?.config.noStartPage);
|
||||
const { editQuizId } = useQuizStore();
|
||||
const { questions } = useQuestionsStore();
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const quizes = await quizApi.getList()
|
||||
setQuizes(quizes)
|
||||
|
||||
const questions = await questionApi.getList({ quiz_id: editQuizId })
|
||||
setQuestions(questions)
|
||||
}
|
||||
getData()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setVisualStartPage(quiz?.config.noStartPage)
|
||||
}, [questions])
|
||||
|
||||
|
||||
const filteredQuestions = questions.filter(
|
||||
({ type }) => type
|
||||
) as AnyTypedQuizQuestion[];
|
||||
const [visualStartPage, setVisualStartPage] = useState<boolean>();
|
||||
|
||||
useEffect(() => {
|
||||
const link = document.querySelector('link[rel="icon"]');
|
||||
|
||||
if (link && quiz?.config.startpage.favIcon) {
|
||||
link.setAttribute("href", quiz.config.startpage.favIcon);
|
||||
}
|
||||
}, [quiz?.config.startpage.favIcon]);
|
||||
|
||||
const filteredQuestions = (
|
||||
questions.filter(({ type }) => type) as AnyTypedQuizQuestion[]
|
||||
).sort((previousItem, item) => previousItem.page - item.page);
|
||||
|
||||
console.log("visualStartPage ", visualStartPage)
|
||||
if (visualStartPage === undefined) return <></>
|
||||
return (
|
||||
<Box>
|
||||
{visualStartPage ? (
|
||||
<StartPageViewPublication
|
||||
setVisualStartPage={setVisualStartPage}
|
||||
showNextButton={!!filteredQuestions.length}
|
||||
/>
|
||||
{!visualStartPage ? (
|
||||
<StartPageViewPublication setVisualStartPage={setVisualStartPage} />
|
||||
) : (
|
||||
<Question
|
||||
questions={filteredQuestions}
|
||||
/>
|
||||
<Question questions={filteredQuestions} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import DatePicker from "react-datepicker";
|
||||
import dayjs from "dayjs";
|
||||
import { DatePicker } from "@mui/x-date-pickers";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
|
||||
import CalendarIcon from "@icons/CalendarIcon";
|
||||
|
||||
type DateProps = {
|
||||
currentQuestion: QuizQuestionDate;
|
||||
@ -13,10 +13,10 @@ type DateProps = {
|
||||
|
||||
export const Date = ({ currentQuestion }: DateProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
const answer = answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
)?.answer as string;
|
||||
const [day, month, year] = answer?.split(".") || [];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@ -30,19 +30,54 @@ export const Date = ({ currentQuestion }: DateProps) => {
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
selected={answer ? new window.Date(answer) : new window.Date()}
|
||||
onChange={(date) =>
|
||||
slots={{
|
||||
openPickerIcon: () => <CalendarIcon />,
|
||||
}}
|
||||
value={dayjs(
|
||||
answer
|
||||
? new window.Date(`${month}.${day}.${year}`)
|
||||
: new window.Date()
|
||||
)}
|
||||
onChange={(date) => {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
String(
|
||||
date?.toLocaleDateString("ru-RU", {
|
||||
new window.Date(date.toDate()).toLocaleDateString("ru-RU", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
slotProps={{
|
||||
openPickerButton: {
|
||||
sx: {
|
||||
p: 0,
|
||||
},
|
||||
"data-cy": "open-datepicker",
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: "#F2F3F7",
|
||||
borderRadius: "10px",
|
||||
maxWidth: "250px",
|
||||
pr: "22px",
|
||||
"& input": {
|
||||
py: "11px",
|
||||
pl: "20px",
|
||||
lineHeight: "19px",
|
||||
},
|
||||
"& fieldset": {
|
||||
borderColor: "#9A9AAF",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@ -6,9 +5,10 @@ import {
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
useTheme,
|
||||
FormControl,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
|
||||
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
@ -22,20 +22,19 @@ type EmojiProps = {
|
||||
export const Emoji = ({ currentQuestion }: EmojiProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
if (!answer) {
|
||||
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
|
||||
}
|
||||
}, []);
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5">{currentQuestion.title}</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
value={currentQuestion.content.variants.findIndex(
|
||||
({ id }) => answer === id
|
||||
)}
|
||||
onChange={({ target }) =>
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
@ -50,33 +49,74 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{currentQuestion.content.variants.map(
|
||||
({ id, answer, extendedText }, index) => (
|
||||
<FormControlLabel
|
||||
key={id}
|
||||
<Box sx={{ display: "flex", width: "100%", gap: "42px" }}>
|
||||
{currentQuestion.content.variants.map((variant, index) => (
|
||||
<FormControl
|
||||
key={variant.id}
|
||||
sx={{
|
||||
borderRadius: "12px",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
overflow: "hidden",
|
||||
maxWidth: "317px",
|
||||
width: "100%",
|
||||
height: "255px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
marginBottom: "15px",
|
||||
borderRadius: "5px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: "193px",
|
||||
background: "#ffffff",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{variant.extendedText && (
|
||||
<Typography fontSize={"100px"}>
|
||||
{variant.extendedText}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
margin: 0,
|
||||
padding: "15px",
|
||||
color: theme.palette.grey2.main,
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
color: "#4D4D4D",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
value={index}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[index].id
|
||||
);
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
deleteAnswer(currentQuestion.content.id);
|
||||
}
|
||||
}}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={
|
||||
<Box sx={{ display: "flex", gap: "10px" }}>
|
||||
<Typography>{extendedText}</Typography>
|
||||
<Typography>{answer}</Typography>
|
||||
<Typography>{variant.answer}</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</FormControl>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
@ -1,28 +1,56 @@
|
||||
import { Box, Typography, ButtonBase } from "@mui/material";
|
||||
|
||||
import UploadBox from "@ui_kit/UploadBox";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
ButtonBase,
|
||||
useTheme,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import { UPLOAD_FILE_TYPES_MAP } from "@ui_kit/QuizPreview/QuizPreviewQuestionTypes/File";
|
||||
|
||||
import UploadIcon from "@icons/UploadIcon";
|
||||
import CloseBold from "@icons/CloseBold";
|
||||
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
|
||||
import type { DragEvent } from "react";
|
||||
import type { UploadFileType } from "@model/questionTypes/file";
|
||||
|
||||
type FileProps = {
|
||||
currentQuestion: QuizQuestionFile;
|
||||
};
|
||||
|
||||
export const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
|
||||
UploadFileType,
|
||||
{ title: string; description: string }
|
||||
> = {
|
||||
all: { title: "Добавить файл", description: "Принимает любые файлы" },
|
||||
picture: {
|
||||
title: "Добавить изображение",
|
||||
description: "Принимает изображения",
|
||||
},
|
||||
video: {
|
||||
title: "Добавить видео",
|
||||
description: "Принимает .mp4 и .mov формат — максимум 100мб",
|
||||
},
|
||||
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" },
|
||||
document: { title: "Добавить документ", description: "Принимает документы" },
|
||||
} as const;
|
||||
|
||||
export const File = ({ currentQuestion }: FileProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
|
||||
const answer = answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
)?.answer as string;
|
||||
const theme = useTheme();
|
||||
const uploadFile = ({ target }: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
updateAnswer(currentQuestion.content.id, `${file.name}|${URL.createObjectURL(file)}`);
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
`${file.name}|${URL.createObjectURL(file)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -35,22 +63,112 @@ export const File = ({ currentQuestion }: FileProps) => {
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
maxWidth: answer?.split("|")[0] ? "640px" : "550px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
|
||||
<input
|
||||
onChange={uploadFile}
|
||||
hidden
|
||||
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
|
||||
multiple
|
||||
type="file"
|
||||
/>
|
||||
<UploadBox icon={<UploadIcon />} text="5 MB максимум" />
|
||||
</ButtonBase>
|
||||
{answer?.split("|")[0] && (
|
||||
<Typography sx={{ marginTop: "15px" }}>
|
||||
{answer?.split("|")[0]}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
|
||||
<Typography>Вы загрузили:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 5px 5px 16px",
|
||||
backgroundColor: theme.palette.brightPurple.main,
|
||||
borderRadius: "8px",
|
||||
color: "#FFFFFF",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Typography>{answer?.split("|")[0]}</Typography>
|
||||
<IconButton
|
||||
sx={{ p: 0 }}
|
||||
onClick={() => {
|
||||
updateAnswer(currentQuestion.content.id, "");
|
||||
}}
|
||||
>
|
||||
<CloseBold />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!answer?.split("|")[0] && (
|
||||
<ButtonBase component="label" sx={{ justifyContent: "flex-start" }}>
|
||||
<input
|
||||
onChange={uploadFile}
|
||||
hidden
|
||||
accept={UPLOAD_FILE_TYPES_MAP[currentQuestion.content.type]}
|
||||
multiple
|
||||
type="file"
|
||||
/>
|
||||
<Box
|
||||
onDragOver={(event: DragEvent<HTMLDivElement>) =>
|
||||
event.preventDefault()
|
||||
}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "120px",
|
||||
display: "flex",
|
||||
gap: "50px",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
padding: "33px 44px 33px 55px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
<UploadIcon />
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.grey2.main,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{
|
||||
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
|
||||
.title
|
||||
}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.grey2.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
}}
|
||||
>
|
||||
{
|
||||
UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type]
|
||||
.description
|
||||
}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
)}
|
||||
{answer && currentQuestion.content.type === "picture" && (
|
||||
<img
|
||||
src={answer.split("|")[1]}
|
||||
alt=""
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{answer && currentQuestion.content.type === "video" && (
|
||||
<video
|
||||
src={answer.split("|")[1]}
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
useTheme,
|
||||
useMediaQuery, FormControl,
|
||||
Box,
|
||||
Typography,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
|
||||
@ -22,28 +21,21 @@ type ImagesProps = {
|
||||
export const Images = ({ currentQuestion }: ImagesProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(500));
|
||||
|
||||
useEffect(() => {
|
||||
if (!answer) {
|
||||
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5">{currentQuestion.title}</Typography>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
onChange={({ target }) =>
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[Number(target.value)].id
|
||||
)
|
||||
}
|
||||
value={currentQuestion.content.variants.findIndex(
|
||||
({ id }) => answer === id
|
||||
)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
@ -64,50 +56,59 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.variants.map(
|
||||
({ id, answer, extendedText }, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
borderRadius: "5px",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", gap: "10px" }}
|
||||
>
|
||||
<Box sx={{ width: "100%", height: "300px" }}>
|
||||
{extendedText && (
|
||||
<img
|
||||
src={extendedText}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{currentQuestion.content.variants.map((variant, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "5px",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[index].id
|
||||
);
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
deleteAnswer(currentQuestion.content.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<Box sx={{ width: "100%", height: "300px" }}>
|
||||
{variant.extendedText && (
|
||||
<img
|
||||
src={variant.extendedText}
|
||||
alt=""
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<FormControl
|
||||
key={id}
|
||||
sx={{
|
||||
display: "block",
|
||||
textAlign: "center",
|
||||
color: theme.palette.grey2.main,
|
||||
marginTop: "10px",
|
||||
}}
|
||||
value={index}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={answer}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
display: "block",
|
||||
textAlign: "center",
|
||||
color: theme.palette.grey2.main,
|
||||
marginTop: "10px",
|
||||
}}
|
||||
value={index}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={variant.answer}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Typography, Slider, useTheme } from "@mui/material";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
import { CustomSlider } from "@ui_kit/CustomSlider";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
|
||||
@ -12,16 +14,47 @@ type NumberProps = {
|
||||
};
|
||||
|
||||
export const Number = ({ currentQuestion }: NumberProps) => {
|
||||
const [minRange, setMinRange] = useState<string>("0");
|
||||
const [maxRange, setMaxRange] = useState<string>("100");
|
||||
const theme = useTheme();
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const updateMinRangeDebounced = useDebouncedCallback(
|
||||
(value, crowded = false) => {
|
||||
if (crowded) {
|
||||
setMinRange(maxRange);
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.content.id, value);
|
||||
},
|
||||
1000
|
||||
);
|
||||
const updateMaxRangeDebounced = useDebouncedCallback(
|
||||
(value, crowded = false) => {
|
||||
if (crowded) {
|
||||
setMaxRange(minRange);
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.content.id, value);
|
||||
},
|
||||
1000
|
||||
);
|
||||
const answer = answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
)?.answer as string;
|
||||
|
||||
const min = window.Number(currentQuestion.content.range.split("—")[0]);
|
||||
const max = window.Number(currentQuestion.content.range.split("—")[1]);
|
||||
const sliderValue = answer || currentQuestion.content.start + "—" + max;
|
||||
|
||||
useEffect(() => {
|
||||
if (answer) {
|
||||
setMinRange(answer.split("—")[0]);
|
||||
setMaxRange(answer.split("—")[1]);
|
||||
}
|
||||
|
||||
if (!answer) {
|
||||
updateAnswer(currentQuestion.content.id, "1");
|
||||
setMinRange(String(currentQuestion.content.start));
|
||||
setMaxRange(String(max));
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -34,40 +67,105 @@ export const Number = ({ currentQuestion }: NumberProps) => {
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
marginTop: "20px",
|
||||
gap: "30px",
|
||||
}}
|
||||
>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={answer || ""}
|
||||
onChange={({ target }) => {
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
window.Number(target.value) > max
|
||||
? String(max)
|
||||
: window.Number(target.value) < min
|
||||
? String(min)
|
||||
: target.value
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
"& .MuiInputBase-input": { textAlign: "center" },
|
||||
}}
|
||||
/>
|
||||
<Slider
|
||||
value={window.Number(answer || 1)}
|
||||
<CustomSlider
|
||||
value={
|
||||
currentQuestion.content.chooseRange
|
||||
? sliderValue.split("—").length || 0 > 1
|
||||
? sliderValue.split("—").map((item) => window.Number(item))
|
||||
: [min, min + 1]
|
||||
: window.Number(sliderValue.split("—")[0])
|
||||
}
|
||||
min={min}
|
||||
max={max}
|
||||
step={currentQuestion.content.step || 1}
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
padding: "0",
|
||||
marginTop: "25px",
|
||||
}}
|
||||
onChange={(_, value) => {
|
||||
updateAnswer(currentQuestion.content.id, String(value));
|
||||
const range = String(value).replace(",", "—");
|
||||
updateAnswer(currentQuestion.content.id, range);
|
||||
}}
|
||||
onChangeCommitted={(_, value) => {
|
||||
if (currentQuestion.content.chooseRange) {
|
||||
const range = value as number[];
|
||||
|
||||
setMinRange(String(range[0]));
|
||||
setMaxRange(String(range[1]));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{!currentQuestion.content.chooseRange && (
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={answer}
|
||||
onChange={({ target }) => {
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
window.Number(target.value) > max
|
||||
? String(max)
|
||||
: window.Number(target.value) < min
|
||||
? String(min)
|
||||
: target.value
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
"& .MuiInputBase-input": { textAlign: "center" },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentQuestion.content.chooseRange && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
alignItems: "center",
|
||||
"& .MuiFormControl-root": { width: "auto" },
|
||||
}}
|
||||
>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={minRange}
|
||||
onChange={({ target }) => {
|
||||
setMinRange(target.value);
|
||||
|
||||
if (window.Number(target.value) >= window.Number(maxRange)) {
|
||||
updateMinRangeDebounced(`${maxRange}—${maxRange}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMinRangeDebounced(`${target.value}—${maxRange}`);
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
"& .MuiInputBase-input": { textAlign: "center" },
|
||||
}}
|
||||
/>
|
||||
<Typography>до</Typography>
|
||||
<CustomTextField
|
||||
placeholder="0"
|
||||
value={maxRange}
|
||||
onChange={({ target }) => {
|
||||
setMaxRange(target.value);
|
||||
|
||||
if (window.Number(target.value) <= window.Number(minRange)) {
|
||||
updateMaxRangeDebounced(`${minRange}—${minRange}`, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateMaxRangeDebounced(`${minRange}—${target.value}`);
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: "80px",
|
||||
"& .MuiInputBase-input": { textAlign: "center" },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -7,6 +7,12 @@ import {
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
|
||||
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
|
||||
import FlagIcon from "@icons/questionsPage/FlagIcon";
|
||||
import HeartIcon from "@icons/questionsPage/heartIcon";
|
||||
import LikeIcon from "@icons/questionsPage/likeIcon";
|
||||
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
|
||||
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
|
||||
import StarIconMini from "@icons/questionsPage/StarIconMini";
|
||||
|
||||
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
|
||||
@ -15,53 +21,82 @@ type RatingProps = {
|
||||
currentQuestion: QuizQuestionRating;
|
||||
};
|
||||
|
||||
const buttonRatingForm = [
|
||||
{
|
||||
name: "star",
|
||||
icon: (color: string) => <StarIconMini width={50} color={color} />,
|
||||
},
|
||||
{
|
||||
name: "trophie",
|
||||
icon: (color: string) => <TropfyIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "flag",
|
||||
icon: (color: string) => <FlagIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "heart",
|
||||
icon: (color: string) => <HeartIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "like",
|
||||
icon: (color: string) => <LikeIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "bubble",
|
||||
icon: (color: string) => <LightbulbIcon color={color} />,
|
||||
},
|
||||
{
|
||||
name: "hashtag",
|
||||
icon: (color: string) => <HashtagIcon color={color} />,
|
||||
},
|
||||
];
|
||||
|
||||
export const Rating = ({ currentQuestion }: RatingProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
const form = buttonRatingForm.find(
|
||||
({ name }) => name === currentQuestion.content.form
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5">{currentQuestion.title}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "20px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<RatingComponent
|
||||
value={Number(answer || 0)}
|
||||
onChange={(_, value) => updateAnswer(currentQuestion.content.id, String(value))}
|
||||
sx={{ height: "50px" }}
|
||||
max={currentQuestion.content.steps}
|
||||
icon={
|
||||
<StarIconMini
|
||||
color={theme.palette.brightPurple.main}
|
||||
width={50}
|
||||
sx={{ transform: "scale(0.8)" }}
|
||||
/>
|
||||
}
|
||||
emptyIcon={
|
||||
<StarIconMini
|
||||
color={theme.palette.grey2.main}
|
||||
width={50}
|
||||
sx={{ transform: "scale(0.8)" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Typography sx={{ color: theme.palette.grey2.main }}>
|
||||
{currentQuestion.content.ratingNegativeDescription}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
maxWidth: `${currentQuestion.content.steps * 50}px`,
|
||||
color: theme.palette.grey2.main,
|
||||
display: "inline-block",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography>{currentQuestion.content.ratingNegativeDescription}</Typography>
|
||||
<Typography>{currentQuestion.content.ratingPositiveDescription}</Typography>
|
||||
<RatingComponent
|
||||
value={Number(answer || 0)}
|
||||
onChange={(_, value) =>
|
||||
updateAnswer(currentQuestion.content.id, String(value))
|
||||
}
|
||||
sx={{ height: "50px", gap: "15px" }}
|
||||
max={currentQuestion.content.steps}
|
||||
icon={form?.icon(theme.palette.brightPurple.main)}
|
||||
emptyIcon={form?.icon(theme.palette.grey2.main)}
|
||||
/>
|
||||
</Box>
|
||||
<Typography sx={{ color: theme.palette.grey2.main }}>
|
||||
{currentQuestion.content.ratingPositiveDescription}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { Select as SelectComponent } from "../../../pages/Questions/Select";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
|
||||
|
||||
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
|
||||
|
||||
@ -12,7 +12,10 @@ type SelectProps = {
|
||||
|
||||
export const Select = ({ currentQuestion }: SelectProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
@ -26,9 +29,16 @@ export const Select = ({ currentQuestion }: SelectProps) => {
|
||||
}}
|
||||
>
|
||||
<SelectComponent
|
||||
activeItemIndex={Number(answer) || 0}
|
||||
placeholder={currentQuestion.content.default}
|
||||
activeItemIndex={answer ? Number(answer) : -1}
|
||||
items={currentQuestion.content.variants.map(({ answer }) => answer)}
|
||||
onChange={(_, value) => {
|
||||
if (value < 0) {
|
||||
deleteAnswer(currentQuestion.content.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.content.id, String(value));
|
||||
}}
|
||||
/>
|
||||
|
@ -3,31 +3,57 @@ import {
|
||||
Box,
|
||||
Typography,
|
||||
RadioGroup,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Checkbox,
|
||||
TextField,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import {
|
||||
useQuizViewStore,
|
||||
updateAnswer,
|
||||
deleteAnswer,
|
||||
updateOwnVariant,
|
||||
deleteOwnVariant,
|
||||
} from "@root/quizView";
|
||||
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
import { CheckboxIcon } from "@icons/Checkbox";
|
||||
|
||||
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
|
||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||
|
||||
type VariantProps = {
|
||||
stepNumber: number;
|
||||
currentQuestion: QuizQuestionVariant;
|
||||
};
|
||||
|
||||
type VariantItemProps = {
|
||||
currentQuestion: QuizQuestionVariant;
|
||||
variant: QuestionVariant;
|
||||
answer: string | string[] | undefined;
|
||||
index: number;
|
||||
own?: boolean;
|
||||
};
|
||||
|
||||
export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const { answers, ownVariants } = useQuizViewStore();
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
const ownVariant = ownVariants.find(
|
||||
(variant) => variant.contentId === currentQuestion.content.id
|
||||
);
|
||||
|
||||
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
|
||||
|
||||
useEffect(() => {
|
||||
if (!answer) {
|
||||
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
|
||||
if (!ownVariant) {
|
||||
updateOwnVariant(currentQuestion.content.id, "");
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -35,15 +61,11 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
<Box>
|
||||
<Typography variant="h5">{currentQuestion.title}</Typography>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<RadioGroup
|
||||
<Group
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
onChange={({ target }) =>
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[Number(target.value)].id
|
||||
)
|
||||
}
|
||||
value={currentQuestion.content.variants.findIndex(
|
||||
({ id }) => answer === id
|
||||
)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
@ -53,30 +75,35 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", flexWrap: "wrap", width: "100%", gap: "20px", }}>
|
||||
{currentQuestion.content.variants.map(({ id, answer }, index) => (
|
||||
<FormControlLabel
|
||||
key={id}
|
||||
sx={{
|
||||
margin: "0",
|
||||
borderRadius: "12px",
|
||||
padding: "15px",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
display: "flex",
|
||||
maxWidth: "685px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
}}
|
||||
value={index}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={answer}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
{currentQuestion.content.variants.map((variant, index) => (
|
||||
<VariantItem
|
||||
key={variant.id}
|
||||
currentQuestion={currentQuestion}
|
||||
variant={variant}
|
||||
answer={answer}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{currentQuestion.content.own && ownVariant && (
|
||||
<VariantItem
|
||||
own
|
||||
currentQuestion={currentQuestion}
|
||||
variant={ownVariant.variant}
|
||||
answer={answer}
|
||||
index={currentQuestion.content.variants.length + 2}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
</Group>
|
||||
{currentQuestion.content.back && (
|
||||
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
|
||||
<img
|
||||
@ -90,3 +117,70 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const VariantItem = ({
|
||||
currentQuestion,
|
||||
variant,
|
||||
answer,
|
||||
index,
|
||||
own = false,
|
||||
}: VariantItemProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={variant.id}
|
||||
sx={{
|
||||
margin: "0",
|
||||
borderRadius: "12px",
|
||||
padding: "15px",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
display: "flex",
|
||||
maxWidth: "685px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
"&.MuiFormControl-root": {
|
||||
width: "100%",
|
||||
|
||||
}
|
||||
}}
|
||||
value={index}
|
||||
labelPlacement="start"
|
||||
control={
|
||||
currentQuestion.content.multi ? (
|
||||
<Checkbox
|
||||
checked={!!answer?.includes(variant.id)}
|
||||
checkedIcon={<CheckboxIcon checked />}
|
||||
icon={<CheckboxIcon />}
|
||||
/>
|
||||
) : (
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
)
|
||||
}
|
||||
label={own ? <TextField label="Другое..." /> : variant.answer}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
const variantId = currentQuestion.content.variants[index].id;
|
||||
|
||||
if (currentQuestion.content.multi) {
|
||||
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentAnswer.includes(variantId)
|
||||
? currentAnswer?.filter((item) => item !== variantId)
|
||||
: [...currentAnswer, variantId]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateAnswer(currentQuestion.content.id, variantId);
|
||||
|
||||
if (answer === variantId) {
|
||||
deleteAnswer(currentQuestion.content.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@ -8,7 +7,9 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useQuizViewStore, updateAnswer } from "@root/quizView";
|
||||
import gag from "./gag.png"
|
||||
|
||||
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
|
||||
|
||||
import RadioCheck from "@ui_kit/RadioCheck";
|
||||
import RadioIcon from "@ui_kit/RadioIcon";
|
||||
@ -22,67 +23,97 @@ type VarimgProps = {
|
||||
export const Varimg = ({ currentQuestion }: VarimgProps) => {
|
||||
const { answers } = useQuizViewStore();
|
||||
const theme = useTheme();
|
||||
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
|
||||
const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
|
||||
const { answer } =
|
||||
answers.find(
|
||||
({ questionId }) => questionId === currentQuestion.content.id
|
||||
) ?? {};
|
||||
const variant = currentQuestion.content.variants.find(
|
||||
({ id }) => answer === id
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!answer) {
|
||||
updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id);
|
||||
}
|
||||
}, []);
|
||||
console.log(currentQuestion)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5">{currentQuestion.title}</Typography>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<Box sx={{ display: "flex", marginTop: "20px" }}>
|
||||
<RadioGroup
|
||||
name={currentQuestion.id}
|
||||
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
|
||||
onChange={({ target }) =>
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[Number(target.value)].id
|
||||
)
|
||||
}
|
||||
value={currentQuestion.content.variants.findIndex(
|
||||
({ id }) => answer === id
|
||||
)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
flexBasis: "100%",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{currentQuestion.content.variants.map(({ id, answer }, index) => (
|
||||
{currentQuestion.content.variants.map((variant, index) => (
|
||||
<FormControlLabel
|
||||
key={id}
|
||||
key={variant.id}
|
||||
sx={{
|
||||
marginBottom: "15px",
|
||||
borderRadius: "5px",
|
||||
padding: "15px",
|
||||
color: theme.palette.grey2.main,
|
||||
color: "#4D4D4D",
|
||||
border: `1px solid ${theme.palette.grey2.main}`,
|
||||
display: "flex",
|
||||
}}
|
||||
value={index}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
updateAnswer(
|
||||
currentQuestion.content.id,
|
||||
currentQuestion.content.variants[index].id
|
||||
);
|
||||
|
||||
if (answer === currentQuestion.content.variants[index].id) {
|
||||
deleteAnswer(currentQuestion.content.id);
|
||||
}
|
||||
}}
|
||||
control={
|
||||
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
|
||||
}
|
||||
label={answer}
|
||||
label={variant.answer}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</RadioGroup>
|
||||
{(variant?.extendedText || currentQuestion.content.back) && (
|
||||
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
|
||||
<img
|
||||
src={answer ? variant?.extendedText : currentQuestion.content.back}
|
||||
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: "450px",
|
||||
width: "100%",
|
||||
height: "450px",
|
||||
border: "1px solid #9A9AAF",
|
||||
borderRadius: "12px",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#9A9AAF12",
|
||||
color: "#9A9AAF"
|
||||
}}
|
||||
>
|
||||
{
|
||||
answer ?
|
||||
<img
|
||||
src={
|
||||
variant?.extendedText || gag
|
||||
}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
alt=""
|
||||
/>
|
||||
:
|
||||
(variant?.extendedText || "Выберите вариант ответа слева")
|
||||
}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
{/* )} */}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
BIN
src/pages/ViewPublicationPage/questions/gag.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
@ -1,14 +1,14 @@
|
||||
import { login } from "@api/auth";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
IconButton,
|
||||
Link,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
IconButton,
|
||||
Link,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { setUserId, useUserStore } from "@root/user";
|
||||
import InputTextfield from "@ui_kit/InputTextfield";
|
||||
@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
|
||||
import { useFormik } from "formik";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { Link as RouterLink, useNavigate, useLocation } from "react-router-dom";
|
||||
import { object, string } from "yup";
|
||||
|
||||
interface Values {
|
||||
@ -43,6 +43,8 @@ export default function SigninDialog() {
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const formik = useFormik<Values>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
@ -110,11 +112,11 @@ export default function SigninDialog() {
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0px 15px 80px rgb(210 208 225 / 70%)",
|
||||
"& .MuiFormHelperText-root.Mui-error, & .MuiFormHelperText-root.Mui-error.MuiFormHelperText-filled":
|
||||
{
|
||||
position: "absolute",
|
||||
top: "46px",
|
||||
margin: "0",
|
||||
},
|
||||
{
|
||||
position: "absolute",
|
||||
top: "46px",
|
||||
margin: "0",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@ -153,7 +155,7 @@ export default function SigninDialog() {
|
||||
id="email"
|
||||
label="Email"
|
||||
gap={upMd ? "10px" : "10px"}
|
||||
/>
|
||||
/>
|
||||
<PasswordInput
|
||||
TextfieldProps={{
|
||||
value: formik.values.password,
|
||||
@ -190,16 +192,17 @@ export default function SigninDialog() {
|
||||
Войти
|
||||
</Button>
|
||||
{/* <Link
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
href="#"
|
||||
sx={{
|
||||
color: "#4D4D4D",
|
||||
mb: "15px",
|
||||
}}
|
||||
>
|
||||
Забыли пароль?
|
||||
</Link> */}
|
||||
component={RouterLink}
|
||||
to="/"
|
||||
href="#"
|
||||
|
||||
sx={{
|
||||
color: "#4D4D4D",
|
||||
mb: "15px",
|
||||
}}
|
||||
>
|
||||
Забыли пароль?
|
||||
</Link> */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
@ -17,7 +17,7 @@ import PasswordInput from "@ui_kit/passwordInput";
|
||||
import { useFormik } from "formik";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import {Link as RouterLink, useLocation, useNavigate} from "react-router-dom";
|
||||
import { object, ref, string } from "yup";
|
||||
|
||||
interface Values {
|
||||
@ -50,6 +50,8 @@ export default function SignupDialog() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const theme = useTheme();
|
||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||
const location = useLocation()
|
||||
|
||||
const navigate = useNavigate();
|
||||
const formik = useFormik<Values>({
|
||||
initialValues,
|
||||
@ -220,6 +222,7 @@ export default function SignupDialog() {
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="/signin"
|
||||
state={{ backgroundLocation: location.state.backgroundLocation }}
|
||||
sx={{
|
||||
color: "#7E2AEA",
|
||||
mt: "auto",
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@ -9,17 +7,14 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { createQuiz } from "@root/quizes/actions";
|
||||
import { useQuizes } from "@root/quizes/hooks";
|
||||
import SectionWrapper from "@ui_kit/SectionWrapper";
|
||||
import { isAxiosError } from "axios";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
import ComplexNavText from "./ComplexNavText";
|
||||
import FirstQuiz from "./FirstQuiz";
|
||||
import QuizCard from "./QuizCard";
|
||||
import { setQuizes, createQuiz } from "@root/quizes/actions";
|
||||
import { useQuizStore } from "@root/quizes/store";
|
||||
|
||||
|
||||
interface Props {
|
||||
@ -31,23 +26,14 @@ export default function MyQuizzesFull({
|
||||
outerContainerSx: sx,
|
||||
children,
|
||||
}: Props) {
|
||||
useSWR("quizes", () => quizApi.getList(), {
|
||||
onSuccess: setQuizes,
|
||||
onError: error => {
|
||||
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
|
||||
|
||||
devlog("Error getting quiz list", error);
|
||||
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
|
||||
},
|
||||
});
|
||||
const quizArray = useQuizStore(state => state.quizes);
|
||||
const { quizes } = useQuizes();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(500));
|
||||
|
||||
return (
|
||||
<>
|
||||
{quizArray.length === 0 ? (
|
||||
{quizes.length === 0 ? (
|
||||
<FirstQuiz />
|
||||
) : (
|
||||
<SectionWrapper maxWidth="lg">
|
||||
@ -83,7 +69,7 @@ export default function MyQuizzesFull({
|
||||
mb: "60px",
|
||||
}}
|
||||
>
|
||||
{quizArray.map(quiz => (
|
||||
{quizes.map(quiz => (
|
||||
<QuizCard
|
||||
key={quiz.id}
|
||||
quiz={quiz}
|
||||
|
@ -156,7 +156,7 @@ export default function QuizCard({
|
||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
<Box onClick={() => setSubMenuOpen(false)}>
|
||||
<Button
|
||||
{/* <Button
|
||||
sx={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
@ -165,7 +165,7 @@ export default function QuizCard({
|
||||
onClick={()=>{}}
|
||||
>
|
||||
Копировать
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
sx={{
|
||||
display: "block",
|
||||
|
@ -29,30 +29,40 @@ import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
import { SidebarMobile } from "./Sidebar/SidebarMobile";
|
||||
import {cleanQuestions, updateOpenBranchingPanel} from "@root/questions/actions";
|
||||
import {BranchingPanel} from "../Questions/BranchingPanel";
|
||||
import {useQuestionsStore} from "@root/questions/store";
|
||||
import { cleanQuestions } from "@root/questions/actions";
|
||||
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
|
||||
import { BranchingPanel } from "../Questions/BranchingPanel";
|
||||
import { setQuestions } from "@root/questions/actions";
|
||||
import { useQuestionsStore } from "@root/questions/store";
|
||||
import { useQuizes } from "@root/quizes/hooks";
|
||||
import { questionApi } from "@api/question";
|
||||
import { useUiTools } from "@root/uiTools/store";
|
||||
|
||||
|
||||
export default function StartPage() {
|
||||
useSWR("quizes", () => quizApi.getList(), {
|
||||
onSuccess: setQuizes,
|
||||
onError: error => {
|
||||
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
|
||||
|
||||
devlog("Error getting quiz list", error);
|
||||
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
|
||||
},
|
||||
});
|
||||
|
||||
export default function EditPage() {
|
||||
const quiz = useCurrentQuiz();
|
||||
const { editQuizId } = useQuizStore();
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
const quizes = await quizApi.getList()
|
||||
setQuizes(quizes)
|
||||
|
||||
const questions = await questionApi.getList({ quiz_id: editQuizId })
|
||||
setQuestions(questions)
|
||||
}
|
||||
getData()
|
||||
}, [])
|
||||
|
||||
const { openBranchingPanel } = useUiTools()
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const editQuizId = useQuizStore(state => state.editQuizId);
|
||||
const quiz = useCurrentQuiz();
|
||||
const currentStep = useQuizStore(state => state.currentStep);
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
|
||||
const {openBranchingPanel} = useQuestionsStore.getState()
|
||||
const quizConfig = quiz?.config;
|
||||
|
||||
useEffect(() => {
|
||||
@ -64,6 +74,7 @@ export default function StartPage() {
|
||||
cleanQuestions();
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*хедер*/}
|
||||
@ -208,12 +219,13 @@ export default function StartPage() {
|
||||
sx={{
|
||||
background: theme.palette.background.default,
|
||||
width: "100%",
|
||||
padding: isMobile ? "16px" : "25px",
|
||||
padding: isMobile ? "16px 16px 140px 16px" : "25px",
|
||||
height: "calc(100vh - 80px)",
|
||||
overflow: "auto",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{/* Выбор текущей страницы редактирования чего-либо находится здесь */}
|
||||
{quizConfig &&
|
||||
<>
|
||||
<Stepper activeStep={currentStep} />
|
||||
@ -226,7 +238,7 @@ export default function StartPage() {
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
{isTablet && [1, 2, 3].includes(currentStep) && (
|
||||
{isTablet &&
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@ -240,75 +252,74 @@ export default function StartPage() {
|
||||
background: "#FFF",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: openBranchingPanel ? "none" : "display",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
padding: "18px",
|
||||
background: "#fff",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
value={openBranchingPanel}
|
||||
onChange={(_, value) => {
|
||||
updateOpenBranchingPanel(value)
|
||||
}}
|
||||
{[1, 2].includes(currentStep) && !openBranchingPanel && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 30,
|
||||
padding: 0,
|
||||
"& .MuiSwitch-switchBase": {
|
||||
padding: 0,
|
||||
margin: "2px",
|
||||
transitionDuration: "300ms",
|
||||
"&.Mui-checked": {
|
||||
transform: "translateX(20px)",
|
||||
color: theme.palette.brightPurple.main,
|
||||
"& + .MuiSwitch-track": {
|
||||
backgroundColor: "#E8DCF9",
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
|
||||
},
|
||||
"&.Mui-disabled .MuiSwitch-thumb": {
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
"&.Mui-disabled + .MuiSwitch-track": {
|
||||
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxSizing: "border-box",
|
||||
width: 25,
|
||||
height: 25,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 13,
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(["background-color"], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
padding: "18px",
|
||||
background: "#fff",
|
||||
borderRadius: "12px",
|
||||
boxShadow: "0px 10px 30px #e7e7e7",
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
|
||||
Логика ветвления
|
||||
</Typography>
|
||||
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
|
||||
Настройте связи между вопросами
|
||||
</Typography>
|
||||
>
|
||||
<Switch
|
||||
checked={openBranchingPanel}
|
||||
onChange={
|
||||
(e) => updateOpenBranchingPanel(e.target.checked)
|
||||
}
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 30,
|
||||
padding: 0,
|
||||
"& .MuiSwitch-switchBase": {
|
||||
padding: 0,
|
||||
margin: "2px",
|
||||
transitionDuration: "300ms",
|
||||
"&.Mui-checked": {
|
||||
transform: "translateX(20px)",
|
||||
color: theme.palette.brightPurple.main,
|
||||
"& + .MuiSwitch-track": {
|
||||
backgroundColor: "#E8DCF9",
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
|
||||
},
|
||||
"&.Mui-disabled .MuiSwitch-thumb": {
|
||||
color:
|
||||
theme.palette.mode === "light"
|
||||
? theme.palette.grey[100]
|
||||
: theme.palette.grey[600],
|
||||
},
|
||||
"&.Mui-disabled + .MuiSwitch-track": {
|
||||
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
||||
},
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxSizing: "border-box",
|
||||
width: 25,
|
||||
height: 25,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
borderRadius: 13,
|
||||
backgroundColor:
|
||||
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
|
||||
opacity: 1,
|
||||
transition: theme.transitions.create(["background-color"], {
|
||||
duration: 500,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
|
||||
Логика ветвления
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
@ -320,7 +331,8 @@ export default function StartPage() {
|
||||
Опубликовать
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
}
|
||||
|
||||
</Box>
|
||||
</>
|
||||
);
|
125
src/pages/startPage/FaviconDropZone.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import UploadIcon from "@icons/UploadIcon";
|
||||
import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useState } from "react";
|
||||
import { UploadImageModal } from "../../pages/Questions/UploadImage/UploadImageModal";
|
||||
import { useDisclosure } from "../../utils/useDisclosure";
|
||||
|
||||
|
||||
const allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];
|
||||
|
||||
interface Props {
|
||||
imageUrl: string | null;
|
||||
onImageUploadClick: (image: Blob) => void;
|
||||
onDeleteClick: () => void;
|
||||
}
|
||||
|
||||
export default function FaviconDropZone({ imageUrl, onImageUploadClick, onDeleteClick }: Props) {
|
||||
const theme = useTheme();
|
||||
const quiz = useCurrentQuiz();
|
||||
const [isDropReady, setIsDropReady] = useState<boolean>(false);
|
||||
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
|
||||
|
||||
if (!quiz) return null;
|
||||
|
||||
async function handleImageUpload(file: File) {
|
||||
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||
if (!allowedFileTypes.includes(file.type)) return enqueueSnackbar("Допустимые форматы изображений: png, jpeg, gif");
|
||||
|
||||
onImageUploadClick(file);
|
||||
closeImageUploadModal();
|
||||
}
|
||||
|
||||
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDropReady(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (!file || imageUrl) return;
|
||||
|
||||
handleImageUpload(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}>
|
||||
<UploadImageModal
|
||||
isOpen={isImageUploadOpen}
|
||||
onClose={closeImageUploadModal}
|
||||
handleImageChange={handleImageUpload}
|
||||
/>
|
||||
<Box
|
||||
onDragEnter={() => !imageUrl && setIsDropReady(true)}
|
||||
onDragExit={() => setIsDropReady(false)}
|
||||
onDragOver={e => e.preventDefault()}
|
||||
onDrop={onDrop}
|
||||
sx={{
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
border: `1px solid ${isDropReady ? "red" : theme.palette.grey2.main}`,
|
||||
borderRadius: "8px",
|
||||
}}>
|
||||
<ButtonBase
|
||||
onClick={imageUrl ? undefined : openImageUploadModal}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{imageUrl ?
|
||||
<img
|
||||
src={imageUrl}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "scale-down",
|
||||
}}
|
||||
/>
|
||||
:
|
||||
<UploadIcon />
|
||||
}
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
}}>
|
||||
{imageUrl &&
|
||||
<ButtonBase onClick={onDeleteClick}>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.orange.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
Удалить
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
}
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.orange.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
textDecoration: "underline",
|
||||
mt: "auto",
|
||||
}}
|
||||
>
|
||||
5 MB максимум
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -2,7 +2,6 @@ import AlignCenterIcon from "@icons/AlignCenterIcon";
|
||||
import AlignLeftIcon from "@icons/AlignLeftIcon";
|
||||
import AlignRightIcon from "@icons/AlignRightIcon";
|
||||
import ArrowDown from "@icons/ArrowDownIcon";
|
||||
import InfoIcon from "@icons/InfoIcon";
|
||||
import LayoutCenteredIcon from "@icons/LayoutCenteredIcon";
|
||||
import LayoutExpandedIcon from "@icons/LayoutExpandedIcon";
|
||||
import LayoutStandartIcon from "@icons/LayoutStandartIcon";
|
||||
@ -11,16 +10,14 @@ import { QuizStartpageType } from "@model/quizSettings";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { incrementCurrentStep, updateQuiz, uploadQuizImage } from "@root/quizes/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
@ -28,15 +25,14 @@ import CustomCheckbox from "@ui_kit/CustomCheckbox";
|
||||
import CustomTextField from "@ui_kit/CustomTextField";
|
||||
import SelectableButton from "@ui_kit/SelectableButton";
|
||||
import { StartPagePreview } from "@ui_kit/StartPagePreview";
|
||||
import UploadBox from "@ui_kit/UploadBox";
|
||||
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
|
||||
import { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import UploadIcon from "../../assets/icons/UploadIcon";
|
||||
import FaviconDropZone from "./FaviconDropZone";
|
||||
import ModalSizeImage from "./ModalSizeImage";
|
||||
import SelectableIconButton from "./SelectableIconButton";
|
||||
import { DropZone } from "./dropZone";
|
||||
import Extra from "./extra";
|
||||
import { resizeFavIcon } from "@ui_kit/reactImageFileResizer";
|
||||
|
||||
|
||||
const designTypes = [
|
||||
@ -65,40 +61,20 @@ export default function StartPageSettings() {
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down(950));
|
||||
const quiz = useCurrentQuiz();
|
||||
const [formState, setFormState] = useState<"design" | "content">("design");
|
||||
|
||||
const designType = quiz?.config?.startpageType;
|
||||
|
||||
const videoHC = (videoInp: HTMLInputElement) => {
|
||||
const file = videoInp.files?.[0];
|
||||
|
||||
if (file) {
|
||||
setVideo(URL.createObjectURL(file));
|
||||
}
|
||||
};
|
||||
|
||||
const [video, setVideo] = useState("");
|
||||
|
||||
const [mobileVersion, setMobileVersion] = useState(false);
|
||||
|
||||
if (!quiz) return null;
|
||||
|
||||
const MobileVersionHC = (bool: boolean) => {
|
||||
setMobileVersion(bool);
|
||||
};
|
||||
|
||||
if (!quiz) return null; // TODO throw and catch with error boundary
|
||||
const designType = quiz?.config?.startpageType;
|
||||
|
||||
const favIconDropZoneElement = (
|
||||
<DropZone
|
||||
sx={{ height: "48px", width: "48px" }}
|
||||
deleteIconSx={{ right: -40, top: -10 }}
|
||||
<FaviconDropZone
|
||||
imageUrl={quiz.config.startpage.favIcon}
|
||||
originalImageUrl={quiz.config.startpage.originalFavIcon}
|
||||
onImageUploadClick={async file => {
|
||||
const resizedImage = await resizeFavIcon(file);
|
||||
uploadQuizImage(quiz.id, resizedImage, (quiz, url) => {
|
||||
quiz.config.startpage.favIcon = url;
|
||||
quiz.config.startpage.originalFavIcon = url;
|
||||
});
|
||||
}}
|
||||
onImageSaveClick={async file => {
|
||||
const resizedImage = await resizeFavIcon(file);
|
||||
uploadQuizImage(quiz.id, resizedImage, (quiz, url) => {
|
||||
quiz.config.startpage.favIcon = url;
|
||||
@ -355,213 +331,70 @@ export default function StartPageSettings() {
|
||||
|
||||
<ModalSizeImage />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "10px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
icon={<IconCheck />}
|
||||
checkedIcon={<MobilePhoneIcon bgcolor={"#EEE4FC"} />}
|
||||
/>
|
||||
}
|
||||
label="мобильная версия"
|
||||
sx={{
|
||||
color: theme.palette.brightPurple.main,
|
||||
textDecorationLine: "underline",
|
||||
textDecorationColor: theme.palette.brightPurple.main,
|
||||
ml: "-9px",
|
||||
userSelect: "none",
|
||||
"& .css-14o5ia4-MuiTypography-root": {
|
||||
fontSize: "16px"
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
MobileVersionHC(!mobileVersion);
|
||||
}}
|
||||
/>
|
||||
{mobileVersion ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey3.main,
|
||||
mt: "11px",
|
||||
mb: "14px",
|
||||
}}
|
||||
>
|
||||
Изображение для мобильной версии
|
||||
</Typography>
|
||||
<DropZone
|
||||
text={"5 MB максимум"}
|
||||
imageUrl={quiz.config.startpage.background.mobile}
|
||||
originalImageUrl={quiz.config.startpage.background.originalMobile}
|
||||
onImageUploadClick={file => {
|
||||
uploadQuizImage(quiz.id, file, (quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
quiz.config.startpage.background.originalMobile = url;
|
||||
});
|
||||
}}
|
||||
onImageSaveClick={file => {
|
||||
uploadQuizImage(quiz.id, file, (quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
});
|
||||
}}
|
||||
onDeleteClick={() => {
|
||||
updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.mobile = null;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: quiz.config.startpage.background.type === "image" ? "none" : "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "7px",
|
||||
mt: "20px",
|
||||
mb: "14px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{ fontWeight: 500, color: theme.palette.grey3.main }}
|
||||
>
|
||||
Добавить видео
|
||||
</Typography>
|
||||
<Tooltip title="Можно загрузить видео." placement="top">
|
||||
<Box>
|
||||
<InfoIcon />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<ButtonBase
|
||||
component="label"
|
||||
sx={{ justifyContent: "flex-start" }}
|
||||
>
|
||||
<input
|
||||
onChange={(event) => videoHC(event.target)}
|
||||
hidden
|
||||
accept=".mp4"
|
||||
multiple
|
||||
type="file"
|
||||
/>
|
||||
<UploadBox
|
||||
icon={<UploadIcon />}
|
||||
sx={{
|
||||
height: "48px",
|
||||
width: "48px",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
{video ? <video src={video} width="400" controls /> : null}
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey3.main,
|
||||
mt: "20px",
|
||||
mb: "5px",
|
||||
}}
|
||||
>
|
||||
Настройки видео
|
||||
</Typography>
|
||||
<CustomCheckbox
|
||||
label="Зацикливать видео"
|
||||
checked={quiz.config.startpage.background.cycle}
|
||||
handleChange={e => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.cycle = e.target.checked;
|
||||
})}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey3.main,
|
||||
mt: "11px",
|
||||
mb: "14px",
|
||||
}}
|
||||
>
|
||||
Изображение для мобильной версии
|
||||
</Typography>
|
||||
<DropZone
|
||||
text={"5 MB максимум"}
|
||||
imageUrl={quiz.config.startpage.background.mobile}
|
||||
originalImageUrl={quiz.config.startpage.background.originalMobile}
|
||||
onImageUploadClick={file => {
|
||||
uploadQuizImage(quiz.id, file, (quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
quiz.config.startpage.background.originalMobile = url;
|
||||
});
|
||||
}}
|
||||
onImageSaveClick={file => {
|
||||
uploadQuizImage(quiz.id, file, (quiz, url) => {
|
||||
quiz.config.startpage.background.mobile = url;
|
||||
});
|
||||
}}
|
||||
onDeleteClick={() => {
|
||||
updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.mobile = null;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey3.main,
|
||||
mt: "20px",
|
||||
mb: "14px",
|
||||
}}
|
||||
>
|
||||
Расположение элементов
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "left";
|
||||
<CustomTextField
|
||||
placeholder="URL видео"
|
||||
text={quiz.config.startpage.background.video ?? ""}
|
||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.background.video = e.target.value;
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "left"}
|
||||
Icon={AlignLeftIcon}
|
||||
/>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "center";
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "center"}
|
||||
Icon={AlignCenterIcon}
|
||||
sx={{ display: designType === "centered" ? "flex" : "none" }}
|
||||
/>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "right";
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "right"}
|
||||
Icon={AlignRightIcon}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
|
||||
{designType !== "centered" &&
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
color: theme.palette.grey3.main,
|
||||
mt: "20px",
|
||||
mb: "14px",
|
||||
}}
|
||||
>
|
||||
Расположение элементов
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "left";
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "left"}
|
||||
Icon={AlignLeftIcon}
|
||||
/>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "center";
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "center"}
|
||||
Icon={AlignCenterIcon}
|
||||
sx={{ display: designType === "standard" ? "none" : "flex" }}
|
||||
/>
|
||||
<SelectableIconButton
|
||||
onClick={() => updateQuiz(quiz.id, quiz => {
|
||||
quiz.config.startpage.position = "right";
|
||||
})}
|
||||
isActive={quiz.config.startpage.position === "right"}
|
||||
Icon={AlignRightIcon}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
}
|
||||
{(isTablet || !isSmallMonitor) && (
|
||||
<>
|
||||
<Box
|
||||
@ -613,25 +446,7 @@ export default function StartPageSettings() {
|
||||
>
|
||||
Favicon
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "end",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{favIconDropZoneElement}
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.orange.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
5 MB максимум
|
||||
</Typography>
|
||||
</Box>
|
||||
{favIconDropZoneElement}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
@ -695,25 +510,7 @@ export default function StartPageSettings() {
|
||||
>
|
||||
Favicon
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "end",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{favIconDropZoneElement}
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.orange.main,
|
||||
fontSize: "16px",
|
||||
lineHeight: "19px",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
5 MB максимум
|
||||
</Typography>
|
||||
</Box>
|
||||
{favIconDropZoneElement}
|
||||
</>
|
||||
)}
|
||||
{(!isSmallMonitor || (isSmallMonitor && formState === "content")) && (
|
||||
|
@ -44,7 +44,7 @@ export const DropZone = ({ text, sx, deleteIconSx, imageUrl, originalImageUrl, o
|
||||
setCropModalImageBlob,
|
||||
} = useCropModalState();
|
||||
|
||||
if (!quiz) return null; // TODO throw and catch with error boundary
|
||||
if (!quiz) return null;
|
||||
|
||||
async function handleImageUpload(file: File) {
|
||||
if (file.size > 5 * 2 ** 20) return enqueueSnackbar("Размер картинки слишком велик");
|
||||
|
@ -10,7 +10,7 @@ export default function StepOne() {
|
||||
const quiz = useCurrentQuiz();
|
||||
const config = quiz?.config;
|
||||
|
||||
if (!config) return null; // TODO throw and catch with error boundary
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -20,7 +20,7 @@ export default function Steptwo() {
|
||||
|
||||
const config = quiz?.config;
|
||||
|
||||
if (!config) return null; // TODO throw and catch with error boundary
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: "60px" }}>
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import { questionApi } from "@api/question";
|
||||
import { quizApi } from "@api/quiz";
|
||||
import { devlog } from "@frontend/kitui";
|
||||
@ -11,24 +10,25 @@ import { nanoid } from "nanoid";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||
import { RequestQueue } from "../../utils/requestQueue";
|
||||
import { updateRootContentId } from "@root/quizes/actions"
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks"
|
||||
import { updateRootContentId } from "@root/quizes/actions";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||
import { useUiTools } from "../uiTools/store";
|
||||
import { withErrorBoundary } from "react-error-boundary";
|
||||
|
||||
|
||||
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
||||
const untypedQuestions = state.questions.filter(q => q.type === null);
|
||||
const untypedResultQuestions = state.questions.filter(q => q.type === null || q.type === "result");
|
||||
|
||||
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
|
||||
state.questions.push(...untypedQuestions);
|
||||
state.questions.push(...untypedResultQuestions);
|
||||
}, {
|
||||
type: "setQuestions",
|
||||
questions,
|
||||
});
|
||||
|
||||
export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
|
||||
state.questions.push({
|
||||
export const createUntypedQuestion = (quizId: number, insertAfterQuestionId?: string) => setProducedState(state => {
|
||||
const newUntypedQuestion = {
|
||||
id: nanoid(),
|
||||
quizId,
|
||||
type: null,
|
||||
@ -36,7 +36,16 @@ export const createUntypedQuestion = (quizId: number) => setProducedState(state
|
||||
description: "",
|
||||
deleted: false,
|
||||
expanded: true,
|
||||
});
|
||||
};
|
||||
|
||||
if (insertAfterQuestionId) {
|
||||
const index = state.questions.findIndex(q => q.id === insertAfterQuestionId);
|
||||
if (index === -1) return;
|
||||
state.questions.splice(index + 1, 0, newUntypedQuestion);
|
||||
return;
|
||||
}
|
||||
|
||||
state.questions.push(newUntypedQuestion);
|
||||
}, {
|
||||
type: "createUntypedQuestion",
|
||||
quizId,
|
||||
@ -87,6 +96,18 @@ const setQuestionBackendId = (questionId: string, backendId: number) => setProdu
|
||||
backendId,
|
||||
});
|
||||
|
||||
const updateQuestionOrders = () => {
|
||||
const questions = useQuestionsStore.getState().questions.filter(
|
||||
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
|
||||
);
|
||||
|
||||
questions.forEach((question, index) => {
|
||||
updateQuestion(question.id, question => {
|
||||
question.page = index;
|
||||
}, true);
|
||||
});
|
||||
};
|
||||
|
||||
export const reorderQuestions = (
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
@ -101,6 +122,8 @@ export const reorderQuestions = (
|
||||
sourceIndex,
|
||||
destinationIndex,
|
||||
});
|
||||
|
||||
updateQuestionOrders();
|
||||
};
|
||||
|
||||
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
|
||||
@ -117,21 +140,53 @@ export const collapseAllQuestions = () => setProducedState(state => {
|
||||
state.questions.forEach(question => question.expanded = false);
|
||||
}, "collapseAllQuestions");
|
||||
|
||||
const DELETE_TIMEOUT = 5000;
|
||||
|
||||
export const deleteQuestionWithTimeout = (questionId: string, deleteFn: (questionId: string) => void) => setProducedState(state => {
|
||||
const question = state.questions.find(q => q.id === questionId);
|
||||
if (!question) return;
|
||||
if (question.type === null || question.type === "result") {
|
||||
queueMicrotask(() => deleteFn(questionId));
|
||||
return;
|
||||
}
|
||||
|
||||
question.deleted = true;
|
||||
clearTimeout(question.deleteTimeoutId);
|
||||
question.deleteTimeoutId = window.setTimeout(() => {
|
||||
deleteFn(questionId);
|
||||
}, DELETE_TIMEOUT);
|
||||
}, {
|
||||
type: "deleteQuestionWithTimeout",
|
||||
questionId,
|
||||
});
|
||||
|
||||
export const cancelQuestionDeletion = (questionId: string) => setProducedState(state => {
|
||||
const question = state.questions.find(q => q.id === questionId);
|
||||
if (!question || question.type === null || question.type === "result") return;
|
||||
|
||||
question.deleted = false;
|
||||
clearTimeout(question.deleteTimeoutId);
|
||||
}, {
|
||||
type: "cancelQuestionDeletion",
|
||||
questionId,
|
||||
});
|
||||
|
||||
|
||||
const REQUEST_DEBOUNCE = 200;
|
||||
const requestQueue = new RequestQueue();
|
||||
let requestTimeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
export const updateQuestion = (
|
||||
export const updateQuestion = <T = AnyTypedQuizQuestion>(
|
||||
questionId: string,
|
||||
updateFn: (question: AnyTypedQuizQuestion) => void,
|
||||
updateFn: (question: T) => void,
|
||||
skipQueue = false,
|
||||
) => {
|
||||
setProducedState(state => {
|
||||
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.type !== null && q.content.id === questionId);
|
||||
if (!question) return;
|
||||
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
|
||||
|
||||
updateFn(question);
|
||||
updateFn(question as T);
|
||||
}, {
|
||||
type: "updateQuestion",
|
||||
questionId,
|
||||
@ -139,21 +194,37 @@ export const updateQuestion = (
|
||||
});
|
||||
|
||||
// clearTimeout(requestTimeoutId);
|
||||
// requestTimeoutId = setTimeout(() => {
|
||||
requestQueue.enqueue(async () => {
|
||||
|
||||
const request = async () => {
|
||||
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.type !== null && q.content.id === questionId);
|
||||
if (!q) return;
|
||||
if (q.type === null) throw new Error("Cannot send update request for untyped question");
|
||||
|
||||
const response = await questionApi.edit(questionToEditQuestionRequest(q));
|
||||
try {
|
||||
const response = await questionApi.edit(questionToEditQuestionRequest(q));
|
||||
|
||||
setQuestionBackendId(questionId, response.updated);
|
||||
}).catch(error => {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
|
||||
const questionResult = useQuestionsStore.getState().questions.find(questionResult => questionResult.type === "result" && questionResult.content.rule.parentId === q.content.id);
|
||||
if (questionResult && q.content.rule.default.length !== 0) deleteQuestion(questionResult.quizId);
|
||||
|
||||
devlog("Error editing question", { error, questionId });
|
||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||
});
|
||||
if (q.backendId !== response.updated) {
|
||||
console.warn(`Question backend id has changed from ${q.backendId} to ${response.updated}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isAxiosCanceledError(error)) return;
|
||||
|
||||
devlog("Error editing question", { error, questionId });
|
||||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||||
}
|
||||
};
|
||||
|
||||
if (skipQueue) {
|
||||
request();
|
||||
return;
|
||||
}
|
||||
|
||||
// requestTimeoutId = setTimeout(() => {
|
||||
requestQueue.enqueue(request);
|
||||
// }, REQUEST_DEBOUNCE);
|
||||
};
|
||||
|
||||
@ -263,13 +334,13 @@ export const changeQuestionType = (
|
||||
type: QuestionType,
|
||||
) => {
|
||||
updateQuestion(questionId, question => {
|
||||
const oldId = question.content.id
|
||||
const oldRule = question.content.rule
|
||||
oldRule.main = []
|
||||
const oldId = question.content.id;
|
||||
const oldRule = question.content.rule;
|
||||
oldRule.main = [];
|
||||
question.type = type;
|
||||
question.content = defaultQuestionByType[type].content;
|
||||
question.content.id = oldId
|
||||
question.content.rule = oldRule
|
||||
question.content.id = oldId;
|
||||
question.content.rule = oldRule;
|
||||
});
|
||||
};
|
||||
|
||||
@ -277,18 +348,21 @@ export const createTypedQuestion = async (
|
||||
questionId: string,
|
||||
type: QuestionType,
|
||||
) => requestQueue.enqueue(async () => {
|
||||
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
const questions = useQuestionsStore.getState().questions;
|
||||
const question = questions.find(q => q.id === questionId);
|
||||
if (!question) return;
|
||||
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
|
||||
|
||||
const untypedOrResultQuestionsLength = questions.filter(q => q.type === "result" || q.type === null).length;
|
||||
|
||||
try {
|
||||
const createdQuestion = await questionApi.create({
|
||||
quiz_id: question.quizId,
|
||||
type,
|
||||
title: question.title,
|
||||
description: question.description,
|
||||
page: 0,
|
||||
required: true,
|
||||
page: questions.length - untypedOrResultQuestionsLength,
|
||||
required: false,
|
||||
content: JSON.stringify(defaultQuestionByType[type].content),
|
||||
});
|
||||
|
||||
@ -303,65 +377,35 @@ export const createTypedQuestion = async (
|
||||
type: "createTypedQuestion",
|
||||
question,
|
||||
});
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error creating question", error);
|
||||
enqueueSnackbar("Не удалось создать вопрос");
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
|
||||
const { questions } = useQuestionsStore.getState()
|
||||
const question = questions.find(q => q.id === questionId);
|
||||
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
|
||||
console.log("Я получил запрос на удаление. ИД - ", questionId)
|
||||
|
||||
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
console.log("delete question ", question)
|
||||
if (!question) return;
|
||||
|
||||
|
||||
|
||||
if (question.type === null) {
|
||||
console.log("removeQuestion")
|
||||
removeQuestion(questionId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await questionApi.delete(question.backendId);
|
||||
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
|
||||
updateRootContentId(quizId, "")
|
||||
clearRuleForAll()
|
||||
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
|
||||
const clearQuestions = [] as string[]
|
||||
|
||||
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
|
||||
questions.forEach((targetQuestion) => {
|
||||
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
|
||||
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
|
||||
getChildren(targetQuestion) //и ищем его потомков
|
||||
}
|
||||
})
|
||||
}
|
||||
getChildren(question)
|
||||
//чистим потомков от инфы ветвления
|
||||
clearQuestions.forEach((id) => {
|
||||
updateQuestion(id, question => {
|
||||
question.content.rule.parentId = ""
|
||||
question.content.rule.main = []
|
||||
question.content.rule.default = ""
|
||||
})
|
||||
})
|
||||
|
||||
//чистим rule родителя
|
||||
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
|
||||
const newRule = {}
|
||||
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
|
||||
newRule.parentId = parentQuestion.content.rule.parentId
|
||||
newRule.default = questions.filter((q) => {
|
||||
return q.content.rule.parentId === question.content.rule.parentId && q.content.id !== question.content.id
|
||||
})[0]?.content.id || ""
|
||||
//Если этот вопрос был дефолтным у родителя - чистим дефолт
|
||||
//Смотрим можем ли мы заменить id на один из main
|
||||
|
||||
console.log(newRule)
|
||||
updateQuestion(question.content.rule.parentId, (PQ) => {
|
||||
PQ.content.rule = newRule
|
||||
})
|
||||
}
|
||||
removeQuestion(questionId);
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error deleting question", error);
|
||||
enqueueSnackbar("Не удалось удалить вопрос");
|
||||
@ -375,8 +419,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
|
||||
const frontId = nanoid();
|
||||
if (question.type === null) {
|
||||
const copiedQuestion = structuredClone(question);
|
||||
copiedQuestion.id = frontId
|
||||
copiedQuestion.content.id = frontId
|
||||
copiedQuestion.id = frontId;
|
||||
|
||||
setProducedState(state => {
|
||||
state.questions.push(copiedQuestion);
|
||||
@ -394,9 +437,9 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
|
||||
|
||||
const copiedQuestion = structuredClone(question);
|
||||
copiedQuestion.backendId = newQuestionId;
|
||||
copiedQuestion.id = frontId
|
||||
copiedQuestion.content.id = frontId
|
||||
copiedQuestion.content.rule = { main: [], parentId: "", default: "" }
|
||||
copiedQuestion.id = frontId;
|
||||
copiedQuestion.content.id = frontId;
|
||||
copiedQuestion.content.rule = { main: [], parentId: "", default: "", children: [] };
|
||||
|
||||
setProducedState(state => {
|
||||
state.questions.push(copiedQuestion);
|
||||
@ -405,6 +448,8 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
|
||||
questionId,
|
||||
quizId,
|
||||
});
|
||||
|
||||
updateQuestionOrders();
|
||||
} catch (error) {
|
||||
devlog("Error copying question", error);
|
||||
enqueueSnackbar("Не удалось скопировать вопрос");
|
||||
@ -419,9 +464,7 @@ function setProducedState<A extends string | { type: unknown; }>(
|
||||
};
|
||||
|
||||
|
||||
export const cleardragQuestionContentId = () => {
|
||||
useQuestionsStore.setState({ dragQuestionContentId: null });
|
||||
};
|
||||
|
||||
|
||||
export const getQuestionById = (questionId: string | null) => {
|
||||
if (questionId === null) return null;
|
||||
@ -436,51 +479,38 @@ export const getQuestionByContentId = (questionContentId: string | null) => {
|
||||
}) || null;
|
||||
};
|
||||
|
||||
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
|
||||
export const updateDragQuestionContentId = (contentId?: string) => {
|
||||
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
|
||||
}
|
||||
|
||||
|
||||
export const clearRuleForAll = () => {
|
||||
const { questions } = useQuestionsStore.getState()
|
||||
const { questions } = useQuestionsStore.getState();
|
||||
questions.forEach(question => {
|
||||
if (question.type !== null && (question.content.rule.main.length > 0 || question.content.rule.default.length > 0 || question.content.rule.parentId.length > 0)) {
|
||||
updateQuestion(question.content.id, question => {
|
||||
question.content.rule.parentId = ""
|
||||
question.content.rule.main = []
|
||||
question.content.rule.default = ""
|
||||
})
|
||||
question.content.rule.parentId = "";
|
||||
question.content.rule.main = [];
|
||||
question.content.rule.default = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: value });
|
||||
};
|
||||
|
||||
|
||||
let UDTOABM: ReturnType<typeof setTimeout>;
|
||||
export const updateDesireToOpenABranchingModal = (contentId: string) => {
|
||||
useQuestionsStore.setState({ desireToOpenABranchingModal: contentId })
|
||||
clearTimeout(UDTOABM)
|
||||
UDTOABM = setTimeout(() => {
|
||||
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
|
||||
}, 7000)
|
||||
}
|
||||
export const clearDesireToOpenABranchingModal = () => {
|
||||
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
|
||||
}
|
||||
export const updateEditSomeQuestion = (contentId?: string) => {
|
||||
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId })
|
||||
}
|
||||
|
||||
export const createFrontResult = (quizId: number) => setProducedState(state => {
|
||||
export const createFrontResult = (quizId: number, parentContentId?: string) => setProducedState(state => {
|
||||
const frontId = nanoid();
|
||||
const content = JSON.parse(JSON.stringify(defaultQuestionByType["result"].content));
|
||||
content.id = frontId;
|
||||
if (parentContentId) content.rule.parentId = parentContentId;
|
||||
state.questions.push({
|
||||
id: nanoid(),
|
||||
id: frontId,
|
||||
quizId,
|
||||
type: "result",
|
||||
title: "",
|
||||
description: "",
|
||||
deleted: false,
|
||||
expanded: true,
|
||||
page: 101,
|
||||
required: true,
|
||||
content
|
||||
});
|
||||
}, {
|
||||
type: "createFrontResult",
|
||||
@ -494,7 +524,7 @@ export const createBackResult = async (
|
||||
) => requestQueue.enqueue(async () => {
|
||||
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
|
||||
if (!question) return;
|
||||
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
|
||||
if (question.type !== "result") throw new Error("Cannot upgrade already typed question");
|
||||
|
||||
try {
|
||||
const createdQuestion = await questionApi.create({
|
||||
@ -515,7 +545,7 @@ export const createBackResult = async (
|
||||
rawQuestionToQuestion(createdQuestion)
|
||||
);
|
||||
}, {
|
||||
type: "createTypedQuestion",
|
||||
type: "createBackResult",
|
||||
question,
|
||||
});
|
||||
} catch (error) {
|
||||
@ -523,9 +553,3 @@ export const createBackResult = async (
|
||||
enqueueSnackbar("Не удалось создать вопрос");
|
||||
}
|
||||
});
|
||||
|
||||
export const updateResult = () => {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ import useSWR from "swr";
|
||||
import { setQuestions } from "./actions";
|
||||
import { useQuestionsStore } from "./store";
|
||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
export function useQuestions() {
|
||||
|
@ -5,20 +5,10 @@ import { devtools } from "zustand/middleware";
|
||||
|
||||
export type QuestionsStore = {
|
||||
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
|
||||
openedModalSettingsId: string | null;
|
||||
dragQuestionContentId: string | null;
|
||||
openBranchingPanel: boolean;
|
||||
desireToOpenABranchingModal: string | null;
|
||||
editSomeQuestion: string | null;
|
||||
};
|
||||
|
||||
const initialState: QuestionsStore = {
|
||||
questions: [],
|
||||
openedModalSettingsId: null as null,
|
||||
dragQuestionContentId: null,
|
||||
openBranchingPanel: false,
|
||||
desireToOpenABranchingModal: null as null,
|
||||
editSomeQuestion: null as null,
|
||||
|
||||
};
|
||||
|
||||
|
@ -1,19 +1,28 @@
|
||||
import { create } from "zustand";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
import type { QuestionVariant } from "../model/questionTypes/shared";
|
||||
|
||||
type Answer = {
|
||||
questionId: string;
|
||||
answer: string;
|
||||
answer: string | string[];
|
||||
};
|
||||
|
||||
type OwnVariant = {
|
||||
contentId: string;
|
||||
variant: QuestionVariant;
|
||||
};
|
||||
|
||||
interface QuizViewStore {
|
||||
answers: Answer[];
|
||||
ownVariants: OwnVariant[];
|
||||
}
|
||||
|
||||
export const useQuizViewStore = create<QuizViewStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
answers: [],
|
||||
ownVariants: [],
|
||||
}),
|
||||
{
|
||||
name: "quizView",
|
||||
@ -21,9 +30,11 @@ export const useQuizViewStore = create<QuizViewStore>()(
|
||||
)
|
||||
);
|
||||
|
||||
export const updateAnswer = (questionId: string, answer: string) => {
|
||||
export const updateAnswer = (questionId: string, answer: string | string[]) => {
|
||||
const answers = [...useQuizViewStore.getState().answers];
|
||||
const answerIndex = answers.findIndex((answer) => questionId === answer.questionId);
|
||||
const answerIndex = answers.findIndex(
|
||||
(answer) => questionId === answer.questionId
|
||||
);
|
||||
|
||||
if (answerIndex < 0) {
|
||||
answers.push({ questionId, answer });
|
||||
@ -33,3 +44,53 @@ export const updateAnswer = (questionId: string, answer: string) => {
|
||||
|
||||
useQuizViewStore.setState({ answers });
|
||||
};
|
||||
|
||||
export const deleteAnswer = (questionId: string) => {
|
||||
const answers = [...useQuizViewStore.getState().answers];
|
||||
const filteredItems = answers.filter(
|
||||
(answer) => questionId !== answer.questionId
|
||||
);
|
||||
|
||||
useQuizViewStore.setState({ answers: filteredItems });
|
||||
};
|
||||
|
||||
export const updateOwnVariant = (contentId: string, answer: string) => {
|
||||
const ownVariants = [...useQuizViewStore.getState().ownVariants];
|
||||
const ownVariantIndex = ownVariants.findIndex(
|
||||
(variant) => variant.contentId === contentId
|
||||
);
|
||||
|
||||
if (ownVariantIndex < 0) {
|
||||
ownVariants.push({
|
||||
contentId,
|
||||
variant: {
|
||||
id: getRandom(),
|
||||
answer,
|
||||
extendedText: "",
|
||||
hints: "",
|
||||
originalImageUrl: "",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
ownVariants[ownVariantIndex].variant.answer = answer;
|
||||
}
|
||||
|
||||
useQuizViewStore.setState({ ownVariants });
|
||||
};
|
||||
|
||||
export const deleteOwnVariant = (contentId: string) => {
|
||||
const ownVariants = [...useQuizViewStore.getState().ownVariants];
|
||||
|
||||
const filteredOwnVariants = ownVariants.filter(
|
||||
(variant) => variant.contentId !== contentId
|
||||
);
|
||||
|
||||
useQuizViewStore.setState({ ownVariants: filteredOwnVariants });
|
||||
};
|
||||
|
||||
function getRandom() {
|
||||
const min = Math.ceil(1000000);
|
||||
const max = Math.floor(10000000);
|
||||
|
||||
return String(Math.floor(Math.random() * (max - min)) + min);
|
||||
}
|
||||
|