Merge branch 'dev' into 'main'

нельзя добавить больше 1 ребенка неветвящемуся, не отображаются шестерёнки у...

See merge request frontend/squiz!93
This commit is contained in:
Nastya 2023-12-27 21:32:25 +00:00
commit 21f0f395f5
79 changed files with 3349 additions and 2337 deletions

@ -16,15 +16,61 @@ import { ResultSettings } from "./pages/ResultPage/ResultSettings";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import Main from "./pages/main";
import EditPage from "./pages/startPage/EditPage";
import { clearAuthToken, getMessageFromFetchError, useUserAccountFetcher, useUserFetcher } from "@frontend/kitui";
import { clearAuthToken, getMessageFromFetchError, useUserFetcher, UserAccount, makeRequest, devlog, createUserAccount } from "@frontend/kitui";
import { clearUserData, setUser, setUserAccount, useUserStore } from "@root/user";
import { enqueueSnackbar } from "notistack";
import PrivateRoute from "@ui_kit/PrivateRoute";
import { Restore } from "./pages/startPage/Restore";
import { isAxiosError } from "axios";
import { useEffect, useLayoutEffect, useRef } from "react";
export function useUserAccountFetcher({ onError, onNewUserAccount, url, userId }: {
url: string;
userId: string | null;
onNewUserAccount: (response: UserAccount) => void;
onError?: (error: any) => void;
}) {
const onNewUserAccountRef = useRef(onNewUserAccount);
const onErrorRef = useRef(onError);
useLayoutEffect(() => {
onNewUserAccountRef.current = onNewUserAccount;
onErrorRef.current = onError;
}, [onError, onNewUserAccount]);
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
makeRequest<never, UserAccount>({
url,
contentType: true,
method: "GET",
useToken: true,
withCredentials: false,
signal: controller.signal,
}).then(result => {
devlog("User account", result);
onNewUserAccountRef.current(result);
}).catch(error => {
devlog("Error fetching user account", error);
if (isAxiosError(error) && error.response?.status === 404) {
createUserAccount(controller.signal, url.replace('get','create')).then(result => {
devlog("Created user account", result);
onNewUserAccountRef.current(result);
}).catch(error => {
devlog("Error creating user account", error);
onErrorRef.current?.(error);
});
} else {
onErrorRef.current?.(error);
}
});
return () => controller.abort();
}, [url, userId]);
}
dayjs.locale("ru");
const routeslink = [
{ path: "/list", page: <MyQuizzesFull />, header: false, sidebar: false },
{ path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true },
@ -53,7 +99,7 @@ export default function App() {
});
useUserAccountFetcher({
url: "https://squiz.pena.digital/customer/account",
url: "https://squiz.pena.digital/squiz/account/get",
userId,
onNewUserAccount: setUserAccount,
onError: (error) => {

@ -1,7 +1,12 @@
import { Box, useTheme } from "@mui/material";
import {Box, SxProps, Theme, useTheme} from "@mui/material";
export default function ArrowDownIcon(props: any) {
interface Color{
color?: string
}
export default function ArrowDownIcon(
props: any,
{color = "#7E2AEA"}: Color
) {
const theme = useTheme();
return (
@ -17,7 +22,7 @@ export default function ArrowDownIcon(props: any) {
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M19.5 9L12 16.5L4.5 9" stroke={theme.palette.brightPurple.main} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M19.5 9L12 16.5L4.5 9" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -0,0 +1,11 @@
export const BackButtonIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M7.86747 18C8.26104 18 9.9253 18 13.4337 18C17.8193 18 19 14.703 19 13.2194C19 11.7358 17.8193 8.93333 13.4337 8.93333C10.1773 8.93333 6.59726 8.93333 5 8.93333M5 8.93333L7.86747 6M5 8.93333L7.86747 11.8182"
stroke="white"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);

@ -1,6 +1,9 @@
import { Box } from "@mui/material";
import {Box, SxProps, Theme} from "@mui/material";
export default function CalendarIcon() {
interface Props {
sx?: SxProps<Theme>
}
export default function CalendarIcon({sx}:Props) {
return (
<Box
sx={{
@ -20,6 +23,7 @@ export default function CalendarIcon() {
"&:active rect": {
stroke: "#FB5607",
},
...sx
}}
>
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">

@ -1,10 +1,11 @@
import { Box, useTheme } from "@mui/material";
type CheckboxIconProps = {
interface CheckboxIconProps {
checked?: boolean;
color?: string;
};
export const CheckboxIcon = ({ checked = false }: CheckboxIconProps) => {
export default function CheckboxIcon ({ checked = false, color = "#7E2AEA", }: CheckboxIconProps) {
const theme = useTheme();
return (
@ -17,9 +18,9 @@ export const CheckboxIcon = ({ checked = false }: CheckboxIconProps) => {
justifyContent: "center",
alignItems: "center",
backgroundColor: checked
? theme.palette.brightPurple.main
: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
? color
: "#F2F3F7",
border: `1px solid #9A9AAF`,
}}
>
{checked && (

@ -2,14 +2,7 @@ import { FC, SVGProps } from "react";
export const CrossedEyeIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
{...props}
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg {...props} width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.8125" width="30" height="30" rx="6" fill="#FFF" />
<path
d="M7.5 7.5625L22.5 24.0625"

@ -1,6 +1,5 @@
import { Box, useTheme } from "@mui/material";
export default function EyeIcon() {
const theme = useTheme();
@ -15,8 +14,18 @@ export default function EyeIcon() {
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M9 3.9375C3.375 3.9375 1.125 9 1.125 9C1.125 9 3.375 14.0625 9 14.0625C14.625 14.0625 16.875 9 16.875 9C16.875 9 14.625 3.9375 9 3.9375Z" stroke={theme.palette.brightPurple.main} strokeLinecap="round" strokeLinejoin="round" />
<path d="M9 11.8125C10.5533 11.8125 11.8125 10.5533 11.8125 9C11.8125 7.4467 10.5533 6.1875 9 6.1875C7.4467 6.1875 6.1875 7.4467 6.1875 9C6.1875 10.5533 7.4467 11.8125 9 11.8125Z" stroke={theme.palette.brightPurple.main} strokeLinecap="round" strokeLinejoin="round" />
<path
d="M9 3.9375C3.375 3.9375 1.125 9 1.125 9C1.125 9 3.375 14.0625 9 14.0625C14.625 14.0625 16.875 9 16.875 9C16.875 9 14.625 3.9375 9 3.9375Z"
stroke={theme.palette.brightPurple.main}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9 11.8125C10.5533 11.8125 11.8125 10.5533 11.8125 9C11.8125 7.4467 10.5533 6.1875 9 6.1875C7.4467 6.1875 6.1875 7.4467 6.1875 9C6.1875 10.5533 7.4467 11.8125 9 11.8125Z"
stroke={theme.palette.brightPurple.main}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -0,0 +1,27 @@
import { FC, SVGProps } from "react";
export const LinkSimple: FC<SVGProps<SVGSVGElement>> = (props) => (
<svg {...props} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M8.82031 15.1781L15.1766 8.8125"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M13.5949 16.7719L10.9418 19.425C10.5238 19.843 10.0276 20.1745 9.48151 20.4007C8.93541 20.6269 8.35009 20.7434 7.75899 20.7434C6.5652 20.7434 5.42031 20.2691 4.57618 19.425C3.73204 18.5809 3.25781 17.436 3.25781 16.2422C3.25781 15.0484 3.73204 13.9035 4.57618 13.0594L7.2293 10.4062"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M16.7719 13.5937L19.425 10.9406C20.2691 10.0965 20.7434 8.95159 20.7434 7.7578C20.7434 6.56401 20.2691 5.41912 19.425 4.57499C18.5809 3.73085 17.436 3.25662 16.2422 3.25662C15.0484 3.25662 13.9035 3.73085 13.0594 4.57499L10.4062 7.22811"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);

@ -12,7 +12,7 @@ export const NameplateLogo: FC<SVGProps<SVGSVGElement>> = (props) => (
<circle cx="54.4046" cy="12.3947" r="2.1546" fill="#7E2AEA" />
<path
d="M88.866 39.4685C88.2378 33.3607 85.3643 27.7037 80.8025 23.594C76.2408 19.4843 70.3156 17.2146 64.1757 17.2248C63.3039 17.2252 62.4328 17.2708 61.5658 17.3614C55.4608 18.0025 49.8093 20.8814 45.7015 25.443C41.5937 30.0046 39.3205 35.9256 39.3203 42.0642V42.0642V77.549H49.9658V62.468C54.128 65.3636 59.0787 66.9119 64.1491 66.9036C65.0208 66.9033 65.8919 66.8577 66.759 66.767C70.0031 66.426 73.1483 65.4494 76.0151 63.8929C78.8818 62.3364 81.4138 60.2305 83.4667 57.6955C85.5195 55.1604 87.0529 52.2458 87.9793 49.1181C88.9058 45.9904 89.2071 42.7109 88.866 39.4667V39.4685ZM75.1937 51.0011C74.0243 52.4537 72.5783 53.6599 70.9395 54.5498C69.3007 55.4397 67.5017 55.9956 65.6465 56.1854C65.149 56.2371 64.6492 56.2631 64.1491 56.2635C60.9296 56.2605 57.8068 55.1631 55.2932 53.1515C52.7796 51.1398 51.0245 48.3334 50.3161 45.1929C49.6077 42.0523 49.988 38.7642 51.3945 35.8683C52.8011 32.9723 55.1504 30.6406 58.0568 29.2558C60.9632 27.871 64.2541 27.5154 67.3892 28.2473C70.5244 28.9793 73.3176 30.7553 75.3103 33.284C77.303 35.8126 78.3769 38.9436 78.3558 42.1629C78.3346 45.3823 77.2196 48.4989 75.1937 51.0011Z"
fill="#151515"
fill={"currentColor" || "#151515"}
/>
</svg>
);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,7 +1,10 @@
import { Box, useTheme } from "@mui/material";
interface Props{
color?: string
}
export default function UploadIcon() {
export default function UploadIcon({color= "#9A9AAF"}: Props) {
const theme = useTheme();
return (
@ -15,9 +18,9 @@ export default function UploadIcon() {
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M10.75 10.25L16 5L21.25 10.25" stroke={theme.palette.grey2.main} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 19V5" stroke={theme.palette.grey2.main} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M27 19V26C27 26.2652 26.8946 26.5196 26.7071 26.7071C26.5196 26.8946 26.2652 27 26 27H6C5.73478 27 5.48043 26.8946 5.29289 26.7071C5.10536 26.5196 5 26.2652 5 26V19" stroke={theme.palette.grey2.main} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M10.75 10.25L16 5L21.25 10.25" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 19V5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M27 19V26C27 26.2652 26.8946 26.5196 26.7071 26.7071C26.5196 26.8946 26.2652 27 26 27H6C5.73478 27 5.48043 26.8946 5.29289 26.7071C5.10536 26.5196 5 26.2652 5 26V19" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);

@ -10,11 +10,11 @@ export const QUIZ_QUESTION_NUMBER: Omit<QuizQuestionNumber, "id" | "backendId">
required: false,
innerNameCheck: false,
innerName: "",
range: "1—100",
range: "0—100",
defaultValue: 0,
step: 1,
steps: 5,
start: 50,
start: 0,
chooseRange: false,
form: "star",
},

@ -18,12 +18,11 @@ export interface QuestionBranchingRuleMain {
or: boolean;
rules: {
question: string; //id родителя (пока что)
answers: string[]
}[]
answers: string[];
}[];
}
export interface QuestionBranchingRule {
children: string[],
children: string[];
//список условий
main: QuestionBranchingRuleMain[];
parentId: string | null | "root";
@ -96,23 +95,27 @@ export type AnyTypedQuizQuestion =
| QuizQuestionRating
| QuizQuestionResult;
type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[]; };
} ? T : never;
content: { variants: QuestionVariant[] };
}
? T
: never;
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
export const createBranchingRuleMain: (targetId: string, parentId: string) => QuestionBranchingRuleMain = (
targetId,
parentId
) => ({
next: targetId,
or: false,
rules: [{
rules: [
{
question: parentId,
answers: [] as string[],
}]
})
},
],
});
export const createQuestionVariant: () => QuestionVariant = () => ({
id: nanoid(),
answer: "",

@ -33,6 +33,7 @@ export interface QuizConfig {
startpageType: QuizStartpageType;
results: QuizResultsType;
haveRoot: string | null;
theme: "StandardTheme" | "StandardDarkTheme" | "PinkTheme" | "PinkDarkTheme" | "BlackWhiteTheme" | "OliveTheme" | "YellowTheme" | "GoldDarkTheme" | "PurpleTheme" | "BlueTheme" | "BlueDarkTheme";
resultInfo: {
when: 'before' | 'after' | 'email',
share: true | false,
@ -95,6 +96,7 @@ export const defaultQuizConfig: QuizConfig = {
startpageType: null,
results: null,
haveRoot: null,
theme: "StandardTheme",
resultInfo: {
when: 'after',
share: false,

@ -56,9 +56,9 @@ function CsComponent({
}: CsComponentProps) {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic } = useUiTools()
const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic, someWorkBackend } = useUiTools()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null && !question.deleted)
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
@ -87,13 +87,6 @@ function CsComponent({
gearsContainer,
});
useEffect(() => {
return () => {
// if (!canCreatePublic) updateModalInfoWhyCantCreate(true)
}
}, []);
useLayoutEffect(() => {
const cy = cyRef?.current
if (desireToOpenABranchingModal) {
@ -104,11 +97,14 @@ function CsComponent({
cy?.elements().data("eroticeyeblink", false)
}
}, [desireToOpenABranchingModal])
//Техническая штучка. Гарантирует не отрисовку модалки по первому входу на страничку. И очистка данных по расскоменчиванию
//Быстро просто дешево и сердито :)
useLayoutEffect(() => {
updateOpenedModalSettingsId()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, [])
//Отлов mouseup для отрисовки ноды
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId })
@ -119,24 +115,39 @@ function CsComponent({
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
if (quiz) {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
const parentQuestion = getQuestionByContentId(parentNodeContentId)
//Нельзя добавлять больше 1 ребёнка вопросам типа страница, ползунок, своё поле для ввода и дата
if ((parentQuestion?.type === "date" || parentQuestion?.type === "text" || parentQuestion?.type === "number" || parentQuestion?.type === "page")
&& parentQuestion.content.rule.children.length === 1
) {
console.log(parentQuestion.content.rule.children)
console.log("parentQuestion.content.rule.children.length === 1",parentQuestion.content.rule.children.length === 1)
enqueueSnackbar("у вопроса этого типа может быть только 1 потомок")
return
}
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || dragQuestionContentId) } as AnyTypedQuizQuestion
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed', true)
createResult(quiz.backendId, targetQuestion.content.id)
console.log("Я собираюсь добавить узел из такого вопроса ", targetQuestion)
console.log("Я собираюсь добавить узел у которого папаша вот такой ", parentNodeContentId)
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title,
parentType: parentNodeContentId
}
},
{
@ -188,14 +199,12 @@ function CsComponent({
})
if (!noChild) {//детей больше 1
console.log("детей ", noChild, " открываем модалку ветвления")
//- предупреждаем стор вопросов об открытии модалки ветвления
updateOpenedModalSettingsId(targetQuestion.content.id)
}
}
useEffect(() => {
if (startCreate) {
addNode({ parentNodeContentId: startCreate });
@ -211,11 +220,22 @@ function CsComponent({
}
}, [startRemove]);
//Отработка первичного рендера странички графика
const firstRender = useRef(true)
useEffect(() => {
console.log("____________ПЕРВЧИНЫЙ РЕНДЕР____________")
console.log("______someWorkBackend______", someWorkBackend)
if (!someWorkBackend && firstRender.current) {
console.log("цс первично отрабатывает")
document
.querySelector("#root")
?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
console.log("СПИСОК ЭЛЕМЕНТОВ ЦИТОСКЕЙПА В ПЕРВЧИНЫЙ РЕНДЕР")
console.log(cy?.elements())
const eles = cy?.add(
storeToNodes(
questions.filter(
@ -229,8 +249,11 @@ function CsComponent({
cy?.on("add", () => cy.data("changed", true));
cy?.fit();
//cy?.layout().run()
firstRender.current = false
}
return () => {
console.log("разрендер")
document
.querySelector("#root")
?.removeEventListener("mouseup", cleardragQuestionContentId);
@ -239,7 +262,7 @@ function CsComponent({
crossesContainer.current?.remove();
gearsContainer.current?.remove();
};
}, []);
}, [someWorkBackend]);
return (

@ -17,6 +17,7 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
useLayoutEffect(() => {
console.log("компонент с плюсом")
updateOpenedModalSettingsId()
updateRootContentId(quiz.id, "")
clearRuleForAll()

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

@ -7,6 +7,7 @@ import type {
NodeSingular,
AbstractEventObject,
} from "cytoscape";
import { getQuestionByContentId } from "@root/questions/actions";
type usePopperArgs = {
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
@ -111,6 +112,8 @@ export const usePopper = ({
);
});
cy?.removeAllListeners()
nodesInView.toArray()?.forEach((item) => {
const node = item as NodeSingularWithPopper;
@ -199,6 +202,9 @@ export const usePopper = ({
});
let gearsPopper: Popper | null = null;
if (node.data().root !== true) {
console.log(node.data("parentType"))
const parentQuestion = getQuestionByContentId(node.data("parentType"))
gearsPopper = node.popper({
popper: {
placement: "left",
@ -223,6 +229,10 @@ export const usePopper = ({
updateOpenedModalSettingsId(item.id());
});
if (parentQuestion?.type === "date" || parentQuestion?.type === "text" || parentQuestion?.type === "number" || parentQuestion?.type === "page") {
gearElement.classList.add("popper-gear-none");
}
return gearElement;
},
});

@ -8,6 +8,7 @@ import { useUiTools } from "@root/uiTools/store";
export const BranchingMap = () => {
const quiz = useCurrentQuiz();
console.log("рендер странички ветвления")
const { dragQuestionContentId } = useUiTools();
const [modalQuestionParentContentId, setModalQuestionParentContentId] =
useState<string>("");

@ -43,3 +43,7 @@
background-repeat: no-repeat;
background-size: contain;
}
#popper-gears > .popper-gear-none {
display: none
}

@ -27,6 +27,8 @@ import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useUiTools } from "@root/uiTools/store";
import {useState} from "react";
import { updateSomeWorkBackend } from "@root/uiTools/actions";
import { DeleteFunction } from "@utils/deleteFunc";
interface Props {
switchState: string;
@ -52,61 +54,6 @@ export default function ButtonsOptions({
updateDesireToOpenABranchingModal(question.content.id);
};
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.type !== null && targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type !== "result" && targetQuestion.type !== null) {
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 = 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);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id);
const result = questions.find(q => q.type === "result" && q.content.rule.parentId === question.content.id)
if (result) deleteQuestion(result.id);
} else {
deleteQuestion(question.id);
}
};
const buttonSetting: {
icon: JSX.Element;
title: string;
@ -171,53 +118,57 @@ export default function ButtonsOptions({
>
{buttonSetting.map(({ icon, title, value, myFunc }) => (
<Box key={value}>
{value === "branching" ? (
<Tooltip
arrow
placement="right"
componentsProps={{
tooltip: {
sx: {
background: "#fff",
borderRadius: "6px",
color: "#9A9AAF",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
"& .MuiTooltip-arrow": {
color: "#FFF",
},
},
},
}}
title={
<Box>
<Typography
sx={{
color: "#4D4D4D",
fontWeight: "bold",
fontSize: "14px",
marginBottom: "10px",
}}
>
Будет показан при условии
</Typography>
<Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>
Название
</Typography>
<Typography
sx={{
fontWeight: "bold",
fontSize: "12px",
marginBottom: "10px",
}}
>
Условие 1, Условие 2
</Typography>
<Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>
Все условия обязательны
</Typography>
</Box>
}
>
{value === "branching" ?
question.type === "page" || question.type === "text" || question.type === "date" || question.type === "number" ?
<></>
:
(
// <Tooltip
// arrow
// placement="right"
// componentsProps={{
// tooltip: {
// sx: {
// background: "#fff",
// borderRadius: "6px",
// color: "#9A9AAF",
// boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
// "& .MuiTooltip-arrow": {
// color: "#FFF",
// },
// },
// },
// }}
// title={
// <Box>
// <Typography
// sx={{
// color: "#4D4D4D",
// fontWeight: "bold",
// fontSize: "14px",
// marginBottom: "10px",
// }}
// >
// Будет показан при условии
// </Typography>
// <Typography sx={{ fontWeight: "bold", fontSize: "12px" }}>
// Название
// </Typography>
// <Typography
// sx={{
// fontWeight: "bold",
// fontSize: "12px",
// marginBottom: "10px",
// }}
// >
// Условие 1, Условие 2
// </Typography>
// <Typography sx={{ color: "#7E2AEA", fontSize: "12px" }}>
// Все условия обязательны
// </Typography>
// </Box>
// }
// >
<MiniButtonSetting
key={title}
onClick={() => {
@ -246,7 +197,7 @@ export default function ButtonsOptions({
{icon}
{isWrappMiniButtonSetting ? null : title}
</MiniButtonSetting>
</Tooltip>
// </Tooltip>
) : (
<>
<MiniButtonSetting
@ -279,7 +230,7 @@ export default function ButtonsOptions({
)}
</Box>
))}
<>
{/* <>
<MiniButtonSetting
onClick={undefined} // TODO
sx={{
@ -310,7 +261,7 @@ export default function ButtonsOptions({
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
</>
</> */}
</Box>
<Box
sx={{
@ -334,7 +285,7 @@ export default function ButtonsOptions({
if(question.content.rule.parentId.length !== 0) {
setOpenDelete(true)
} else {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}
}}
@ -376,7 +327,7 @@ export default function ButtonsOptions({
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}}
>
Подтвердить

@ -29,6 +29,8 @@ import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
import {AnyTypedQuizQuestion} from "@model/questionTypes/shared";
import { updateSomeWorkBackend } from "@root/uiTools/actions";
import { DeleteFunction } from "@utils/deleteFunc";
interface Props {
@ -53,60 +55,6 @@ export default function ButtonsOptionsAndPict({
const [openDelete, setOpenDelete] = useState<boolean>(false);
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
} 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 (targetQuestion.type !== null && targetQuestion.type !== "result") {
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 = 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);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id);
const result = questions.find(q => q.type === "result" && q.content.rule.parentId === question.content.id)
if (result) deleteQuestion(result.id);
} else {
deleteQuestion(question.id);
}
};
useEffect(() => {
if (question.deleteTimeoutId) {
clearTimeout(question.deleteTimeoutId);
@ -199,7 +147,7 @@ export default function ButtonsOptionsAndPict({
{isIconMobile ? null : "Подсказка"}
</MiniButtonSetting>
<>
<Tooltip
{/* <Tooltip
arrow
placement="right"
componentsProps={{
@ -244,7 +192,7 @@ export default function ButtonsOptionsAndPict({
</Typography>
</Box>
}
>
> */}
<MiniButtonSetting
onMouseEnter={() => setButtonHover("branching")}
onMouseLeave={() => setButtonHover("")}
@ -284,7 +232,7 @@ export default function ButtonsOptionsAndPict({
/>
{isIconMobile ? null : "Ветвление"}
</MiniButtonSetting>
</Tooltip>
{/* </Tooltip> */}
<MiniButtonSetting
onMouseEnter={() => setButtonHover("image")}
onMouseLeave={() => setButtonHover("")}
@ -318,7 +266,7 @@ export default function ButtonsOptionsAndPict({
/>
{isIconMobile ? null : "Изображение"}
</MiniButtonSetting>
<MiniButtonSetting
{/* <MiniButtonSetting
onClick={() => setOpenedReallyChangingModal(true)}
sx={{
minWidth: "30px",
@ -347,7 +295,7 @@ export default function ButtonsOptionsAndPict({
}}
>
<VectorQuestions style={{ color: "#FC712F", fontSize: "9px" }} />
</MiniButtonSetting>
</MiniButtonSetting> */}
</>
</Box>
<Box
@ -370,7 +318,7 @@ export default function ButtonsOptionsAndPict({
if(question.content.rule.parentId.length !== 0) {
setOpenDelete(true)
} else {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}
}}
data-cy="delete-question"
@ -411,7 +359,7 @@ export default function ButtonsOptionsAndPict({
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}}
>
Подтвердить

@ -14,7 +14,7 @@ import { getQuestionByContentId } from "@root/questions/actions";
import { updateDeleteId } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { CheckboxIcon } from "@icons/Checkbox";
import CheckboxIcon from "@icons/Checkbox";
type DeleteNodeModalProps = {
removeNode?: (id: string) => void;

@ -12,11 +12,13 @@ import {
Typography,
useTheme,
} from "@mui/material";
import { changeQuestionType } from "@root/questions/actions";
import { changeQuestionType, updateQuestion } from "@root/questions/actions";
import type { RefObject } from "react";
import { useState } from "react";
import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../model/questionTypes/shared";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import { useQuestionsStore } from "@root/questions/store";
import { updateSomeWorkBackend } from "@root/uiTools/actions";
type ChooseAnswerModalProps = {
@ -36,6 +38,7 @@ export const ChooseAnswerModal = ({
}: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuestionType>("text");
const { questions } = useQuestionsStore()
const theme = useTheme();
return (
@ -94,8 +97,8 @@ export const ChooseAnswerModal = ({
background: "#FFFFFF",
}}
>
<Typography variant="h6" sx={{textAlign: "center"}}>
Все настройки, кроме заголовка вопроса будут сброшены <br/>
<Typography variant="h6" sx={{ textAlign: "center" }}>
Все настройки, кроме заголовка вопроса будут сброшены <br />
(вопрос всё ещё будет участвовать в ветвлении, но его условия будут сброшены)
</Typography>
<Box
@ -116,9 +119,44 @@ export const ChooseAnswerModal = ({
<Button
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
onClick={async () => {
updateSomeWorkBackend(true)
setOpenModal(false);
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: any) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.type !== null && targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type !== "result" && targetQuestion.type !== null) {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
updateQuestion(question.id, q => {
q.content.rule.parentId = "";
q.content.rule.main = [];
q.content.rule.children = [];
q.content.rule.default = "";
});
//чистим потомков от инфы ветвления
await Promise.allSettled(
clearQuestions.map((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.children = [];
question.content.rule.default = "";
});
})
)
changeQuestionType(question.id, selectedValue);
updateSomeWorkBackend(false)
}}
>
Подтвердить

@ -29,7 +29,6 @@ function DraggableListItem({ question, isDragging, index }: Props) {
updateEditSomeQuestion();
}
}, 200);
}
}, [editSomeQuestion]);

@ -55,6 +55,8 @@ import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
import { updateSomeWorkBackend } from "@root/uiTools/actions";
import { DeleteFunction } from "@utils/deleteFunc";
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -87,61 +89,6 @@ const maxLengthTextField = 225;
});
}, 200);
const deleteFn = () => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
clearRuleForAll();
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.type !== null && targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type !== null && targetQuestion.type !== "result") {
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 = 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);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
deleteQuestion(question.id);
}
deleteQuestion(question.id);
const result = questions.find(q => q.type === "result" && q.content.rule.parentId === question.content.id)
if (result) deleteQuestion(result.id);
} else {
deleteQuestion(question.id);
}
};
const handleInputFocus = () => {
setIsTextFieldtActive(true);
};
@ -333,7 +280,7 @@ const maxLengthTextField = 225;
if (question.content.rule.parentId.length !== 0) {
setOpenDelete(true);
} else {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}
}}
data-cy="delete-question"
@ -371,7 +318,7 @@ const maxLengthTextField = 225;
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(question.id, deleteFn);
deleteQuestionWithTimeout(question.id, () => DeleteFunction(questions, question, quiz));
}}
>
Подтвердить

@ -12,21 +12,24 @@ import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider";
import {
Box, Checkbox,
Box,
Checkbox,
FormControl,
FormControlLabel,
IconButton,
InputAdornment,
Paper, TextField,
Paper,
TextField,
useMediaQuery,
useTheme
useTheme,
} from "@mui/material";
import {
copyQuestion,
deleteQuestion, deleteQuestionWithTimeout,
deleteQuestion,
deleteQuestionWithTimeout,
toggleExpandQuestion,
updateQuestion,
updateUntypedQuestion
updateUntypedQuestion,
} from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import { useRef, useState } from "react";
@ -110,15 +113,13 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
<TextField
placeholder={`Заголовок ${questionIndex + 1} вопроса`}
value={question.title}
onChange={({target}) => setTitle(target.value)}
onChange={({ target }) => setTitle(target.value)}
sx={{
width: "100%",
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: question.expanded
? theme.palette.background.default
: "transparent",
backgroundColor: question.expanded ? theme.palette.background.default : "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
@ -137,7 +138,7 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
<InputAdornment
ref={anchorRef}
position="start"
sx={{cursor: "pointer"}}
sx={{ cursor: "pointer" }}
onClick={() => setOpen((isOpened) => !isOpened)}
>
{IconAndrom(question.type)}
@ -172,7 +173,7 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
}}
>
<IconButton
sx={{padding: "0", margin: "5px"}}
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => toggleExpandQuestion(question.id)}
@ -235,13 +236,8 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
userSelect: "none",
}}
/>
<IconButton
sx={{ padding: "0" }}
onClick={() => copyQuestion(question.id, question.quizId)}
>
<CopyIcon
style={{ color: theme.palette.brightPurple.main }}
/>
<IconButton sx={{ padding: "0" }} onClick={() => copyQuestion(question.id, question.quizId)}>
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
<IconButton
sx={{
@ -251,13 +247,10 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
margin: "0 5px 0 10px",
}}
onClick={() => {
deleteQuestionWithTimeout(question.id, deleteQuestion(question.id));
deleteQuestionWithTimeout(question.id, () => deleteQuestion(question.id));
}}
>
<DeleteIcon
style={{ color: theme.palette.brightPurple.main }}
/>
<DeleteIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
</Box>
)}
@ -289,16 +282,16 @@ export default function QuestionsPageCard({ question, questionIndex, draggablePr
}}
{...draggableProps}
>
<PointsIcon style={{color: "#4D4D4D", fontSize: "30px"}}/>
<PointsIcon style={{ color: "#4D4D4D", fontSize: "30px" }} />
</IconButton>
</Box>
</Box>
{question.expanded && (
<>
{question.type === null ? (
<FormTypeQuestions question={question}/>
<FormTypeQuestions question={question} />
) : (
<SwitchQuestionsPage question={question}/>
<SwitchQuestionsPage question={question} />
)}
</>
)}

@ -1,18 +1,13 @@
import { VideofileIcon } from "@icons/questionsPage/VideofileIcon";
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import ButtonsOptions from "../ButtonsOptions";
import { UploadImageModal } from "../UploadImage/UploadImageModal";
import { UploadVideoModal } from "../UploadVideoModal";
import SwitchPageOptions from "./switchPageOptions";
import { useDisclosure } from "../../../utils/useDisclosure";
import { MediaSelectionAndDisplay } from "@ui_kit/MediaSelectionAndDisplay";
type Props = {
disableInput?: boolean;
@ -20,16 +15,11 @@ type Props = {
};
export default function PageOptions({ disableInput, question }: Props) {
const [openVideoModal, setOpenVideoModal] = useState<boolean>(false);
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(980));
const isFigmaTablet = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(780));
const quizQid = useCurrentQuiz()?.qid;
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const setText = useDebouncedCallback((value) => {
updateQuestion(question.id, (question) => {
@ -43,25 +33,6 @@ export default function PageOptions({ disableInput, question }: Props) {
setSwitchState(data);
};
async function handleImageUpload(file: File) {
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (question.type !== "page") return;
question.content.picture = url;
question.content.originalPicture = url;
});
closeImageUploadModal();
openCropModal(file, url);
}
function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
if (question.type !== "page") return;
question.content.picture = url;
});
}
return (
<>
<Box
@ -71,7 +42,6 @@ export default function PageOptions({ disableInput, question }: Props) {
display: "flex",
px: "20px",
flexDirection: "column",
gap: isMobile ? "25px" : "20px",
}}
>
<Box sx={{ display: disableInput ? "none" : "", mt: isMobile ? "15px" : "0px" }}>
@ -82,179 +52,7 @@ export default function PageOptions({ disableInput, question }: Props) {
/>
</Box>
<Box
sx={{
mb: "20px",
ml: isTablet ? "0px" : "60px",
display: "flex",
alignItems: "center",
gap: "28px",
justifyContent: isMobile ? "space-between" : null,
}}
>
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
<AddOrEditImageButton
imageSrc={question.content.picture}
onImageClick={() => {
if (question.content.picture) {
return openCropModal(question.content.picture, question.content.originalPicture);
}
openImageUploadModal();
}}
onPlusClick={() => {
openImageUploadModal();
}}
/>
<Typography
sx={{
display: isMobile ? "none" : "block",
fontWeight: 400,
fontSize: "16px",
lineHeight: "18.96px",
color: question.content.useImage ? "#7E2AEA" : "#9A9AAF",
}}
onClick={() =>
updateQuestion(question.id, (question) => ((question as QuizQuestionPage).content.useImage = true))
}
>
Изображение
</Typography>
</Box>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
<Typography> или</Typography>
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{isMobile ? (
<Box
sx={{
display: "flex",
alignItems: "center",
width: "120px",
position: "relative",
}}
>
<Box
sx={{
width: "100%",
background: "#EEE4FC",
height: "40px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderTopLeftRadius: "4px",
borderBottomLeftRadius: "4px",
}}
>
<VideofileIcon
style={{
color: "#7E2AEA",
fontSize: "20px",
}}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "20px",
background: "#EEE4FC",
height: "40px",
color: "white",
backgroundColor: "#7E2AEA",
borderTopRightRadius: "4px",
borderBottomRightRadius: "4px",
}}
>
+
</Box>
</Box>
) : (
<Box
sx={{
width: "60px",
height: "40px",
background: "#EEE4FC",
display: "flex",
justifyContent: "space-between",
}}
>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%" }}>
<VideofileIcon fontSize="22px" color="#7E2AEA" />
</Box>
<span
onClick={() => setOpenVideoModal(true)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#7E2AEA",
height: "100%",
width: "25px",
color: "white",
fontSize: "15px",
}}
>
+
</span>
</Box>
)}
<Typography
sx={{
display: isMobile ? "none" : "block",
fontWeight: 400,
fontSize: "16px",
lineHeight: "18.96px",
color: question.content.useImage ? "#9A9AAF" : "#7E2AEA",
}}
onClick={() =>
updateQuestion(question.id, (question) => ((question as QuizQuestionPage).content.useImage = false))
}
>
Видео
</Typography>
</Box>
<UploadVideoModal
open={openVideoModal}
onClose={() => setOpenVideoModal(false)}
video={question.content.video}
onUpload={(url) => {
updateQuestion(question.id, (question) => {
if (question.type !== "page") return;
question.content.video = url;
});
}}
/>
</Box>
<MediaSelectionAndDisplay resultData={question} />
</Box>
<ButtonsOptions switchState={switchState} SSHC={SSHC} question={question} />
<SwitchPageOptions switchState={switchState} question={question} />

@ -1,26 +1,47 @@
import {
Box, useMediaQuery, useTheme,
} from "@mui/material";
import { useEffect, useLayoutEffect } from "react";
import { 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 { useQuestionsStore } from "@root/questions/store";
import { useUiTools } from "@root/uiTools/store";
import { useQuestions } from "@root/questions/hooks";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions";
export const QuestionSwitchWindowTool = () => {
const {questions} = useQuestionsStore.getState()
const {openBranchingPanel} = useUiTools()
interface Props {
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
}
export const QuestionSwitchWindowTool = ({ openBranchingPage, setOpenBranchingPage }: Props) => {
const { questions } = useQuestionsStore.getState();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const quiz = useCurrentQuiz();
console.log("Я компонент в котором отвечала");
const openBranchingPageHC = () => {
if (!openBranchingPage) {
deleteTimeoutedQuestions(questions, quiz);
}
setOpenBranchingPage(!openBranchingPage);
};
return (
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap", marginBottom: isMobile ? "20px" : undefined }}>
<Box sx={{ flexBasis: "796px" }}>
{openBranchingPanel? <BranchingMap /> : <DraggableList />}
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
marginBottom: isMobile ? "20px" : undefined,
}}
>
<Box sx={{ flexBasis: "796px" }}>{openBranchingPage ? <BranchingMap /> : <DraggableList />}</Box>
{openBranchingPage && (
<SwitchBranchingPanel openBranchingPage={openBranchingPage} setOpenBranchingPage={openBranchingPageHC} />
)}
</Box>
<SwitchBranchingPanel
/>
</Box>
)
}
);
};

@ -13,7 +13,12 @@ import { useQuestionsStore } from "@root/questions/store";
import { updateOpenBranchingPanel, updateEditSomeQuestion } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
export default function QuestionsPage() {
interface Props {
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
}
export default function QuestionsPage({ openBranchingPage, setOpenBranchingPage }: Props) {
const theme = useTheme();
const { openedModalSettingsId, openBranchingPanel } = useUiTools();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
@ -55,7 +60,7 @@ export default function QuestionsPage() {
Свернуть всё
</Button>
</Box>
<QuestionSwitchWindowTool />
<QuestionSwitchWindowTool openBranchingPage={openBranchingPage} setOpenBranchingPage={setOpenBranchingPage} />
<Box
sx={{
display: "flex",

@ -4,7 +4,7 @@ import {
MenuItem,
FormControl,
Typography,
useTheme,
useTheme, Theme,
} from "@mui/material";
import ArrowDown from "@icons/ArrowDownIcon";
@ -18,6 +18,9 @@ type SelectProps = {
onChange?: (item: string, num: number) => void;
sx?: SxProps;
placeholder?: string;
colorPlaceholder?: string;
colorMain?: string;
color?: string;
};
export const Select = ({
@ -27,6 +30,9 @@ export const Select = ({
onChange,
sx,
placeholder = "",
colorMain = "#7E2AEA",
colorPlaceholder = "#9A9AAF",
color
}: SelectProps) => {
const [activeItem, setActiveItem] = useState<number>(
empty ? -1 : activeItemIndex
@ -63,7 +69,7 @@ export const Select = ({
value ? (
items[Number(value)]
) : (
<Typography sx={{ color: theme.palette.grey2.main }}>
<Typography sx={{ color: colorPlaceholder }}>
{placeholder}
</Typography>
)
@ -77,7 +83,7 @@ export const Select = ({
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${theme.palette.brightPurple.main} !important`,
border: `1px solid ${colorMain} !important`,
height: "48px",
borderRadius: "10px",
},
@ -100,21 +106,21 @@ export const Select = ({
gap: "8px",
"& .Mui-selected": {
backgroundColor: theme.palette.background.default,
color: theme.palette.brightPurple.main,
color: colorMain,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.brightPurple.main,
color: colorMain,
display: "flex",
alignItems: "center",
px: "9px",
gap: "20px",
},
}}
IconComponent={(props) => <ArrowDown {...props} />}
IconComponent={(props) => <ArrowDown {...props } color={color}/>}
>
{items.map((item, index) => (
<MenuItem
@ -126,7 +132,7 @@ export const Select = ({
gap: "20px",
padding: "10px",
borderRadius: "5px",
color: theme.palette.grey2.main,
color: colorPlaceholder,
}}
>
{item}

@ -1,11 +1,16 @@
import { useEffect, useState } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import ButtonsOptions from "../ButtonsOptions";
import { useDebouncedCallback } from "use-debounce";
import CustomNumberField from "@ui_kit/CustomNumberField";
import ButtonsOptions from "../ButtonsOptions";
import SwitchSlider from "./switchSlider";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { updateQuestion } from "@root/questions/actions";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
interface Props {
question: QuizQuestionNumber;
}
@ -19,14 +24,31 @@ export default function SliderOptions({ question }: Props) {
const [startError, setStartError] = useState<boolean>(false);
const [minError, setMinError] = useState<boolean>(false);
const [maxError, setMaxError] = useState<boolean>(false);
const startValueDebounce = useDebouncedCallback((value) => {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.start = value;
});
}, 2000);
useEffect(() => {
const min = Number(question.content.range.split("—")[0]);
const max = Number(question.content.range.split("—")[1]);
const start = Number(question.content.start);
if (start < min || start > max) {
if (start < min) {
setStartError(true);
startValueDebounce(min);
return;
}
if (start > max && min < max) {
setStartError(true);
startValueDebounce(max);
return;
}
if (start >= min && start <= max) {
@ -211,7 +233,11 @@ export default function SliderOptions({ question }: Props) {
placeholder={"1"}
error={stepError}
value={String(question.content.step)}
onChange={({ target }) => {
onChange={({ target, type }) => {
if (type === "blur") {
return;
}
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
@ -219,8 +245,10 @@ export default function SliderOptions({ question }: Props) {
});
}}
onBlur={({ target }) => {
const min = Number(question.content.range.split("—")[0]);
const max = Number(question.content.range.split("—")[1]);
const step = Number(target.value);
const range = max - min;
if (step > max) {
updateQuestion(question.id, (question) => {
@ -229,6 +257,14 @@ export default function SliderOptions({ question }: Props) {
question.content.step = max;
});
}
if (range % step) {
updateQuestion(question.id, (question) => {
if (question.type !== "number") return;
question.content.step = 1;
});
}
}}
/>
</Box>

@ -1,23 +1,32 @@
import {Box, Typography, Switch, useTheme, Button, useMediaQuery} from "@mui/material";
import {
Box,
Typography,
Switch,
useTheme,
Button,
useMediaQuery,
} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { updateOpenBranchingPanel } from "@root/uiTools/actions";
import {useQuestionsStore} from "@root/questions/store";
import {useRef} from "react";
import { useQuestionsStore } from "@root/questions/store";
import { useRef } from "react";
import { useUiTools } from "@root/uiTools/store";
interface Props {
openBranchingPage: boolean;
setOpenBranchingPage: () => void;
}
export const SwitchBranchingPanel = () => {
export const SwitchBranchingPanel = ({
openBranchingPage,
setOpenBranchingPage}:Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const isTablet = useMediaQuery(theme.breakpoints.down(1446));
const ref = useRef();
const {openBranchingPanel} = useUiTools()
const ref = useRef()
return ( !isTablet || openBranchingPanel ?
return !isTablet || openBranchingPage ? (
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
<Box
sx={{
@ -31,10 +40,8 @@ export const SwitchBranchingPanel = () => {
}}
>
<Switch
checked={openBranchingPanel}
onChange={
(e) => updateOpenBranchingPanel(e.target.checked)
}
checked={openBranchingPage}
onChange={setOpenBranchingPage}
sx={{
width: 50,
height: 30,
@ -87,11 +94,10 @@ export const SwitchBranchingPanel = () => {
Настройте связи между вопросами
</Typography>
</Box>
</Box>
{ openBranchingPanel && <QuestionsList /> }
{openBranchingPage && <QuestionsList />}
</Box>
:
) : (
<></>
);
};

@ -1,14 +1,18 @@
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 { useEffect, useRef, useState } from "react";
// import { useBlocker } from "react-router-dom";
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";
@ -17,15 +21,27 @@ import { useQuestionsStore } from "@root/questions/store";
import { deleteQuestion } from "@root/questions/actions";
import { QuizQuestionResult } from "@model/questionTypes/result";
import IconPlus from "@icons/IconPlus";
import Info from "@icons/Info";
import Plus from "@icons/Plus";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
export const ResultSettings = () => {
const { questions } = useQuestionsStore();
const quiz = useCurrentQuiz();
const results = useQuestionsStore().questions.filter((q): q is QuizQuestionResult => q.type === "result");
const results = useQuestionsStore().questions.filter(
(q): q is QuizQuestionResult => q.type === "result"
);
const [quizExpand, setQuizExpand] = useState(true);
const [resultContract, setResultContract] = useState(true);
const [triggerExit, setTriggerExit] = useState<{
follow: boolean;
path: string;
}>({ follow: false, path: "" });
const [openNotificationModal, setOpenNotificationModal] =
useState<boolean>(true);
const isReadyToLeaveRef = useRef(true);
console.log('quiz ', quiz)
// const blocker = useBlocker(false);
useEffect(
function calcIsReadyToLeave() {
@ -42,11 +58,29 @@ console.log('quiz ', quiz)
useEffect(() => {
return () => {
if (isReadyToLeaveRef.current === false) alert("Пожалуйста, проверьте, что вы заполнили все результаты");
if (!isReadyToLeaveRef.current && window.location.pathname !== "/edit") {
setOpenNotificationModal(true);
}
};
}, []);
const cnsl = results.filter(q=> q.content.usage)
const cnsl = results.filter((q) => q.content.usage);
const shouldBlock = true; // Replace this
// useEffect(() => {
// if (shouldBlock) {
// blocker.proceed?.()
// }
// }, [shouldBlock]);
const leavePage = (leave: boolean) => {
if (leave) {
console.log("ливаем");
}
setOpenNotificationModal(false);
};
return (
<Box sx={{ maxWidth: "796px" }}>
@ -81,9 +115,13 @@ console.log('quiz ', quiz)
</Box>
<WhenCard quizExpand={quizExpand} />
{quiz.config.resultInfo.when === "email" && <EmailSettingsCard quizExpand={quizExpand} />}
{quiz.config.resultInfo.when === "email" && (
<EmailSettingsCard quizExpand={quizExpand} />
)}
<Box sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}>
<Box
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
>
<Typography variant="p1" sx={{ color: "#4D4D4D", fontSize: "14px" }}>
Создайте результат
</Typography>
@ -109,7 +147,11 @@ console.log('quiz ', quiz)
</Box>
{cnsl.map((resultQuestion) => (
<ResultCard resultContract={resultContract} resultData={resultQuestion} key={resultQuestion.id} />
<ResultCard
resultContract={resultContract}
resultData={resultQuestion}
key={resultQuestion.id}
/>
))}
<Modal
open={false}

@ -1,14 +1,11 @@
import * as React from "react";
import { getQuestionByContentId, updateQuestion, uploadQuestionImage } from "@root/questions/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import {
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
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,
@ -20,7 +17,7 @@ import {
useMediaQuery,
useTheme,
FormControl,
Popover
Popover,
} from "@mui/material";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
@ -30,15 +27,19 @@ 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";
import { MediaSelectionAndDisplay } from "@ui_kit/MediaSelectionAndDisplay";
interface Props {
resultContract: boolean;
resultData: QuizQuestionResult;
}
export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => {
let check = true
export const checkEmptyData = ({
resultData,
}: {
resultData: QuizQuestionResult;
}) => {
let check = true;
if (
resultData.title.length > 0 ||
resultData.description.length > 0 ||
@ -48,14 +49,17 @@ export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult
resultData.content.text.length > 0 ||
resultData.content.video.length > 0 ||
resultData.content.hint.text.length > 0
) check = false
return check
}
)
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 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);
@ -66,20 +70,18 @@ const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => {
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
const id = open ? "simple-popover" : undefined;
return (
<>
<Info
sx={{
"MuiIconButton-root": {
boxShadow: "0 0 10px 10px red"
}
boxShadow: "0 0 10px 10px red",
},
}}
className={checkEmpty ? "blink" : ""}
onClick={handleClick}
/>
<Popover
id={id}
@ -87,85 +89,51 @@ const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => {
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
vertical: "bottom",
horizontal: "left",
}}
>
<Paper
sx={{
p: '20px',
p: "20px",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column"
flexDirection: "column",
}}
>
<Typography>
{resultData?.content.rule.parentId === "line" ? "Единый результат в конце прохождения опросника без ветвления"
:
`Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"`
}
{resultData?.content.rule.parentId === "line"
? "Единый результат в конце прохождения опросника без ветвления"
: `Заголовок вопроса, после которого появится результат: "${
question?.title || "нет заголовка"
}"`}
</Typography>
{checkEmpty &&
{checkEmpty && (
<Typography color="red">
Вы не заполнили этот результат никакими данными
</Typography>
}
)}
</Paper>
</Popover>
</>
)
}
);
};
export const ResultCard = ({ resultContract, resultData }: Props) => {
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)
const [expand, setExpand] = React.useState(true);
const [resultCardSettings, setResultCardSettings] = React.useState(false);
const [buttonPlus, setButtonPlus] = React.useState(true);
const question = getQuestionByContentId(resultData.content.rule.parentId);
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;
});
}
setExpand(true);
}, [resultContract]);
return (
<Paper
@ -177,18 +145,24 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
m: "20px 0",
}}
>
<Typography sx={{ color: theme.palette.grey2.main, padding: "5px 20px" }}>
{resultData?.content.rule.parentId === "line"
? "Единый результат в конце прохождения опросника без ветвления"
: `Заголовок вопроса, после которого появится результат: "${
question?.title || "нет заголовка"
}"`}
</Typography>
<Box
sx={{
display: expand ? "none" : "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
padding: isMobile ? "10px" : "0 20px 20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<FormControl
@ -203,7 +177,12 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<TextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)}
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) => (question.title = target.value)
)
}
sx={{
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
@ -277,7 +256,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Box sx={{ p: "0 20px", pt: "30px" }}>
<Box sx={{ p: "0 20px", pt: "10px" }}>
<Box
sx={{
width: "100%",
@ -291,7 +270,13 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<CustomTextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)} />
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) => (question.title = target.value)
)
}
/>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
@ -301,18 +286,21 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<ExpandLessIconBG />
</IconButton>
<InfoView resultData={resultData} />
</Box>
<Box
sx={{
margin: "20px 0"
margin: "20px 0",
}}
>
<CustomTextField
value={resultData.description}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.description = target.value)}
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) => (question.description = target.value)
)
}
placeholder={"Заголовок пожирнее"}
sx={{
borderRadius: "8px",
@ -323,9 +311,13 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
</Box>
<TextField
value={resultData.content.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.text = target.value)}
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) => (question.content.text = target.value)
)
}
fullWidth
placeholder="Описание"
multiline
@ -349,128 +341,12 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
}}
/>
<MediaSelectionAndDisplay resultData={resultData} />
<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 => {
q.content.video = e.target.value;
})}
/>
</Box>
}
</Box>
{
buttonPlus ?
{buttonPlus ? (
<Button
onClick={() => {
setButtonPlus(false)
setButtonPlus(false);
}}
sx={{
display: "inline flex",
@ -484,25 +360,31 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
border: "1px solid #9A9AAF",
background: " #F2F3F7",
color: "#9A9AAF",
mb: "30px"
mb: "30px",
}}
>
Кнопка +
</Button>
:
) : (
<Box
sx={{
mb: "30px"
mb: "30px",
}}
>
<Box>
<Typography component={"span"} sx={{ weight: "500", fontSize: "18px", mb: "10px" }}>
<Typography
component={"span"}
sx={{ weight: "500", fontSize: "18px", mb: "10px" }}
>
Призыв к действию
</Typography>
<IconButton
onClick={() => {
setButtonPlus(true)
updateQuestion(resultData.id, (q) => q.content.hint.text = "")
setButtonPlus(true);
updateQuestion(
resultData.id,
(q) => (q.content.hint.text = "")
);
}}
>
<Trash />
@ -511,7 +393,13 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
<TextField
value={resultData.content.hint.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.hint.text = target.value)}
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) =>
(question.content.hint.text = target.value)
)
}
fullWidth
placeholder="Например: узнать подробнее"
sx={{
@ -533,14 +421,7 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
}}
/>
</Box>
}
)}
</Box>
<Box
sx={{
@ -560,19 +441,21 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
>
<MiniButtonSetting
onClick={() => {
setResultCardSettings(!resultCardSettings)
setResultCardSettings(!resultCardSettings);
}}
sx={{
backgroundColor:
resultCardSettings
backgroundColor: resultCardSettings
? theme.palette.brightPurple.main
: "transparent",
color:
resultCardSettings ? "#ffffff" : theme.palette.grey3.main,
color: resultCardSettings
? "#ffffff"
: theme.palette.grey3.main,
"&:hover": {
backgroundColor: resultCardSettings ? "#581CA7" : "#7E2AEA",
color: "white"
}
backgroundColor: resultCardSettings
? "#581CA7"
: "#7E2AEA",
color: "white",
},
}}
>
<SettingIcon
@ -585,25 +468,28 @@ export const ResultCard = ({ resultContract, resultData }: Props) => {
</Box>
</Box>
</Box>
{
resultCardSettings &&
{resultCardSettings && (
<Box
sx={{
backgroundColor: "white",
p: "20px",
borderRadius: "0 0 12px 12px"
borderRadius: "0 0 12px 12px",
}}
>
<CustomTextField
placeholder={"Внутреннее описание вопроса"}
value={resultData.innerName}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.innerName = target.value)}
onChange={({ target }: { target: HTMLInputElement }) =>
updateQuestion(
resultData.id,
(question) => (question.content.innerName = target.value)
)
}
/>
</Box>
}
)}
</>
)
}
</Paper >
)
}
)}
</Paper>
);
};

@ -1,4 +1,4 @@
import { Box, Typography, Button, Paper, TextField, Link, InputAdornment } from "@mui/material";
import {Box, Typography, Button, Paper, TextField, Link, InputAdornment, useTheme} from "@mui/material";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
@ -13,6 +13,7 @@ import { useQuestionsStore } from "@root/questions/store";
import { checkEmptyData } from "../ResultPage/cards/ResultCard";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import {modes} from "../../utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
type ContactFormProps = {
@ -37,8 +38,9 @@ export const ContactForm = ({
setShowResultForm,
}: ContactFormProps) => {
const quiz = useCurrentQuiz();
const mode = modes;
const { questions } = useQuestionsStore();
const theme = useTheme();
const [ready, setReady] = useState(false)
const followNextForm = () => {
setShowContactForm(false);
@ -68,6 +70,7 @@ export const ContactForm = ({
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.palette.background.default,
height: "100vh"
}}
>
@ -82,7 +85,8 @@ export const ContactForm = ({
sx={{
textAlign: "center",
m: "20px 0",
fontSize: "28px"
fontSize: "28px",
color: theme.palette.text.primary
}}
>
{quiz?.config.formContact.title || "Заполните форму, чтобы получить результаты теста"}
@ -109,6 +113,7 @@ export const ContactForm = ({
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
backgroundColor: theme.palette.background.default,
p: "30px"
}}>
@ -146,13 +151,13 @@ export const ContactForm = ({
width: "450px",
}}
>
<CustomCheckbox label="" handleChange={({ target }) => { setReady(target.checked) }} checked={ready} />
<CustomCheckbox label="" handleChange={({ target }) => { setReady(target.checked) }} checked={ready} colorIcon={theme.palette.primary.main}/>
<Typography>
С
С&ensp;
<Link> Положением об обработке персональных данных </Link>
и
&ensp;и&ensp;
<Link> Политикой конфиденциальности </Link>
ознакомлен
&ensp;ознакомлен
</Typography>
</Box>
@ -160,11 +165,14 @@ export const ContactForm = ({
sx={{
display: "flex",
alignItems: "center",
mt: "20px"
mt: "20px",
gap: "15px"
}}
>
<NameplateLogo style={{ fontSize: "34px" }} />
<Typography sx={{ fontSize: "20px", color: "#4D4D4D", whiteSpace: "nowrap" }}>Сделано на PenaQuiz</Typography>
<NameplateLogo style={{ fontSize: "34px", color: mode[quiz.config.theme] ? "#151515" : "#FFFFFF" }} />
<Typography sx={{ fontSize: "20px", color: mode[quiz.config.theme] ? "#4D4D4D" : "#F5F7FF", whiteSpace: "nowrap" }}>
Сделано на PenaQuiz
</Typography>
</Box>
</Paper>
</Box >

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Box, Button, Typography, useTheme } from "@mui/material";
import {Box, Button, Typography, useMediaQuery, useTheme} from "@mui/material";
import { useQuizViewStore } from "@root/quizView";
import { useCurrentQuiz } from "@root/quizes/hooks";
@ -9,6 +9,8 @@ import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questio
import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
import {NameplateLogoFQDark} from "@icons/NameplateLogoFQDark";
import {modes} from "../../utils/themes/Publication/themePublication";
import { checkEmptyData } from "../ResultPage/cards/ResultCard";
type FooterProps = {
@ -23,9 +25,11 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const [stepNumber, setStepNumber] = useState(1);
const quiz = useCurrentQuiz();
const mode = modes;
const { answers } = useQuizViewStore();
const questions = useQuestionsStore().questions as AnyTypedQuizQuestion[];
const theme = useTheme();
const isMobileMini = useMediaQuery(theme.breakpoints.down(382));
const linear = !questions.find(({ content }) => content.rule.parentId === "root");
useEffect(() => {
@ -113,10 +117,14 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
};
const getNextQuestionId = () => {
console.log("net")
console.log(question)
let readyBeNextQuestion = "";
//вопрос обязателен, анализируем ответ и условия ветвления
if (answers.length) {
const answer = answers.find(({ questionId }) => questionId === question.content.id);
let readyBeNextQuestion = "";
(question as QuizQuestionBase).content.rule.main.forEach(({ next, rules }) => {
let longerArray = Math.max(
@ -143,8 +151,32 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
}
});
return readyBeNextQuestion;
if (readyBeNextQuestion) return readyBeNextQuestion;
}
if (!question.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
console.log("вопрос не обязателен ищем дальше")
const defaultQ = question.content.rule.default
if (defaultQ) return defaultQ
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
(question?.type === "date" ||
question?.type === "text" ||
question?.type === "number" ||
question?.type === "page") && question.content.rule.children.length === 1
) return question.content.rule.children[0]
}
//ничё не нашли, ищем резулт
console.log("ничё не нашли, ищем резулт ")
return questions.find(q => {
console.log('q.type === "result"', q.type === "result")
console.log('q.content.rule.parentId === question.content.id', q.content.rule.parentId === question.content.id)
return q.type === "result" && q.content.rule.parentId === question.content.id
})?.content.id
};
const followPreviousStep = () => {
@ -192,23 +224,19 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
const nextQuestionId = getNextQuestionId();
console.log(nextQuestionId)
if (nextQuestionId) {
const nextQuestion = getQuestionByContentId(nextQuestionId);
console.log(nextQuestion)
if (nextQuestion?.type && nextQuestion.type !== "result") {
if (nextQuestion?.type && nextQuestion.type === "result") {
showResult(nextQuestion);
} else {
setCurrentQuestion(nextQuestion);
return;
}
} else {
enqueueSnackbar("не могу получить последующий вопрос");
}
} else {
const nextQuestion = getQuestionByContentId(question.content.rule.default);
if (nextQuestion?.type && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion);
} else {
showResult(nextQuestion);
}
}
};
return (
@ -233,7 +261,11 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
gap: "10px",
}}
>
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} />
{/*{mode[quiz.config.theme] ? (*/}
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/}
{linear &&
<>
<Box
@ -242,7 +274,7 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
alignItems: "center",
gap: "10px",
marginRight: "auto",
color: theme.palette.grey1.main,
color: theme.palette.text.primary,
}}
>
<Typography>Шаг</Typography>
@ -256,7 +288,7 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
width: "30px",
height: "30px",
color: "#FFF",
background: theme.palette.brightPurple.main,
background: theme.palette.primary.main,
}}
>
{stepNumber}
@ -268,13 +300,50 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
</Box>
</>
}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
// color: theme.palette.grey1.main,
}}
>
{/* <Typography>Шаг</Typography>
<Typography
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "50%",
width: "30px",
height: "30px",
color: "#FFF",
background: theme.palette.brightPurple.main,
}}
>
{stepNumber} */}
{/* </Typography> */}
{/* <Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}>
{questions.length}
</Typography> */}
</Box>
<Button
disabled={disablePreviousButton}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
sx={{ fontSize: "16px", padding: "10px 15px",}}
onClick={followPreviousStep}
>
Назад
{isMobileMini ? (
"←"
) : (
"← Назад"
)}
</Button>
<Button
disabled={disableNextButton}

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Box } from "@mui/material";
import {Box, useTheme} from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { getQuestionByContentId } from "@root/questions/actions";
@ -22,6 +22,9 @@ import { ResultQuestion } from "./ResultQuestion";
import type { QuestionType } from "../../model/question/question";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import {NameplateLogoFQ} from "@icons/NameplateLogoFQ";
import {NameplateLogoFQDark} from "@icons/NameplateLogoFQDark";
import {modes} from "@utils/themes/Publication/themePublication";
type QuestionProps = {
questions: AnyTypedQuizQuestion[];
@ -43,11 +46,10 @@ const QUESTIONS_MAP: any = {
export const Question = ({ questions }: QuestionProps) => {
const quiz = useCurrentQuiz();
const [currentQuestion, setCurrentQuestion] =
useState<AnyTypedQuizQuestion>();
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false);
const mode = modes;
useEffect(() => {
const nextQuestion = getQuestionByContentId(quiz?.config.haveRoot || "");
@ -64,9 +66,12 @@ export const Question = ({ questions }: QuestionProps) => {
const QuestionComponent =
QUESTIONS_MAP[currentQuestion.type as Exclude<QuestionType, "nonselected">];
const theme = useTheme();
return (
<Box
sx={{
backgroundColor: theme.palette.background.default
}}
height="100vh"
>
@ -78,9 +83,18 @@ export const Question = ({ questions }: QuestionProps) => {
maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
overflow: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
>
<QuestionComponent currentQuestion={currentQuestion} />
{mode[quiz.config.theme] ? (
<NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />
):(
<NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />
)}
</Box>
)}
{showResultForm && quiz?.config.resultInfo.when === "before" && (

@ -7,6 +7,7 @@ import { useQuestionsStore } from "@root/questions/store";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import YoutubeEmbedIframe from "../../ui_kit/StartPagePreview/YoutubeEmbedIframe.tsx"
import { NameplateLogo } from "@icons/NameplateLogo";
import {modes} from "../../utils/themes/Publication/themePublication";
type ResultFormProps = {
currentQuestion: AnyTypedQuizQuestion;
@ -23,6 +24,7 @@ export const ResultForm = ({
}: ResultFormProps) => {
const quiz = useCurrentQuiz();
const mode = modes;
const { questions } = useQuestionsStore();
const resultQuestion = questions.find(
(question) =>
@ -126,7 +128,7 @@ export const ResultForm = ({
}}
>
<NameplateLogo style={{ fontSize: "34px" }} />
<Typography sx={{ fontSize: "20px", color: "#4D4D4D", whiteSpace: "nowrap" }}>Сделано на PenaQuiz</Typography>
<Typography sx={{ fontSize: "20px", color: mode[quiz.config.theme] ? "#4D4D4D" : "#F5F7FF", whiteSpace: "nowrap" }}>Сделано на PenaQuiz</Typography>
</Box>
</Box>

@ -5,6 +5,7 @@ import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice";
import { NameplateLogo } from "@icons/NameplateLogo";
import {modes} from "../../utils/themes/Publication/themePublication";
interface Props {
setVisualStartPage: (a: boolean) => void;
@ -13,10 +14,14 @@ interface Props {
export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
const theme = useTheme();
const quiz = useCurrentQuiz();
const mode = modes;
const { isMobileDevice } = useUADevice();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
if (!quiz) return null;
console.log(quiz);
const handleCopyNumber = () => {
navigator.clipboard.writeText(quiz.config.info.phonenumber);
};
@ -76,13 +81,13 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
height: "100vh",
width: "100vw",
background:
quiz.config.startpageType === "expanded"
quiz.config.startpageType === "expanded" && !isMobile
? 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)"
: "",
: theme.palette.background.default,
color: quiz.config.startpageType === "expanded" ? "white" : "black",
}}
@ -109,10 +114,10 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
alt=""
/>
)}
<Typography sx={{ fontSize: "14px" }}>{quiz.config.info.orgname}</Typography>
<Typography sx={{ fontSize: "14px", color: quiz.config.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary}}>{quiz.config.info.orgname}</Typography>
</Box>
<Link mb="16px" href={quiz.config.info.site}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{quiz.config.info.site}
</Typography>
</Link>
@ -147,6 +152,7 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
overflowWrap: "break-word",
width: "100%",
textAlign: quiz.config.startpageType === "centered" ? "center" : "-moz-initial",
color: quiz.config.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
}}
>
{quiz.name}
@ -178,25 +184,32 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
</Box>
<Box
sx={{ mt: "46px", display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}
sx={{
mt: "46px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
flexDirection: isMobile ? "column" : "row"
}}
>
<Box sx={{ maxWidth: "300px" }}>
{quiz.config.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${quiz.config.info.phonenumber}`}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{quiz.config.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{quiz.config.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.brightPurple.main }}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{quiz.config.info.phonenumber}
</Typography>
)}
@ -209,10 +222,11 @@ export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
sx={{
display: "flex",
alignItems: "center",
gap: "15px"
}}
>
<NameplateLogo style={{ fontSize: "34px" }} />
<Typography sx={{ fontSize: "20px", color: "#4D4D4D", whiteSpace: "nowrap" }}>
<NameplateLogo style={{ fontSize: "34px", color: quiz.config.startpageType === "expanded" && !isMobile ? "#FFFFFF" : (mode[quiz.config.theme] ? "#151515" : "#FFFFFF") }} />
<Typography sx={{ fontSize: "20px", color: quiz.config.startpageType === "expanded" && !isMobile ? "#F5F7FF" : (mode[quiz.config.theme] ? "#4D4D4D" : "#F5F7FF"), whiteSpace: "nowrap", }}>
Сделано на PenaQuiz
</Typography>
</Box>
@ -241,29 +255,84 @@ function QuizPreviewLayoutByType({
alignType: QuizStartpageAlignType;
}) {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(630));
switch (startpageType) {
case null:
case "standard": {
return (
const isMobile = useMediaQuery(theme.breakpoints.down(650));
function StartPageMobile() {
return(
<Box
sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: !isTablet ? "40%" : "100%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: !isTablet ? "flex-start" : "center",
alignItems: "flex-start",
p: "25px",
height: "80%"
}}
>
{quizHeaderBlock}
<Box
sx={{
height: "80%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%"
}}
>
{quizMainBlock}
</Box>
</Box>
<Box
sx={{
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)
}
switch (startpageType) {
case null:
case "standard": {
return (
<>
{isMobile ? (
<StartPageMobile/>
) : (
<Box
sx={{
display: "flex",
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
flexGrow: 1,
justifyContent: isMobile ? "flex-end" : undefined,
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: isMobile ? "100%" : "40%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: isMobile ? "80%" : undefined
}}
>
{quizHeaderBlock}
@ -272,17 +341,25 @@ function QuizPreviewLayoutByType({
<Box
sx={{
width: "60%",
width: isMobile ? "100%" : "60%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)}
</>
);
}
case "expanded": {
return (
<>
{isMobile ? (
<StartPageMobile/>
) : (
<Box
sx={{
position: "relative",
@ -322,10 +399,18 @@ function QuizPreviewLayoutByType({
{backgroundBlock}
</Box>
</Box>
)
}
</>
);
}
case "centered": {
return (
<>
{isMobile ? (
<StartPageMobile/>
) : (
<Box
sx={{
padding: "16px",
@ -351,6 +436,10 @@ function QuizPreviewLayoutByType({
)}
{quizMainBlock}
</Box>
)
}
</>
);
}
default:

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Box } from "@mui/material";
import {Box, Button, ThemeProvider, useTheme} from "@mui/material";
import { StartPageViewPublication } from "./StartPageViewPublication";
import { Question } from "./Question";
@ -7,7 +7,7 @@ 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 { setQuizes, updateQuiz } from "@root/quizes/actions";
import { isAxiosError } from "axios";
import { devlog } from "@frontend/kitui";
import { useQuizStore } from "@root/quizes/store";
@ -18,14 +18,14 @@ import { useQuestionsStore } from "@root/questions/store";
import { setQuestions } from "@root/questions/actions";
import { questionApi } from "@api/question";
import { ApologyPage } from "./ApologyPage"
import {themesPublication} from "../../utils/themes/Publication/themePublication";
export const ViewPage = () => {
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore();
console.log("quiz ", quiz)
const theme = useTheme();
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList();
@ -57,12 +57,50 @@ export const ViewPage = () => {
if (visualStartPage === undefined) return <></>;
if (questions.length === 0 || (questions.length === 1 && questions[0].type === "result")) return <ApologyPage message="Нет созданных вопросов"/>
return (
<Box>
<ThemeProvider theme={themesPublication?.[quiz?.config.theme]}>
<Box sx={{backgroundColor: quiz.config.startpageType === "expanded" ? undefined : theme.palette.background.default}}>
{!visualStartPage ? (
<StartPageViewPublication setVisualStartPage={setVisualStartPage} />
) : (
<Question questions={filteredQuestions} />
)}
<Box>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "StandardTheme"
})}>Standard</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "PinkTheme"
})}>Pink</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "BlackWhiteTheme"
})}>BlackWhite</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "OliveTheme"
})}>Olive</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "YellowTheme"
})}>Yellow</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "PurpleTheme"
})}>Purple</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "BlueTheme"
})}>Blue</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "StandardDarkTheme"
})}>StandardDark</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "PinkDarkTheme"
})}>PinkDark</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "GoldDarkTheme"
})}>GoldDark</Button>
<Button onClick={() => updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = "BlueDarkTheme"
})}>BlueDark</Button>
</Box>
</Box>
</ThemeProvider>
);
};

@ -1,17 +1,22 @@
import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography } from "@mui/material";
import {Box, Typography, useTheme} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon";
import {modes} from "../../../utils/themes/Publication/themePublication";
import {useCurrentQuiz} from "@root/quizes/hooks";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme();
const mode = modes;
const quiz = useCurrentQuiz();
const { answers } = useQuizViewStore();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -20,7 +25,7 @@ export const Date = ({ currentQuestion }: DateProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
@ -31,7 +36,10 @@ export const Date = ({ currentQuestion }: DateProps) => {
>
<DatePicker
slots={{
openPickerIcon: () => <CalendarIcon />,
openPickerIcon: () => <CalendarIcon sx={{
"& path": {stroke: theme.palette.primary.main},
"& rect": {stroke: theme.palette.primary.main}
}} />,
}}
value={dayjs(
answer
@ -61,10 +69,14 @@ export const Date = ({ currentQuestion }: DateProps) => {
},
"data-cy": "open-datepicker",
},
layout: {
sx: {backgroundColor: theme.palette.background.default,}
}
}}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
borderRadius: "10px",
maxWidth: "250px",
pr: "22px",
@ -77,6 +89,7 @@ export const Date = ({ currentQuestion }: DateProps) => {
borderColor: "#9A9AAF",
},
},
}}
/>
</Box>

@ -5,7 +5,7 @@ import {
FormControlLabel,
Radio,
useTheme,
FormControl,
FormControl, useMediaQuery,
} from "@mui/material";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView";
@ -22,6 +22,7 @@ type EmojiProps = {
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -29,7 +30,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
@ -49,13 +50,14 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", width: "100%", gap: "42px" }}>
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={variant.id}
sx={{
borderRadius: "12px",
border: `1px solid ${theme.palette.grey2.main}`,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
@ -89,7 +91,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
sx={{
margin: 0,
padding: "15px",
color: "#4D4D4D",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
}}
@ -107,7 +109,7 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
}
}}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>

@ -56,7 +56,7 @@ export const File = ({ currentQuestion }: FileProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
@ -68,11 +68,11 @@ export const File = ({ currentQuestion }: FileProps) => {
>
{answer?.split("|")[0] && (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography>Вы загрузили:</Typography>
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
<Box
sx={{
padding: "5px 5px 5px 16px",
backgroundColor: theme.palette.brightPurple.main,
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
@ -115,7 +115,8 @@ export const File = ({ currentQuestion }: FileProps) => {
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.grey2.main}`,
border: `1px solid #9A9AAF`,
// border: `1px solid ${theme.palette.grey2.main}`,
borderRadius: "8px",
}}
>
@ -123,7 +124,8 @@ export const File = ({ currentQuestion }: FileProps) => {
<Box>
<Typography
sx={{
color: theme.palette.grey2.main,
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontWeight: 500,
}}
>
@ -134,7 +136,8 @@ export const File = ({ currentQuestion }: FileProps) => {
</Typography>
<Typography
sx={{
color: theme.palette.grey2.main,
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
}}

@ -30,7 +30,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
@ -62,7 +62,8 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid ${theme.palette.grey2.main}`,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
}}
onClick={(event) => {
event.preventDefault();
@ -98,12 +99,12 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
sx={{
display: "block",
textAlign: "center",
color: theme.palette.grey2.main,
color: theme.palette.text.primary,
marginTop: "10px",
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
}
label={variant.answer}
/>

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { Box, Typography, Slider, useTheme } from "@mui/material";
import {Box, Typography, Slider, useTheme, useMediaQuery} from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomTextField from "@ui_kit/CustomTextField";
@ -8,6 +8,8 @@ import { CustomSlider } from "@ui_kit/CustomSlider";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import {modes} from "../../../utils/themes/Publication/themePublication";
import {useCurrentQuiz} from "@root/quizes/hooks";
type NumberProps = {
currentQuestion: QuizQuestionNumber;
@ -17,6 +19,9 @@ export const Number = ({ currentQuestion }: NumberProps) => {
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const mode = modes;
const quiz = useCurrentQuiz();
const { answers } = useQuizViewStore();
const updateMinRangeDebounced = useDebouncedCallback(
(value, crowded = false) => {
@ -26,7 +31,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
updateAnswer(currentQuestion.content.id, value);
},
1000
3000
);
const updateMaxRangeDebounced = useDebouncedCallback(
(value, crowded = false) => {
@ -36,7 +41,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
updateAnswer(currentQuestion.content.id, value);
},
1000
3000
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -53,14 +58,14 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMinRange(sliderValue.split("—")[0]);
setMaxRange(String(max));
}
}, []);
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
@ -68,6 +73,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
width: "100%",
marginTop: "20px",
gap: "30px",
paddingRight: isMobile ? "10px" : undefined
}}
>
<CustomSlider
@ -93,6 +99,11 @@ export const Number = ({ currentQuestion }: NumberProps) => {
setMaxRange(String(range[1]));
}
}}
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,}
}}
/>
{!currentQuestion.content.chooseRange && (
@ -111,7 +122,11 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
},
}}
/>
)}
@ -141,10 +156,14 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
},
}}
/>
<Typography>до</Typography>
<Typography color={theme.palette.text.primary}>до</Typography>
<CustomTextField
placeholder="0"
value={maxRange}
@ -161,7 +180,11 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}}
sx={{
maxWidth: "80px",
"& .MuiInputBase-input": { textAlign: "center" },
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
},
}}
/>
</Box>

@ -1,8 +1,9 @@
import { Box, Typography } from "@mui/material";
import {Box, Typography, useTheme} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import YoutubeEmbedIframe from "@ui_kit/StartPagePreview/YoutubeEmbedIframe";
type PageProps = {
currentQuestion: QuizQuestionPage;
@ -11,11 +12,12 @@ type PageProps = {
export const Page = ({ currentQuestion }: PageProps) => {
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const theme = useTheme();
return (
<Box>
<Typography variant="h5" sx={{ paddingBottom: "25px" }}>{currentQuestion.title}</Typography>
<Typography>{currentQuestion.content.text}</Typography>
<Typography variant="h5" sx={{ paddingBottom: "25px", color: theme.palette.text.primary }}>{currentQuestion.title}</Typography>
<Typography color={theme.palette.text.primary}>{currentQuestion.content.text}</Typography>
<Box
sx={{
display: "flex",
@ -24,11 +26,10 @@ export const Page = ({ currentQuestion }: PageProps) => {
marginTop: "20px",
}}
>
{currentQuestion.content.picture && (
<Box sx={{borderRadius: "12px",
border: "1px solid #9A9AAF", overflow: "hidden" }}>
{currentQuestion.content.useImage ? (
<Box sx={{ borderRadius: "12px", border: "1px solid #9A9AAF", overflow: "hidden" }}>
<img
src={currentQuestion.content.picture}
src={currentQuestion.content.back}
alt=""
style={{
display: "block",
@ -38,18 +39,10 @@ export const Page = ({ currentQuestion }: PageProps) => {
}}
/>
</Box>
)}
{currentQuestion.content.video && (
<video
src={currentQuestion.content.video}
controls
style={{
width: "100%",
height: "100%",
maxHeight: "80vh",
objectFit: "contain",
}}
) : (
<YoutubeEmbedIframe
containerSX={{ width: "100%", height: "100%", maxHeight: "80vh", objectFit: "contain" }}
videoUrl={currentQuestion.content.video}
/>
)}
</Box>

@ -2,7 +2,7 @@ import {
Box,
Typography,
Rating as RatingComponent,
useTheme,
useTheme, useMediaQuery,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
@ -55,6 +55,7 @@ const buttonRatingForm = [
export const Rating = ({ currentQuestion }: RatingProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -65,16 +66,20 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
width: isMobile ? "100%" : undefined
}}
>
<Typography sx={{ color: theme.palette.grey2.main }}>
<Typography sx={{
color: "#9A9AAF"
// color: theme.palette.grey2.main
}}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Box
@ -88,13 +93,17 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
onChange={(_, value) =>
updateAnswer(currentQuestion.content.id, String(value))
}
sx={{ height: "50px", gap: "15px" }}
sx={{ height: "50px",
gap: isMobile ? undefined : "15px",
justifyContent: isMobile ? "space-between" : undefined,
width: isMobile ? "100%" : undefined
}}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.brightPurple.main)}
emptyIcon={form?.icon(theme.palette.grey2.main)}
icon={form?.icon(theme.palette.primary.main)}
emptyIcon={form?.icon("#9A9AAF")}
/>
</Box>
<Typography sx={{ color: theme.palette.grey2.main }}>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>

@ -1,4 +1,4 @@
import { Box, Typography } from "@mui/material";
import {Box, Typography, useTheme} from "@mui/material";
import { Select as SelectComponent } from "../../../pages/Questions/Select";
@ -12,6 +12,7 @@ type SelectProps = {
export const Select = ({ currentQuestion }: SelectProps) => {
const { answers } = useQuizViewStore();
const theme = useTheme();
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -19,7 +20,7 @@ export const Select = ({ currentQuestion }: SelectProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
@ -32,6 +33,8 @@ export const Select = ({ currentQuestion }: SelectProps) => {
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
color={theme.palette.primary.main}
onChange={(_, value) => {
if (value < 0) {
deleteAnswer(currentQuestion.content.id);

@ -1,4 +1,4 @@
import { Box, Typography } from "@mui/material";
import {Box, Typography, useTheme} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
@ -13,10 +13,10 @@ type TextProps = {
export const Text = ({ currentQuestion }: TextProps) => {
const { answers } = useQuizViewStore();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {};
const theme = useTheme();
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
@ -29,6 +29,11 @@ export const Text = ({ currentQuestion }: TextProps) => {
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={({ target }) => updateAnswer(currentQuestion.content.id, target.value)}
sx={{
"&:focus-visible": {
borderColor: theme.palette.primary.main
}
}}
/>
</Box>
</Box>

@ -21,10 +21,12 @@ import {
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { CheckboxIcon } from "@icons/Checkbox";
import CheckboxIcon from "@icons/Checkbox";
import {modes} from "../../../utils/themes/Publication/themePublication";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import {useCurrentQuiz} from "@root/quizes/hooks";
type VariantProps = {
stepNumber: number;
@ -40,6 +42,7 @@ type VariantItemProps = {
};
export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme()
const { answers, ownVariants } = useQuizViewStore();
const { answer } =
answers.find(
@ -59,7 +62,7 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}>
<Group
name={currentQuestion.id}
@ -126,15 +129,19 @@ const VariantItem = ({
own = false,
}: VariantItemProps) => {
const theme = useTheme();
const mode = modes
const quiz = useCurrentQuiz();
return (
<FormControlLabel
key={variant.id}
sx={{
margin: "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
border: `1px solid ${theme.palette.grey2.main}`,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
display: "flex",
maxWidth: "685px",
justifyContent: "space-between",
@ -150,11 +157,11 @@ const VariantItem = ({
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<CheckboxIcon checked />}
checkedIcon={<CheckboxIcon checked color={theme.palette.primary.main} />}
icon={<CheckboxIcon />}
/>
) : (
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}

@ -4,7 +4,7 @@ import {
RadioGroup,
FormControlLabel,
Radio,
useTheme,
useTheme, useMediaQuery,
} from "@mui/material";
import gag from "./gag.png"
@ -15,6 +15,8 @@ import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import {modes} from "../../../utils/themes/Publication/themePublication";
import {useCurrentQuiz} from "@root/quizes/hooks";
type VarimgProps = {
currentQuestion: QuizQuestionVarImg;
@ -22,7 +24,10 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { answers } = useQuizViewStore();
const mode = modes;
const quiz = useCurrentQuiz();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.content.id
@ -33,8 +38,14 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex", marginTop: "20px" }}>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Box sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: isMobile ? "30px" : undefined
}}>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
@ -48,7 +59,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
flexBasis: "100%",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%", gap: isMobile ? "20px" : undefined }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
@ -56,9 +67,12 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: "#4D4D4D",
border: `1px solid ${theme.palette.grey2.main}`,
color: theme.palette.text.primary,
backgroundColor: mode[quiz.config.theme] ? "white" : theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: isMobile ? 0 : undefined,
}}
value={index}
onClick={(event) => {
@ -74,7 +88,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
}
}}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
}
label={variant.answer}
/>
@ -107,7 +121,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
alt=""
/>
:
(variant?.extendedText || "Выберите вариант ответа слева")
(variant?.extendedText || isMobile ? ("Выберите вариант ответа ниже") : ("Выберите вариант ответа слева"))
}
</Box>

@ -0,0 +1,72 @@
import { Box, Button, Modal, Typography } from "@mui/material";
import { useUiTools } from "@root/uiTools/store";
type ConfirmLeaveModalProps = {
open: boolean;
follow: () => void;
cancel: () => void;
};
export const ConfirmLeaveModal = ({
open,
follow,
cancel,
}: ConfirmLeaveModalProps) => (
<Modal open={open} onClose={cancel}>
<Box
sx={{
outline: "none",
position: "absolute",
overflow: "hidden",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: 0,
}}
>
<Box
sx={{
boxSizing: "border-box",
background: "#F2F3F7",
height: "70px",
padding: "0 25px",
display: "flex",
alignItems: "center",
}}
>
<Typography component="span">
Пожалуйста, проверьте, что вы заполнили все результаты
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "end",
gap: "10px",
margin: "20px",
}}
>
<Button
variant="contained"
sx={{ width: "100%", maxWidth: "130px" }}
onClick={cancel}
>
Остаться
</Button>
<Button
variant="contained"
sx={{ width: "100%", maxWidth: "130px" }}
onClick={follow}
>
Покинуть
</Button>
</Box>
</Box>
</Modal>
);

@ -4,6 +4,8 @@ import BackArrowIcon from "@icons/BackArrowIcon";
import { Burger } from "@icons/Burger";
import EyeIcon from "@icons/EyeIcon";
import { PenaLogoIcon } from "@icons/PenaLogoIcon";
import VisibilityIcon from "@mui/icons-material/Visibility";
import {
Box,
Button,
@ -16,7 +18,7 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { decrementCurrentStep, resetEditConfig, setQuizes, updateQuiz } from "@root/quizes/actions";
import { decrementCurrentStep, resetEditConfig, setQuizes, updateQuiz, setCurrentStep } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuizStore } from "@root/quizes/store";
import CustomAvatar from "@ui_kit/Header/Avatar";
@ -33,7 +35,13 @@ import useSWR from "swr";
import { useDebouncedCallback } from "use-debounce";
import { SidebarMobile } from "./Sidebar/SidebarMobile";
import { cleanQuestions, createResult, setQuestions } from "@root/questions/actions";
import { updateOpenBranchingPanel, updateCanCreatePublic, updateModalInfoWhyCantCreate } from "@root/uiTools/actions";
import {
updateOpenBranchingPanel,
updateCanCreatePublic,
updateModalInfoWhyCantCreate,
setShowConfirmLeaveModal,
updateSomeWorkBackend,
} from "@root/uiTools/actions";
import { BranchingPanel } from "../Questions/BranchingPanel";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizes } from "@root/quizes/hooks";
@ -46,94 +54,98 @@ import { clearAuthToken } from "@frontend/kitui";
import { logout } from "@api/auth";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { ModalInfoWhyCantCreate } from "./ModalInfoWhyCantCreate";
import { type } from "os";
import { ConfirmLeaveModal } from "./ConfirmLeaveModal";
import { checkQuestionHint } from "@utils/checkQuestionHint";
import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions";
import { toggleQuizPreview } from "@root/quizPreview";
import { LinkSimple } from "@icons/LinkSimple";
import { BackButtonIcon } from "@icons/BackButtonIcon";
let init: () => void;
export default function EditPage() {
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore();
console.log("quiz ", quiz);
console.log(questions);
useEffect(() => {
const getData = async () => {
const quizes = await quizApi.getList();
setQuizes(quizes);
if (editQuizId) {
const questions = await questionApi.getList({ quiz_id: editQuizId });
setQuestions(questions);
//Всегда должен существовать хоть 1 резулт - "line"
console.log(questions)
if (!questions?.find(q=>q.type === "result" && q.content.includes(':"line"') || q.content.includes(":'line'"))) createResult(quiz?.backendId, "line")
// console.log("сейчас будем ворошиться в этих квешенах ", questions);
if (
!questions?.find(
(q) => (q.type === "result" && q.content.includes(':"line"')) || q.content.includes(":'line'")
)
) {
createResult(quiz?.backendId, "line");
console.log("Я не нашёл линейный резулт и собираюсь создать новый");
}
}
};
getData();
}, []);
const { openBranchingPanel, whyCantCreatePublic, canCreatePublic } = useUiTools();
const { openBranchingPanel, whyCantCreatePublic, canCreatePublic, showConfirmLeaveModal } = useUiTools();
const theme = useTheme();
const navigate = useNavigate();
const currentStep = useQuizStore((state) => state.currentStep);
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const isMobileSm = useMediaQuery(theme.breakpoints.down(370));
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const [nextStep, setNextStep] = useState<number>(0);
const quizConfig = quiz?.config;
const disableTest = quiz === undefined ? true : (quiz.config.type === null)
const disableTest = quiz === undefined ? true : quiz.config.type === null;
const [openBranchingPage, setOpenBranchingPage] = useState<boolean>(false);
const [buttonText, setButtonText] = useState("Опубликовать");
const openBranchingPageHC = () => {
if (!openBranchingPage) {
deleteTimeoutedQuestions(questions, quiz);
}
setOpenBranchingPage((old) => !old);
};
useEffect(() => {
if (editQuizId === null) navigate("/list");
}, [navigate, editQuizId]);
useEffect(
() => {
return () => {
() => () => {
resetEditConfig();
cleanQuestions();
updateModalInfoWhyCantCreate(false)
}
updateModalInfoWhyCantCreate(false);
updateSomeWorkBackend(false)
},
[]
);
const updateQuestionHint = useDebouncedCallback((questions: AnyTypedQuizQuestion[]) => {
const problems: any = {}
questions.forEach((question) => {
//Если не участвует в ветвлении, или безтиповый, или резулт - он нам не интересен
if (question.type === null
|| question.type === "result"
|| question.content.rule.parentId.length === 0) return
//если есть дети, но нет дефолта - логическая ошибка. Так нельзя
if (question.content.rule.children.length > 0 && question.content.rule.default.length === 0) {
problems[question.content.id] = {
name: question.title,
problems: ["Не выбран дефолтный вопрос"]
}
}
})
useUiTools.setState({ whyCantCreatePublic: problems })
const problems = checkQuestionHint(questions);
useUiTools.setState({ whyCantCreatePublic: problems });
if (Object.keys(problems).length > 0) {
updateQuiz(quiz?.id, (state) => { state.status = "stop" })
updateCanCreatePublic(false)
updateQuiz(quiz?.id, (state) => {
state.status = "stop";
});
updateCanCreatePublic(false);
} else {
updateCanCreatePublic(true)
updateCanCreatePublic(true);
}
}, 600);
useEffect(() => {
updateQuestionHint(questions)
updateQuestionHint(questions);
}, [questions]);
async function handleLogoutClick() {
const [, logoutError] = await logout();
@ -145,9 +157,38 @@ export default function EditPage() {
clearUserData();
navigate("/");
}
console.log(questions)
if (!quizConfig) return <></>
const followNewPage = () => {
setShowConfirmLeaveModal(false);
setCurrentStep(nextStep);
};
if (!quizConfig) return <></>;
const isConditionMet = [1].includes(currentStep) && !openBranchingPanel && quizConfig.type !== "form";
const handleClickStatusQuiz = () => {
if (Object.keys(whyCantCreatePublic).length === 0) {
if (buttonText === "Опубликовать") {
setButtonText("Опубликовано");
setTimeout(() => {
setButtonText("Отозвать");
}, 3000);
} else {
setButtonText("Опубликовать");
}
updateQuiz(quiz?.id, (state) => {
state.status = quiz?.status === "start" ? "stop" : "start";
});
} else {
updateModalInfoWhyCantCreate(true);
}
};
console.log(quiz?.status);
return (
<>
{/*хедер*/}
@ -276,14 +317,19 @@ export default function EditPage() {
display: isMobile ? "block" : "flex",
}}
>
{isMobile ? <SidebarMobile open={mobileSidebar} /> : <Sidebar />}
{isMobile ? <SidebarMobile open={mobileSidebar} /> : <Sidebar setNextStep={setNextStep} />}
<Box
sx={{
background: theme.palette.background.default,
width: "100%",
padding: isMobile ? "16px 16px 140px 16px" : "25px 25px 140px 25px",
height: "calc(100vh - 80px)",
overflow: "hidden",
}}
>
<Box
sx={{
padding: isMobile ? "16px 16px 20px 16px" : "25px 25px 20px 25px",
overflow: "auto",
height: isMobile ? ` calc(100vh - 125px) ` : `calc(100vh - ${isConditionMet ? "186px" : "166px"})`,
boxSizing: "border-box",
}}
>
@ -296,26 +342,25 @@ export default function EditPage() {
quizType={quizConfig.type}
quizResults={quizConfig.results}
quizStartPageType={quizConfig.startpageType}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}
/>
</>
)}
</Box>
<Box
sx={{
position: "absolute",
left: 0,
bottom: 0,
width: "100%",
padding: isMobile ? "20px 16px" : "20px 40px 20px 250px",
padding: isMobile ? "20px 16px" : "20px 20px",
display: "flex",
justifyContent: "flex-start",
justifyContent: isMobile ? (isMobileSm ? "center" : "flex-end") : "flex-start",
flexDirection: isMobile ? "row-reverse" : "-moz-initial",
alignItems: "center",
gap: "15px",
background: "#FFF",
}}
>
{[1].includes(currentStep) && !openBranchingPanel && quizConfig.type !== "form" && (
{isConditionMet && (
<Box
sx={{
display: "flex",
@ -328,8 +373,8 @@ export default function EditPage() {
}}
>
<Switch
checked={openBranchingPanel}
onChange={(e) => updateOpenBranchingPanel(e.target.checked)}
checked={openBranchingPage}
onChange={openBranchingPageHC}
sx={{
width: 50,
height: 30,
@ -376,7 +421,8 @@ export default function EditPage() {
</Box>
)}
{!canCreatePublic && quiz.config.type !== "form" ?
<Box sx={{ display: isMobile ? "none" : "block" }}>
{!canCreatePublic && quiz.config.type !== "form" ? (
<Button
variant="contained"
// disabled
@ -386,11 +432,13 @@ export default function EditPage() {
height: "34px",
minWidth: "130px",
}}
onClick={() => Object.keys(whyCantCreatePublic).length === 0 ? () => { } : updateModalInfoWhyCantCreate(true)}
onClick={() =>
Object.keys(whyCantCreatePublic).length === 0 ? () => {} : updateModalInfoWhyCantCreate(true)
}
>
Тестовый просмотр
</Button>
:
) : (
<a href={`/view`} target="_blank" rel="noreferrer" style={{ textDecoration: "none" }}>
<Button
variant="contained"
@ -404,40 +452,105 @@ export default function EditPage() {
Тестовый просмотр
</Button>
</a>
}
)}
</Box>
<Button
variant="outlined"
variant="contained"
sx={{
fontSize: "14px",
lineHeight: "18px",
height: "34px",
border: `1px solid ${theme.palette.brightPurple.main}`,
backgroundColor: quiz?.status === "start" ? theme.palette.brightPurple.main : "transparent",
color: quiz?.status === "start" ? "#FFFFFF" : theme.palette.brightPurple.main,
background: quiz?.status === "start" ? "#7E2AEA" : "#FA5B0E",
}}
onClick={
Object.keys(whyCantCreatePublic).length === 0 ?
() => updateQuiz(quiz?.id, (state) => {
state.status = quiz?.status === "start" ? "stop" : "start";
})
:
() => updateModalInfoWhyCantCreate(true)
}
onClick={handleClickStatusQuiz}
>
{quiz?.status === "start" ? "Стоп" : "Старт"}
{buttonText === "Отозвать" ? (
<Box sx={{ display: "flex", gap: "4px", alignItems: "center" }}>
{buttonText} <BackButtonIcon />
</Box>
) : (
buttonText
)}
</Button>
{quiz?.status === "start" && <Box
{quiz?.status === "start" && (
<Box
component={Link}
sx={{
color: "#7e2aea",
fontSize: "14px"
display: isMobile ? "none" : "block",
color: "#7E2AEA",
fontSize: "14px",
}}
target="_blank" to={"https://hbpn.link/" + quiz.qid}>https://hbpn.link/{quiz.qid}
</Box>}
target="_blank"
to={"https://hbpn.link/" + quiz.qid}
>
https://hbpn.link/{quiz.qid}
</Box>
)}
{isMobile ? (
<Button
onClick={toggleQuizPreview}
variant="outlined"
sx={{
display: "flex",
gap: "4px",
fontSize: "14px",
lineHeight: "18px",
height: "34px",
border: "1px solid #7E2AEA",
color: "#7E2AEA",
background: "white",
p: "8px 14px",
}}
>
<EyeIcon />
Предпросмотр
</Button>
) : (
<IconButton
onClick={toggleQuizPreview}
sx={{
pointerEvents: "auto",
marginLeft: "auto",
position: "relative",
zIndex: "999999",
}}
>
<VisibilityIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
)}
{isMobile && quiz?.status === "start" && (
<Box
component={Link}
sx={{
cursor: "pointer",
width: "34px",
height: "34px",
color: "#7E2AEA",
fontSize: "14px",
display: "flex",
justifyContent: "center",
alignItems: "Center",
background: "#EEE4FC",
borderRadius: "8px",
}}
target="_blank"
to={"https://hbpn.link/" + quiz.qid}
>
<LinkSimple />
</Box>
)}
</Box>
</Box>
</Box>
</Box >
<ModalInfoWhyCantCreate />
<ConfirmLeaveModal
open={showConfirmLeaveModal}
follow={followNewPage}
cancel={() => setShowConfirmLeaveModal(false)}
/>
</>
);
}

@ -1,49 +1,44 @@
import { Box, Modal, Typography, Divider } from "@mui/material"
import { Box, Modal, Typography, Divider } from "@mui/material";
import { useUiTools } from "@root/uiTools/store";
import { updateModalInfoWhyCantCreate } from "@root/uiTools/actions";
import { useLayoutEffect } from "react";
export const ModalInfoWhyCantCreate = () => {
const { whyCantCreatePublic, openModalInfoWhyCantCreate } = useUiTools();
return (
<Modal
open={openModalInfoWhyCantCreate}
onClose={() => updateModalInfoWhyCantCreate(false)}
>
<Box sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
maxWidth: '620px',
width: '100%',
bgcolor: 'background.paper',
borderRadius: '12px',
<Modal open={openModalInfoWhyCantCreate} onClose={() => updateModalInfoWhyCantCreate(false)}>
<Box
sx={{
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: "25px",
minHeight: "60vh",
maxHeight: "90vh",
overflow: "auto"
overflow: "auto",
}}
>
{
Object.values(whyCantCreatePublic).map((data) => {
{Object.values(whyCantCreatePublic).map((data) => {
return (
<Box>
<Typography color="#7e2aea">У вопроса "{data.name}"</Typography>
{
data.problems.map((problem) => <Typography p="5px 0">{problem}</Typography>)
}
<Divider/>
{data.problems.map((problem) => (
<Typography p="5px 0">{problem}</Typography>
))}
<Divider />
</Box>
)
})
}
);
})}
</Box>
</Modal>
)
}
);
};

@ -42,7 +42,7 @@ export default function ModalSizeImage() {
createData("Варианты и картинка", "380х307 px"),
createData("Консультант", "140х140 px"),
createData("Логотип", "107х37 px"),
createData("", "1100х600 px"),
createData("Результаты", "1100х600 px"),
createData("Бонус", "200х60 px"),
createData('Картинка для формата вопроса "Страница"', "860х1250 px"),
];
@ -100,12 +100,8 @@ export default function ModalSizeImage() {
</IconButton>
</Box>
<Box sx={{ padding: "15px 20px 0px" }}>
<Typography
variant={"body2"}
sx={{ color: theme.palette.grey2.main, fontWeight: 400 }}
>
Рекомендованный размер зависит от того, как вы будете чаще
использовать квиз:
<Typography variant={"body2"} sx={{ color: theme.palette.grey2.main, fontWeight: 400 }}>
Рекомендованный размер зависит от того, как вы будете чаще использовать квиз:
</Typography>
</Box>
<Box sx={{ padding: "15px 40px 30px" }}>
@ -115,24 +111,30 @@ export default function ModalSizeImage() {
sx={{
display: "flex",
justifyContent: "space-between",
gap: "6px",
position: "relative",
width: "100%",
paddingBottom: "5px",
}}
>
<Box sx={{ display: "block ruby", height: "20px" }}>
<Box
sx={{
position: "absolute",
top: 18,
left: 0,
right: 0,
borderBottom: "solid 1px #F2F3F7",
}}
/>
<Box sx={{ display: "block ruby", position: "relative", zIndex: 1, background: "white" }}>
<Typography variant={"body2"} fontWeight={400}>
{name}
</Typography>
</Box>
<Box
sx={{
borderBottom: "solid 1px #F2F3F7",
width: "100%",
margin: "0 7px",
}}
></Box>
<Box sx={{ display: "block ruby", height: "20px" }}>
<Typography variant={"body2"}>{size}</Typography>
<Box sx={{ display: "block ruby", position: "relative", zIndex: 1, background: "white" }}>
<Typography sx={{ whiteSpace: "nowrap" }} variant={"body2"}>
{size}
</Typography>
</Box>
</Box>
))}
@ -153,24 +155,30 @@ export default function ModalSizeImage() {
sx={{
display: "flex",
justifyContent: "space-between",
position: "relative",
gap: "6px",
width: "100%",
paddingBottom: "5px",
}}
>
<Box sx={{ display: "block ruby", height: "20px" }}>
<Box
sx={{
position: "absolute",
top: 18,
left: 0,
right: 0,
borderBottom: "solid 1px #F2F3F7",
}}
/>
<Box sx={{ display: "block ruby", position: "relative", zIndex: 1, background: "white" }}>
<Typography variant={"body2"} fontWeight={400}>
{name}
</Typography>
</Box>
<Box
sx={{
borderBottom: "solid 1px #F2F3F7",
width: "100%",
margin: "0 7px",
}}
></Box>
<Box sx={{ display: "block ruby", height: "20px" }}>
<Typography variant={"body2"}>{size}</Typography>
<Box sx={{ display: "block ruby", position: "relative", zIndex: 1, background: "white" }}>
<Typography sx={{ whiteSpace: "nowrap" }} variant={"body2"}>
{size}
</Typography>
</Box>
</Box>
))}

@ -177,7 +177,7 @@ const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = <T = AnyTypedQuizQuestion>(
export const updateQuestion = async <T = AnyTypedQuizQuestion>(
questionId: string,
updateFn: (question: T) => void,
skipQueue = false,
@ -481,20 +481,22 @@ export const getQuestionByContentId = (questionContentId: string | null) => {
export const clearRuleForAll = () => {
const { questions } = useQuestionsStore.getState();
questions.forEach(question => {
return Promise.allSettled(
questions.map(question => {
if (question.type !== null &&
(question.content.rule.main.length > 0
|| question.content.rule.default.length > 0
|| question.content.rule.parentId.length > 0)
&& question.type !== "result") {
console.log("вызываю очистку рул вопросов")
updateQuestion(question.content.id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
}
});
})
)
};
export const createResult = async (
@ -506,7 +508,7 @@ export const createResult = async (
}
//Мы получили запрос на создание резулта. Анализируем существует ли такой. Если да - просто делаем его активным
const question = useQuestionsStore.getState().questions.find(q=> q.type !== null && q?.content.rule.parentId === parentContentId)
const question = useQuestionsStore.getState().questions.find(q => q.type !== null && q?.content.rule.parentId === parentContentId)
console.log("Получил запрос на создание результа родителю ", parentContentId)
console.log("Ищу такой же результ в списке ", question)
@ -521,7 +523,7 @@ export const createResult = async (
content.rule.parentId = parentContentId;
try {
const createdQuestion:RawQuestion = await questionApi.create({
const createdQuestion: RawQuestion = await questionApi.create({
quiz_id: quizId,
type: "result",
title: "",

@ -38,3 +38,7 @@ export const updateCanCreatePublic = (can: boolean) => useUiTools.setState({ can
export const updateModalInfoWhyCantCreate = (can: boolean) => useUiTools.setState({ openModalInfoWhyCantCreate: can });
export const updateDeleteId = (deleteNodeId: string | null = null) => useUiTools.setState({ deleteNodeId });
export const setShowConfirmLeaveModal = (showConfirmLeaveModal: boolean) => useUiTools.setState({ showConfirmLeaveModal });
export const updateSomeWorkBackend = (someWorkBackend: boolean) => useUiTools.setState({ someWorkBackend });

@ -10,8 +10,11 @@ export type UiTools = {
canCreatePublic: boolean;
whyCantCreatePublic: Record<string, WhyCantCreatePublic>//ид вопроса и список претензий к нему
openModalInfoWhyCantCreate: boolean;
deleteNodeId: string | null;
deleteNodeId: string | null;
showConfirmLeaveModal: boolean;
someWorkBackend: boolean;
};
export type WhyCantCreatePublic = {
name: string;
problems: string[]
@ -27,7 +30,9 @@ const initialState: UiTools = {
canCreatePublic: false,
whyCantCreatePublic: {},
openModalInfoWhyCantCreate: false,
deleteNodeId: null,
deleteNodeId: null,
showConfirmLeaveModal: false,
someWorkBackend: false
};
export const useUiTools = create<UiTools>()(

@ -1,7 +1,7 @@
import { FormControlLabel, Checkbox, useTheme, Box, useMediaQuery } from "@mui/material";
import React from "react";
import { CheckboxIcon } from "@icons/Checkbox";
import CheckboxIcon from "@icons/Checkbox";
import type { SxProps } from "@mui/material";
@ -11,9 +11,10 @@ interface Props {
checked?: boolean;
sx?: SxProps;
dataCy?: string;
colorIcon?: string;
}
export default function CustomCheckbox({ label, handleChange, checked, sx, dataCy }: Props) {
export default function CustomCheckbox({ label, handleChange, checked, sx, dataCy, colorIcon }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -24,7 +25,7 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
sx={{ padding: "0px 13px 1px 11px" }}
disableRipple
icon={<CheckboxIcon />}
checkedIcon={<CheckboxIcon checked />}
checkedIcon={<CheckboxIcon checked color={colorIcon} />}
onChange={handleChange}
checked={checked}
data-cy={dataCy}
@ -32,7 +33,7 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
}
label={label}
sx={{
color: theme.palette.grey2.main,
color: "#9A9AAF",
height: "26px",
...sx,
}}

@ -1,4 +1,4 @@
import { Slider, useTheme } from "@mui/material";
import {Slider, SxProps, Theme, useTheme} from "@mui/material";
type CustomSliderProps = {
defaultValue?: number;
@ -6,6 +6,7 @@ type CustomSliderProps = {
min?: number;
max?: number;
step?: number;
sx?: SxProps<Theme>
onChange?: (_: Event, value: number | number[]) => void;
onChangeCommitted?: (_: React.SyntheticEvent | Event, value: number | number[]) => void;
};
@ -18,6 +19,7 @@ export const CustomSlider = ({
step,
onChange,
onChangeCommitted,
sx,
}: CustomSliderProps) => {
// const handleChange = ({ type }: Event, newValue: number | number[]) => {
// // Для корректной работы слайдера в FireFox
@ -40,11 +42,11 @@ export const CustomSlider = ({
onMouseDown={(e) => e.stopPropagation()}
data-cy="slider"
sx={{
color: theme.palette.brightPurple.main,
color: "#7E2AEA",
padding: "0",
marginTop: "75px",
"& .MuiSlider-valueLabel": {
background: theme.palette.brightPurple.main,
background: "#7E2AEA",
borderRadius: "8px",
minWidth: "60px",
width: "auto",
@ -71,6 +73,7 @@ export const CustomSlider = ({
"& .MuiSlider-track": {
height: "12px",
},
...sx
}}
/>
);

@ -1,5 +1,11 @@
import React, { useState } from "react";
import { Box, FormControl, TextField, Typography, useTheme } from "@mui/material";
import React, { useEffect, useState } from "react";
import {
Box,
FormControl,
TextField,
Typography,
useTheme,
} from "@mui/material";
import type { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
import type { InputProps, SxProps, Theme } from "@mui/material";
@ -19,7 +25,7 @@ interface CustomTextFieldProps {
export default function CustomTextField({
placeholder,
value,
value = "",
onChange,
onKeyDown,
onBlur,
@ -32,9 +38,13 @@ export default function CustomTextField({
}: CustomTextFieldProps) {
const theme = useTheme();
const [inputValue, setInputValue] = useState(value || text || "");
const [inputValue, setInputValue] = useState("");
const [isInputActive, setIsInputActive] = useState(false);
useEffect(() => {
setInputValue(value);
}, [value]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
setInputValue(inputValue);
@ -88,8 +98,8 @@ export default function CustomTextField({
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
...sx,
},
}}
data-cy="textfield"
/>

@ -0,0 +1,149 @@
import { FC } from "react";
import { Box, Button } from "@mui/material";
import CustomTextField from "./CustomTextField";
import { updateQuestion, uploadQuestionImage } from "@root/questions/actions";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import { UploadImageModal } from "../pages/Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../utils/useDisclosure";
import { useCurrentQuiz } from "../stores/quizes/hooks";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
interface Iprops {
resultData: AnyTypedQuizQuestion;
}
export const MediaSelectionAndDisplay: FC<Iprops> = ({ resultData }) => {
const quizQid = useCurrentQuiz()?.qid;
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 (
<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) => {
q.content.video = e.target.value;
})
}
/>
</Box>
)}
</Box>
);
};

@ -1,4 +1,3 @@
import VisibilityIcon from '@mui/icons-material/Visibility';
import { Box, IconButton } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { useLayoutEffect, useRef } from "react";
@ -34,13 +33,7 @@ export default function QuizPreview() {
function stickPreviewToBottomRight() {
const rnd = rndRef.current;
const rndSelfElement = rnd?.getSelfElement();
if (
!rnd ||
!rndSelfElement ||
!rndParentRef.current ||
!isFirstShowRef.current
)
return;
if (!rnd || !rndSelfElement || !rndParentRef.current || !isFirstShowRef.current) return;
const rndParentRect = rndParentRef.current.getBoundingClientRect();
const rndRect = rndSelfElement.getBoundingClientRect();
@ -118,18 +111,6 @@ export default function QuizPreview() {
<QuizPreviewLayout />
</Rnd>
)}
<IconButton
onClick={toggleQuizPreview}
data-cy="toggle-quiz-preview"
sx={{
position: "absolute",
right: 0,
bottom: 0,
pointerEvents: "auto",
}}
>
<VisibilityIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
</Box>
);
}

@ -1,10 +1,20 @@
import { Box, Button, LinearProgress, Paper, Typography, FormControl, Select as MuiSelect, MenuItem, useTheme } from "@mui/material";
import {
Box,
Button,
LinearProgress,
Paper,
Typography,
FormControl,
Select as MuiSelect,
MenuItem,
useTheme,
} from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import {
decrementCurrentQuestionIndex,
incrementCurrentQuestionIndex,
useQuizPreviewStore,
setCurrentQuestionIndex
setCurrentQuestionIndex,
} from "@root/quizPreview";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "model/questionTypes/shared";
import { useEffect } from "react";
@ -25,19 +35,12 @@ import ArrowDownIcon from "@icons/ArrowDownIcon";
export default function QuizPreviewLayout() {
const theme = useTheme();
const questions = useQuestionsStore(state => state.questions);
const currentQuizStep = useQuizPreviewStore(
(state) => state.currentQuestionIndex
);
const questions = useQuestionsStore((state) => state.questions);
const currentQuizStep = useQuizPreviewStore((state) => state.currentQuestionIndex);
const nonDeletedQuizQuestions = questions.filter(
(question) => !question.deleted
);
const maxCurrentQuizStep =
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor(
(currentQuizStep / maxCurrentQuizStep) * 100
);
const nonDeletedQuizQuestions = questions.filter((question) => !question.deleted);
const maxCurrentQuizStep = nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor((currentQuizStep / maxCurrentQuizStep) * 100);
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
@ -84,19 +87,13 @@ export default function QuizPreviewLayout() {
}}
>
<Box sx={{ marginBottom: "10px" }}>
<FormControl
fullWidth
size="small"
sx={{ width: "100%", minWidth: "200px", height: "48px" }}
>
<FormControl fullWidth size="small" sx={{ width: "100%", minWidth: "200px", height: "48px" }}>
<MuiSelect
id="category-select"
variant="outlined"
value={currentQuizStep}
placeholder="Заголовок вопроса"
onChange={({ target }) =>
setCurrentQuestionIndex(window.Number(target.value))
}
onChange={({ target }) => setCurrentQuestionIndex(window.Number(target.value))}
sx={{
height: "48px",
borderRadius: "8px",
@ -138,8 +135,7 @@ export default function QuizPreviewLayout() {
}}
IconComponent={(props) => <ArrowDownIcon {...props} />}
>
{Object.values(questions).map(
({ id, title }, index) => (
{Object.values(questions).map(({ id, title }, index) => (
<MenuItem
key={id}
value={index}
@ -154,8 +150,7 @@ export default function QuizPreviewLayout() {
>
{`${index + 1}. ${title}`}
</MenuItem>
)
)}
))}
</MuiSelect>
</FormControl>
</Box>
@ -170,8 +165,7 @@ export default function QuizPreviewLayout() {
>
<Typography>
{nonDeletedQuizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length
}`
? `Вопрос ${currentQuizStep + 1} из ${nonDeletedQuizQuestions.length}`
: "Нет вопросов"}
</Typography>
{nonDeletedQuizQuestions.length > 0 && (
@ -218,23 +212,33 @@ export default function QuizPreviewLayout() {
);
}
function QuestionPreviewComponent({ question }: {
question: AnyTypedQuizQuestion | UntypedQuizQuestion | undefined;
}) {
function QuestionPreviewComponent({ question }: { question: AnyTypedQuizQuestion | UntypedQuizQuestion | undefined }) {
if (!question || question.type === null) return null;
switch (question.type) {
case "variant": return <Variant question={question} />;
case "images": return <Images question={question} />;
case "varimg": return <Varimg question={question} />;
case "emoji": return <Emoji question={question} />;
case "text": return <Text question={question} />;
case "select": return <Select question={question} />;
case "date": return <Date question={question} />;
case "number": return <Number question={question} />;
case "file": return <File question={question} />;
case "page": return <Page question={question} />;
case "rating": return <Rating question={question} />;
default: notReachable(question);
case "variant":
return <Variant question={question} />;
case "images":
return <Images question={question} />;
case "varimg":
return <Varimg question={question} />;
case "emoji":
return <Emoji question={question} />;
case "text":
return <Text question={question} />;
case "select":
return <Select question={question} />;
case "date":
return <Date question={question} />;
case "number":
return <Number question={question} />;
case "file":
return <File question={question} />;
case "page":
return <Page question={question} />;
case "rating":
return <Rating question={question} />;
default:
notReachable(question);
}
}

@ -1,4 +1,5 @@
import { Box, Typography } from "@mui/material";
import YoutubeEmbedIframe from "@ui_kit/StartPagePreview/YoutubeEmbedIframe";
import type { QuizQuestionPage } from "model/questionTypes/page";
@ -7,6 +8,7 @@ interface Props {
}
export default function Page({ question }: Props) {
console.log(question);
return (
<Box
sx={{
@ -16,13 +18,17 @@ export default function Page({ question }: Props) {
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title" sx={{ paddingBottom: "25px" }}>{question.title}</Typography>
<Typography data-cy="question-text" sx={{ paddingBottom: "20px" }}>{question.content.text}</Typography>
{question.content.picture && (
<Box sx={{borderRadius: "12px",
border: "1px solid #9A9AAF", width: "100%", overflow: "hidden"}}>
<Typography variant="h6" data-cy="question-title" sx={{ paddingBottom: "25px" }}>
{question.title}
</Typography>
<Typography data-cy="question-text" sx={{ paddingBottom: "20px" }}>
{question.content.text}
</Typography>
{question.content.useImage ? (
<img
src={question.content.picture}
src={question.content.back}
alt=""
style={{
display: "block",
@ -31,7 +37,8 @@ export default function Page({ question }: Props) {
objectFit: "contain",
}}
/>
</Box>
) : (
<YoutubeEmbedIframe containerSX={{ width: "100%", height: "50vh" }} videoUrl={question.content.video} />
)}
</Box>
);

@ -1,7 +1,10 @@
import { Box, useTheme } from "@mui/material";
interface Props {
color?: string
}
export default function RadioCheck() {
export default function RadioCheck( {color = "#7E2AEA"} : Props) {
const theme = useTheme();
return (
@ -16,7 +19,7 @@ export default function RadioCheck() {
}}
>
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="25" height="25" rx="12.5" fill="#7E2AEA" stroke="#7E2AEA"/>
<rect x="0.5" y="0.5" width="25" height="25" rx="12.5" fill={color} stroke={color}/>
<rect x="8" y="8" width="10" height="10" rx="5" fill="white"/>
</svg>
</Box>

@ -4,19 +4,13 @@ import PencilCircleIcon from "@icons/PencilCircleIcon";
import PuzzlePieceIcon from "@icons/PuzzlePieceIcon";
import TagIcon from "@icons/TagIcon";
import { quizSetupSteps } from "@model/quizSettings";
import {
Box,
IconButton,
List,
Typography,
useTheme
} from "@mui/material";
import { Box, IconButton, List, Typography, useTheme } from "@mui/material";
import { setCurrentStep } from "@root/quizes/actions";
import { useQuizStore } from "@root/quizes/store";
import { useState } from "react";
import MenuItem from "./MenuItem";
import {useCurrentQuiz} from "@root/quizes/hooks";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { setShowConfirmLeaveModal } from "@root/uiTools/actions";
const quizSettingsMenuItems = [
[TagIcon, "Дополнения"],
@ -25,15 +19,29 @@ const quizSettingsMenuItems = [
[GearIcon, "Настройки"],
] as const;
export default function Sidebar() {
type SidebarProps = {
setNextStep: (step: number) => void;
};
export default function Sidebar({ setNextStep }: SidebarProps) {
const theme = useTheme();
const [isMenuCollapsed, setIsMenuCollapsed] = useState(false);
const currentStep = useQuizStore(state => state.currentStep);
const currentStep = useQuizStore((state) => state.currentStep);
const quiz = useCurrentQuiz();
const handleMenuCollapseToggle = () => setIsMenuCollapsed((prev) => !prev);
const changePage = (index: number) => {
if (currentStep === 2) {
setNextStep(index);
setShowConfirmLeaveModal(true);
return;
}
setCurrentStep(index);
};
return (
<Box
sx={{
@ -49,7 +57,7 @@ export default function Sidebar() {
overflow: "hidden",
whiteSpace: "nowrap",
boxSizing: "border-box",
zIndex: 1
zIndex: 1,
}}
>
<Box
@ -92,12 +100,18 @@ export default function Sidebar() {
return (
<MenuItem
onClick={() => setCurrentStep(index)}
onClick={() => changePage(index)}
key={index}
text={menuItem.sidebarText}
isCollapsed={isMenuCollapsed}
isActive={currentStep === index}
disabled={index===0 ? false : quiz===undefined ? true : (quiz?.config.type === null)}
disabled={
index === 0
? false
: quiz === undefined
? true
: quiz?.config.type === null
}
icon={
<Icon
color={

@ -1,20 +1,10 @@
import {
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, Button, ButtonBase, Link, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import YoutubeEmbedIframe from "./YoutubeEmbedIframe";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice";
export default function QuizPreviewLayout() {
const theme = useTheme();
const quiz = useCurrentQuiz();
@ -26,9 +16,9 @@ export default function QuizPreviewLayout() {
navigator.clipboard.writeText(quiz.config.info.phonenumber);
};
const background = quiz.config.startpage.background.type === "image"
? quiz.config.startpage.background.desktop
? (
const background =
quiz.config.startpage.background.type === "image" ? (
quiz.config.startpage.background.desktop ? (
<img
src={quiz.config.startpage.background.desktop}
alt=""
@ -38,25 +28,25 @@ export default function QuizPreviewLayout() {
objectFit: "cover",
}}
/>
)
: null
: quiz.config.startpage.background.type === "video"
? quiz.config.startpage.background.video
? (
) : null
) : quiz.config.startpage.background.type === "video" ? (
quiz.config.startpage.background.video ? (
<YoutubeEmbedIframe videoUrl={quiz.config.startpage.background.video} />
)
: null
: null;
) : null
) : null;
return (
<Paper className="quiz-preview-draghandle" sx={{ height: "100%" }}>
<QuizPreviewLayoutByType
quizHeaderBlock={<>
<Box sx={{
quizHeaderBlock={
<>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
}}>
}}
>
{quiz.config.startpage.logo && (
<img
src={quiz.config.startpage.logo}
@ -68,25 +58,26 @@ export default function QuizPreviewLayout() {
alt=""
/>
)}
<Typography sx={{ fontSize: "12px" }}>
{quiz.config.info.orgname}
</Typography>
<Typography sx={{ fontSize: "12px" }}>{quiz.config.info.orgname}</Typography>
</Box>
</>}
quizMainBlock={<>
<Box sx={{
</>
}
quizMainBlock={
<>
<Box
sx={{
display: "flex",
gap: "10px",
flexDirection: "column",
justifyContent: "center",
alignItems: (quiz.config.startpageType === "expanded" && quiz.config.startpage.position === "center")
alignItems:
quiz.config.startpageType === "expanded" && quiz.config.startpage.position === "center"
? "center"
: "start",
}}>
}}
>
<Typography sx={{ fontWeight: "bold" }}>{quiz.name}</Typography>
<Typography sx={{ fontSize: "12px" }}>
{quiz.config.startpage.description}
</Typography>
<Typography sx={{ fontSize: "12px" }}>{quiz.config.startpage.description}</Typography>
<Box>
<Button
variant="contained"
@ -119,11 +110,10 @@ export default function QuizPreviewLayout() {
{quiz.config.info.phonenumber}
</Typography>
)}
<Typography sx={{ fontSize: "12px" }}>
{quiz.config.info.law}
</Typography>
<Typography sx={{ fontSize: "12px" }}>{quiz.config.info.law}</Typography>
</Box>
</>}
</>
}
backgroundBlock={background}
startpageType={quiz.config.startpageType}
alignType={quiz.config.startpage.position}
@ -132,7 +122,13 @@ export default function QuizPreviewLayout() {
);
}
function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlock, startpageType, alignType }: {
function QuizPreviewLayoutByType({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
}: {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
@ -146,27 +142,33 @@ function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlo
case null:
case "standard": {
return (
<Box sx={{
<Box
sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
}}
>
<Box
sx={{
width: !isTablet ? "40%" : "100%",
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: !isTablet ? "flex-start" : "center",
}}>
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
<Box
sx={{
width: "60%",
}}>
}}
>
{backgroundBlock}
</Box>
</Box>
@ -174,15 +176,18 @@ function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlo
}
case "expanded": {
return (
<Box sx={{
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}>
<Box sx={{
}}
>
<Box
sx={{
width: "40%",
position: "relative",
padding: "16px",
@ -191,18 +196,21 @@ function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlo
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}>
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box sx={{
<Box
sx={{
position: "absolute",
left: 0,
top: 0,
height: "100%",
width: "100%",
zIndex: 1,
}}>
}}
>
{backgroundBlock}
</Box>
</Box>
@ -210,7 +218,8 @@ function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlo
}
case "centered": {
return (
<Box sx={{
<Box
sx={{
padding: "16px",
display: "flex",
flexDirection: "column",
@ -218,18 +227,16 @@ function QuizPreviewLayoutByType({ quizHeaderBlock, quizMainBlock, backgroundBlo
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}>
}}
>
{quizHeaderBlock}
{backgroundBlock &&
<Box>
{backgroundBlock}
</Box>
}
{backgroundBlock && <Box>{backgroundBlock}</Box>}
{quizMainBlock}
</Box>
);
}
default: notReachable(startpageType);
default:
notReachable(startpageType);
}
}

@ -1,4 +1,3 @@
import VisibilityIcon from "@mui/icons-material/Visibility";
import { Box, IconButton, useTheme, useMediaQuery } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { useLayoutEffect, useRef } from "react";
@ -35,13 +34,7 @@ export const StartPagePreview = () => {
function stickPreviewToBottomRight() {
const rnd = rndRef.current;
const rndSelfElement = rnd?.getSelfElement();
if (
!rnd ||
!rndSelfElement ||
!rndParentRef.current ||
!isFirstShowRef.current
)
return;
if (!rnd || !rndSelfElement || !rndParentRef.current || !isFirstShowRef.current) return;
const rndParentRect = rndParentRef.current.getBoundingClientRect();
const rndRect = rndSelfElement.getBoundingClientRect();
@ -119,17 +112,6 @@ export const StartPagePreview = () => {
<QuizPreviewLayout />
</Rnd>
)}
<IconButton
onClick={toggleQuizPreview}
sx={{
position: "absolute",
right: 0,
bottom: 0,
pointerEvents: "auto",
}}
>
<VisibilityIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
</Box>
);
};

@ -16,6 +16,8 @@ interface Props {
quizType: QuizType;
quizStartPageType: QuizStartpageType;
quizResults: QuizResultsType;
openBranchingPage: boolean;
setOpenBranchingPage: (a:boolean) => void;
}
export default function SwitchStepPages({
@ -23,6 +25,8 @@ export default function SwitchStepPages({
quizType,
quizStartPageType,
quizResults,
openBranchingPage,
setOpenBranchingPage
}: Props) {
switch (activeStep) {
case 0: {
@ -30,7 +34,10 @@ export default function SwitchStepPages({
if (!quizStartPageType) return <Steptwo />;
return <StartPageSettings />;
}
case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />;
case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}
/>;
case 2: return <ResultPage />;
case 3: return <ContactFormPage />;
case 4: return <InstallQuiz />;

@ -0,0 +1,82 @@
import { AnyTypedQuizQuestion, QuestionBranchingRuleMain } from "@model/questionTypes/shared";
import { WhyCantCreatePublic } from "@root/uiTools/store";
import { getQuestionByContentId, updateQuestion } from "@root/questions/actions";
export const checkQuestionHint = (questions: AnyTypedQuizQuestion): Record<string, WhyCantCreatePublic> => {
const problems: any = {}
const pushProblem = (id: string, problem: string, title: string) => {
//Если первый вопрос с проблемой - создаём запись. Если не первый - добавляем проблему
if (id in problems) {
problems[id].problems.push(problem)
} else {
problems[id] = {
name: title,
problems: [problem]
}
}
}
questions.forEach((question: AnyTypedQuizQuestion) => {
//Если не участвует в ветвлении, или безтиповый, или резулт - он нам не интересен
if (question.type === null
|| question.type === "result"
|| question.content.rule.parentId.length === 0) return
if (
question?.type === "date" ||
question?.type === "text" ||
question?.type === "number" ||
question?.type === "page"
) {//Если у вопроса типа страница, ползунок, своё поле для ввода и дата есть ребёнок, но нет дефолта - молча его добавляем.
if (question.content.rule.children.length === 1 && question.content.rule.default.length === 0)
updateQuestion(question.id, (q) => {
question.content.rule.default = question.content.rule.children[0]
})
} else {
//если есть дети, но нет дефолта - логическая ошибка. Так нельзя
if (question.content.rule.children.length > 0 && question.content.rule.default.length === 0) {
pushProblem(question.content.id, "Не выбран дефолтный вопрос", question.title)
}
}
//Rules вопроса не должны совпадать
const buffer: QuestionBranchingRuleMain[] = []
question.content.rule.main.forEach((condition: QuestionBranchingRuleMain) => {
buffer.forEach((oldCondition: QuestionBranchingRuleMain) => {
if (areRulesEqual(condition.rules, oldCondition.rules)) {
pushProblem(
question.content.id,
`У вопроса "${getQuestionByContentId(condition.next)?.title || "noname"}" и "${getQuestionByContentId(oldCondition.next)?.title || "noname"}" одинаковые условия ветвления`,
question.title
)
}
})
buffer.push(condition)
})
})
return problems
}
const areRulesEqual = (first: any, second: any) => {
const firstArray = first[0].answers
const secondArray = second[0].answers
if (firstArray.length === secondArray.length) {
if (firstArray.length > 1) {
if (
firstArray.every((element: any, index: number) => element === secondArray[index])
&&
first.or === second.or
) return true;
} else {
if (firstArray[0] === secondArray[0]) return true;
}
}
return false;
};

67
src/utils/deleteFunc.ts Normal file

@ -0,0 +1,67 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { clearRuleForAll, deleteQuestion, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions";
//Всё здесь нужно сделать последовательно. И пусть весь мир ждёт.
export const DeleteFunction = async (questions: any, question: any, quiz: any) => {
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "");
await clearRuleForAll();
console.log("очистка рулов закончилась")
await deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[];
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.type !== null && targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type !== "result" && targetQuestion.type !== null) {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id);
getChildren(targetQuestion); //и ищем его потомков
}
}
});
};
getChildren(question);
//чистим потомков от инфы ветвления
await Promise.allSettled(
clearQuestions.map((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = "";
question.content.rule.children = [];
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 = 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);
await updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule;
});
await deleteQuestion(question.id);
}
await deleteQuestion(question.id);
const result = questions.find(q => q.type === "result" && q.content.rule.parentId === question.content.id)
if (result) await deleteQuestion(result.id);
} else {
await deleteQuestion(question.id);
}
}

@ -0,0 +1,25 @@
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
import { Quiz } from "@model/quiz/quiz";
import { updateSomeWorkBackend } from "@root/uiTools/actions";
import { DeleteFunction } from "@utils/deleteFunc";
type allQuestionsTypes = AnyTypedQuizQuestion | UntypedQuizQuestion
export const deleteTimeoutedQuestions = async (questions: allQuestionsTypes[], quiz: Quiz|undefined) => {
console.log("Я отвечаю за удаление неудалёнышей при переключении. Привет, буде знакомы")
const questionsForDeletion = questions.filter(
({ type, deleted }) => type && type !== "result" && deleted
) as AnyTypedQuizQuestion[];
if (questionsForDeletion.length > 0) {
console.log("меняю занятость беком на true")
updateSomeWorkBackend(true)
await Promise.allSettled(
questionsForDeletion.map(question => DeleteFunction(questions, question, quiz))
)
console.log("______________меняю на 'можно редактировать дальше'______________")
updateSomeWorkBackend(false)
}
};

@ -0,0 +1,43 @@
import { createTheme } from "@mui/material";
import theme from "../generic";
const themePublic = createTheme({
...theme,
components: {
MuiButton: {
variants: [
{
props: {
variant: 'contained'
},
style: {
padding: '13px 20px',
borderRadius: '8px',
boxShadow: "none",
// "&:hover": {
// backgroundColor: "#581CA7"
// }
},
},
{
props: {
variant: 'outlined'
},
style: {
padding: '10px 20px',
borderRadius: '8px',
"&:hover": {
backgroundColor: "#581CA7",
border: '1px solid #581CA7',
}
},
},
],
},
},
},
)
export default themePublic;

@ -0,0 +1,253 @@
import { createTheme } from "@mui/material";
import themePublic from "./genericPublication";
import theme from "../generic";
const StandardTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#7E2AEA",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#7E2AEA",
},
background: {
default: "#FFFFFF",
},
}
})
const StandardDarkTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#7E2AEA",
},
secondary: {
main: "#252734"
},
text: {
primary: "#FFFFFF",
secondary: "#7E2AEA",
},
background: {
default: "#333647",
},
}
})
const PinkTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#D34085",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#D34085",
},
background: {
default: "#FFF9FC",
},
}
})
const PinkDarkTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#D34085",
},
secondary: {
main: "#252734"
},
text: {
primary: "#FFFFFF",
secondary: "#D34085",
},
background: {
default: "#333647",
},
}
})
const BlackWhiteTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#4E4D51",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#FFF9FC",
},
background: {
default: "#FFFFFF",
},
}
})
const OliveTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#758E4F",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#758E4F",
},
background: {
default: "#F9FBF1",
},
}
})
const PurpleTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#7E2AEA",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#7E2AEA",
},
background: {
default: "#FBF8FF",
},
}
})
const YellowTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#F2B133",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#F2B133",
},
background: {
default: "#FFFCF6",
},
}
})
const GoldDarkTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#E6AA37",
},
secondary: {
main: "#FFFCF6",
},
text: {
primary: "#FFFFFF",
secondary: "#F2B133",
},
background: {
default: "#333647",
},
}
})
const BlueTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#4964ED",
},
secondary: {
main: "#252734"
},
text: {
primary: "#333647",
secondary: "#4964ED",
},
background: {
default: "#F5F7FF",
},
}
})
const BlueDarkTheme = createTheme({
...themePublic,
palette: {
primary: {
main: "#07A0C3",
},
secondary: {
main: "#252734"
},
text: {
primary: "#FFFFFF",
secondary: "#07A0C3",
},
background: {
default: "#333647",
},
}
})
export const modes = {
StandardTheme: true,
StandardDarkTheme: false,
PinkTheme: true,
PinkDarkTheme: false,
BlackWhiteTheme: true,
OliveTheme: true,
YellowTheme: true,
GoldDarkTheme: false,
PurpleTheme: true,
BlueTheme: true,
BlueDarkTheme: false
}
export const themesPublication = {
StandardTheme,
StandardDarkTheme,
PinkTheme,
PinkDarkTheme,
BlackWhiteTheme,
OliveTheme,
YellowTheme,
GoldDarkTheme,
PurpleTheme,
BlueTheme,
BlueDarkTheme,
}

@ -16,6 +16,9 @@
],
"@model/*": [
"./model/*"
],
"@utils/*": [
"./utils/*"
]
}
}