Merge branch 'backend-integration' into view-logic

This commit is contained in:
Nastya 2023-12-09 14:38:39 +03:00
commit 5ea76afceb
40 changed files with 1448 additions and 545 deletions

@ -12,10 +12,10 @@ import InstallQuiz from "./pages/InstallQuiz/InstallQuiz";
import Landing from "./pages/Landing/Landing";
import QuestionsPage from "./pages/Questions/QuestionsPage";
import { Result } from "./pages/ResultPage/Result";
import { Setting } from "./pages/ResultPage/Setting";
import { ResultSettings } from "./pages/ResultPage/ResultSettings";
import MyQuizzesFull from "./pages/createQuize/MyQuizzesFull";
import Main from "./pages/main";
import StartPage from "./pages/startPage/StartPage";
import EditPage from "./pages/startPage/EditPage";
import { clearAuthToken, getMessageFromFetchError, useUserFetcher } from "@frontend/kitui";
import { clearUserData, setUser, useUserStore } from "@root/user";
import { enqueueSnackbar } from "notistack";
@ -28,7 +28,7 @@ const routeslink = [
{ path: "/questions/:quizId", page: <QuestionsPage />, header: true, sidebar: true, },
{ path: "/contacts", page: <ContactFormPage />, header: true, sidebar: true },
{ path: "/result", page: <Result />, header: true, sidebar: true },
{ path: "/settings", page: <Setting />, header: true, sidebar: true },
{ path: "/settings", page: <ResultSettings />, header: true, sidebar: true },
{ path: "/install", page: <InstallQuiz />, header: true, sidebar: true },
] as const;
@ -57,7 +57,7 @@ export default function App() {
{routeslink.map((e, i) => (
<Route key={i} path={e.path} element={<Main page={e.page} header={e.header} sidebar={e.sidebar} />} />
))}
<Route path="edit" element={<StartPage />} />
<Route path="edit" element={<EditPage />} />
<Route path="crop" element={<ImageCrop />} />
<Route path="/" element={<Landing />} />
<Route path="/signin" element={<SigninDialog />} />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 823 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

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

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

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 494 B

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

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

@ -10,6 +10,7 @@ import type { QuizQuestionSelect } from "./select";
import type { QuizQuestionText } from "./text";
import type { QuizQuestionVariant } from "./variant";
import type { QuizQuestionVarImg } from "./varimg";
import type { QuizQuestionResult } from "./result";
import { nanoid } from "nanoid";
export interface QuestionBranchingRuleMain {
@ -92,7 +93,8 @@ export type AnyTypedQuizQuestion =
| QuizQuestionNumber
| QuizQuestionFile
| QuizQuestionPage
| QuizQuestionRating;
| QuizQuestionRating
| QuizQuestionResult;
type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[]; };

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

@ -122,7 +122,9 @@ function CsComponent ({
}: Props) {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions, desireToOpenABranchingModal } = useQuestionsStore()
const { dragQuestionContentId, desireToOpenABranchingModal } = useQuestionsStore()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result")
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");

@ -12,7 +12,7 @@ interface Props {
}
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, questions } = useQuestionsStore()
const { dragQuestionContentId } = useQuestionsStore()
const Container = useRef<HTMLDivElement | null>(null);
const modalOpen = () => setOpenedModalQuestions(true)

@ -15,7 +15,8 @@ export const BranchingQuestionsModal = ({
setModalQuestionTargetContentId,
setModalQuestionParentContentId
}: Props) => {
const { questions } = useQuestionsStore();
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result")
const handleClose = () => {
setOpenedModalQuestions(false);

@ -39,7 +39,7 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -99,7 +99,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
<TextField
defaultValue={question.title}
placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => setTitle(target.value)}
onChange={({ target }: { target: HTMLInputElement; }) => setTitle(target.value)}
InputProps={{
startAdornment: (
<Box>
@ -239,7 +239,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
margin: "0 5px 0 10px",
}}
onClick={() => { // TODO
const removedId = question.id;
const removedId = question.id;
// if (question.deleteTimeoutId) {
// clearTimeout(question.deleteTimeoutId);
// }
@ -265,26 +265,28 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
</IconButton>
</Box>
)}
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
>
{index + 1}
</Box>
{question.type !== null &&
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "30px",
width: "30px",
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
}}
>
{question.page + 1}
</Box>
}
<IconButton
disableRipple
sx={{

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

@ -48,7 +48,8 @@ export default function QuestionsPage() {
margin: "60px 0 40px 0",
}}
>
<Typography variant={"h5"}>Заголовок квиза</Typography>
<Typography variant={"h5"}>{
quiz.name ? quiz.name : "Заголовок квиза" }</Typography>
<Button
sx={{
display: openBranchingPanel ? "none" : "flex",

@ -24,7 +24,9 @@ const getItemStyle = (isDragging: any, draggableStyle: any) => ({
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
export const QuestionsList = () => {
const { questions, desireToOpenABranchingModal } = useQuestionsStore()
const { desireToOpenABranchingModal } = useQuestionsStore()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result")
return (
<Box sx={{ padding: "15px" }}>
@ -85,7 +87,7 @@ export const QuestionsList = () => {
updateEditSomeQuestion(content.id)
}}
>
<Pencil />
<Pencil style={{color: "#7e2aea"}}/>
</IconButton>
{content.rule.parentId && <CheckedIcon />}
</Button>

@ -39,24 +39,6 @@ const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
whiteSpace: "nowrap",
},
},
{
title: "ƒ",
type: "ƒ",
sx: {
width: "38px",
height: "48px",
border: "1px solid #9A9AAF",
},
},
{
title: "Скидка",
type: "discount",
sx: {
width: "93px",
height: "48px",
border: "1px solid #9A9AAF",
},
},
];
type Props = {
@ -74,9 +56,6 @@ export default function PriceButtons({
<Typography component={"h6"} sx={{ weight: "500", fontSize: "18px" }}>
Стоимость
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
</Box>
<Box
component="div"

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

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

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

@ -2,19 +2,31 @@ import IconPlus from "@icons/IconPlus";
import Info from "@icons/Info";
import Plus from "@icons/Plus";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { Box, Button, Typography } from "@mui/material";
import { Box, Button, Typography, Paper, FormControl, 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 { useState } from "react";
import { WhenCard } from "./cards/WhenCard";
import { ResultCard } from "./cards/ResultCard";
import { EmailSettingsCard } from "./cards/EmailSettingsCard";
import { useCurrentQuiz } from "@root/quizes/hooks"
export const ResultSettings = () => {
const quiz = useCurrentQuiz()
const [quizExpand, setQuizExpand] = useState(true)
const [resultContract, setResultContract] = useState(true)
export const Setting = () => {
return (
<Box sx={{ maxWidth: "796px" }}>
<Box sx={{ display: "flex", alignItems: "center", mb: "40px" }}>
<Typography sx={{ pr: "10px" }} variant="h5">
<Box sx={{
display: "flex",
alignItems: "center",
margin: "60px 0 40px 0",
}}>
<Typography variant="h5">
Настройки результатов
</Typography>
<Info />
@ -33,12 +45,17 @@ export const Setting = () => {
},
}}
variant="text"
onClick={() => setQuizExpand(!quizExpand)}
>
Свернуть
</Button>
</Box>
<CustomWrapper sx={{ mt: "30px" }} text="Показывать результат" />
<CustomWrapper sx={{ mt: "30px" }} text="Настройки почты" />
<WhenCard quizExpand={quizExpand} />
{quiz.config.resultInfo.when === "email" && <EmailSettingsCard quizExpand={quizExpand} />}
<Box
sx={{ display: "flex", alignItems: "center", mb: "15px", mt: "15px" }}
>
@ -60,52 +77,14 @@ export const Setting = () => {
},
}}
variant="text"
onClick={() => setResultContract(!resultContract)}
>
Развернуть все
</Button>
</Box>
<CustomWrapper result={true} text="Показывать результат" />
<Box
sx={{
display: "flex",
width: "100%",
alignItems: "center",
columnGap: "10px",
}}
>
<Box
sx={{
boxSizing: "border-box",
width: "100%",
height: "1px",
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage:
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/>
<IconPlus />
</Box>
<CustomWrapper result={true} text="Настройки почты" />
<Box sx={{ pt: "30px", display: "flex", alignItems: "center" }}>
<Plus />
<Typography component="div" sx={{ ml: "auto" }}>
<Button
variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px" }}
>
<ArrowLeft />
</Button>
<Button variant="contained" sx={{ ml: "10px" }} onClick={incrementCurrentStep}>
Настроить форму
</Button>
</Typography>
</Box>
<SettingForm />
<ResultListForm />
<DescriptionForm />
<ResultCard resultContract={resultContract} />
</Box>
);
};

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

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

@ -0,0 +1,291 @@
import { useEffect, useState } from "react";
import { updateQuiz } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { SwitchSetting } from "../SwichResult";
import {
Box,
IconButton,
Paper,
Button,
Typography,
TextField,
useMediaQuery,
useTheme,
} from "@mui/material";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import ShareNetwork from "@icons/ShareNetwork.svg";
import ArrowCounterClockWise from "@icons/ArrowCounterClockWise.svg";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import SwitchResult from "../DescriptionForm/SwitchResult";
import ButtonsOptionsForm from "../DescriptionForm/ButtinsOptionsForm";
import PriceButtons from "../DescriptionForm/PriceButton";
import DiscountButtons from "../DescriptionForm/DiscountButtons";
import CustomTextField from "@ui_kit/CustomTextField";
import { OneIcon } from "@icons/questionsPage/OneIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import Info from "@icons/Info";
import ImageAndVideoButtons from "../DescriptionForm/ImageAndVideoButtons";
interface Props {
resultContract: boolean;
}
export const ResultCard = ({ resultContract }:Props) => {
const quiz = useCurrentQuiz()
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1100));
const [expand, setExpand] = useState(true)
useEffect(() => {
setExpand(true)
}, [resultContract])
const [switchState, setSwitchState] = useState<string>("");
const [priceButtonsActive, setPriceButtonsActive] = useState<number>(0);
const [priceButtonsType, setPriceButtonsType] = useState<string>();
const [forwarding, setForwarding] = useState<boolean>(false);
const buttonsActive = (index: number, type: string) => {
setPriceButtonsActive(index);
setPriceButtonsType(type);
};
const SSHC = (data: string) => {
setSwitchState(data);
};
return(
<Paper
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<Typography
sx={{
margin: isMobile ? "10px 0" : 0,
color: expand ? "#9A9AAF" : "#000000",
}}
>
Заголовок результата
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
</Box>
</Box>
{expand && (
<Box
sx={{
overflow: "hidden",
maxWidth: "796px",
height: "100%",
bgcolor: "#FFFFFF",
borderRadius: "12px",
boxShadow: "0px 10px 30px #e7e7e7",
}}
>
<Box sx={{ p: "0 20px", pt: "30px" }}>
<Box
sx={{
width: "100%",
maxWidth: "760px",
display: "flex",
alignItems: "center",
gap: "10px",
mb: "19px",
}}
>
<CustomTextField placeholder="Заголовок вопроса" text={""} />
<IconButton>
<ExpandMoreIcon />
</IconButton>
<OneIcon />
<PointsIcon style={{ color: "#9A9AAF" }} />
</Box>
<Box sx={{ display: "flex" }}>
<PriceButtons
ButtonsActive={buttonsActive}
priceButtonsActive={priceButtonsActive}
/>
</Box>
<TextField
fullWidth
placeholder="Описание"
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "100%",
height: "110px",
borderRadius: "10px",
},
}}
inputProps={{
sx: {
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
<ImageAndVideoButtons />
{priceButtonsType === "smooth" ? (
<Box sx={{ mb: "20px" }}>
<Box sx={{ display: "flex", alignItems: "center", mb: "14xp" }}>
<Typography
component={"h6"}
sx={{ weight: "500", fontSize: "18px" }}
>
Призыв к действию
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
</Box>
<Box sx={{ display: "flex" }}>
<TextField
placeholder="Узнать подробнее"
fullWidth
sx={{
width: "410px",
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "410px",
height: "48px",
borderRadius: "10px",
},
}}
inputProps={{
sx: {
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
<Button
onClick={() => setForwarding(true)}
variant="outlined"
sx={{
display: forwarding ? "none" : "",
ml: "20px",
mb: "20px",
}}
>
Переадресация +
</Button>
{forwarding ? (
<Box sx={{ ml: "20px", mt: "-36px" }}>
<Box
sx={{ display: "flex", alignItems: "center", mb: "14xp" }}
>
<Typography
component={"h6"}
sx={{ weight: "500", fontSize: "18px" }}
>
Переадресация
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<Info />
</Box>
<Box>
<TextField
placeholder="https://exemple.ru"
fullWidth
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "326px",
height: "48px",
borderRadius: "10px",
},
}}
inputProps={{
sx: {
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
</Box>
</Box>
) : (
<></>
)}
</Box>
</Box>
) : (
<Button variant="outlined" sx={{ mb: "20px" }}>
Кнопка +
</Button>
)}
</Box>
<ButtonsOptionsForm switchState={switchState} SSHC={SSHC} />
<SwitchResult switchState={switchState} totalIndex={0} />
</Box>
)}
</Paper>
)
}

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

@ -1,4 +1,4 @@
import { useLayoutEffect, useState } from "react";
import { useEffect, useState } from "react";
import { Box } from "@mui/material";
import { StartPageViewPublication } from "./StartPageViewPublication";
@ -13,6 +13,13 @@ export const ViewPage = () => {
const { questions } = useQuestions();
const [visualStartPage, setVisualStartPage] = useState<boolean>(!quiz?.config.noStartPage);
useEffect(() => {
const link = document.querySelector('link[rel="icon"]');
if (link && quiz?.config.startpage.favIcon) {
link.setAttribute("href", quiz.config.startpage.favIcon);
}
}, [quiz?.config.startpage.favIcon]);
const filteredQuestions = questions.filter(
({ type }) => type

@ -5,7 +5,7 @@ import {
RadioGroup,
FormControlLabel,
Radio,
useTheme,
useTheme, FormControl,
} from "@mui/material";
import { useQuizViewStore, updateAnswer } from "@root/quizView";
@ -50,31 +50,49 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
<Box sx={{ display: "flex", width: "100%", gap: "42px" }}>
{currentQuestion.content.variants.map(
({ id, answer, extendedText }, index) => (
<FormControlLabel
key={id}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.grey2.main,
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
gap: "10px",
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography>{extendedText}</Typography>
<Typography>{answer}</Typography>
<FormControl
key={id}
sx={{
borderRadius: "12px",
border: `1px solid ${theme.palette.grey2.main}`,
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px"
}}
>
<Box
sx={{ display: "flex", alignItems: "center", height: "193px", background: "#ffffff" }}
>
<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}>
{extendedText && (
<Typography fontSize={"100px"}>{extendedText}</Typography>
)}
</Box>
</Box>
}
/>
<FormControlLabel
key={id}
sx={{
margin: 0,
padding: "15px",
color: "#4D4D4D",
display: "flex",
gap: "10px",
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography>{answer}</Typography>
</Box>
}
/>
</FormControl>
)
)}
</Box>

@ -91,7 +91,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
)}
</Box>
</Box>
<FormControl
<FormControlLabel
key={id}
sx={{
display: "block",

@ -34,7 +34,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
return (
<Box>
<Typography variant="h5">{currentQuestion.title}</Typography>
<Box sx={{ display: "flex" }}>
<Box sx={{ display: "flex", marginTop: "20px", }}>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
@ -50,7 +50,6 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
@ -61,7 +60,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.grey2.main,
color: "#4D4D4D",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
}}
@ -75,7 +74,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
</Box>
</RadioGroup>
{(variant?.extendedText || currentQuestion.content.back) && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<Box sx={{ maxWidth: "450px", width: "100%", height: "450px", border: "1px solid #E3E3E3", borderRadius: "12px", overflow: "hidden", }}>
<img
src={answer ? variant?.extendedText : currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}

@ -32,9 +32,10 @@ import { SidebarMobile } from "./Sidebar/SidebarMobile";
import {cleanQuestions, updateOpenBranchingPanel} from "@root/questions/actions";
import {BranchingPanel} from "../Questions/BranchingPanel";
import {useQuestionsStore} from "@root/questions/store";
import { useQuestions } from "@root/questions/hooks";
export default function StartPage() {
export default function EditPage() {
useSWR("quizes", () => quizApi.getList(), {
onSuccess: setQuizes,
onError: error => {
@ -44,6 +45,9 @@ export default function StartPage() {
enqueueSnackbar(`Не удалось получить квизы. ${message}`);
},
});
// if (isLoading && !questions) return <Box>Загрузка вопросов...</Box>;
const theme = useTheme();
const navigate = useNavigate();
const editQuizId = useQuizStore(state => state.editQuizId);
@ -54,6 +58,7 @@ export default function StartPage() {
const [mobileSidebar, setMobileSidebar] = useState<boolean>(false);
const {openBranchingPanel} = useQuestionsStore.getState()
const quizConfig = quiz?.config;
const { questions, isLoading } = useQuestions();
useEffect(() => {
if (editQuizId === null) navigate("/list");
@ -214,6 +219,7 @@ export default function StartPage() {
boxSizing: "border-box",
}}
>
{/* Выбор текущей страницы редактирования чего-либо находится здесь */}
{quizConfig &&
<>
<Stepper activeStep={currentStep} />

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

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

@ -1,4 +1,3 @@
import { questionApi } from "@api/question";
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
@ -18,10 +17,10 @@ import { withErrorBoundary } from "react-error-boundary";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
const untypedQuestions = state.questions.filter(q => q.type === null);
const untypedResultQuestions = state.questions.filter(q => q.type === null || q.type === "result");
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
state.questions.push(...untypedQuestions);
state.questions.push(...untypedResultQuestions);
}, {
type: "setQuestions",
questions,
@ -87,6 +86,18 @@ const setQuestionBackendId = (questionId: string, backendId: number) => setProdu
backendId,
});
const updateQuestionOrders = () => {
const questions = useQuestionsStore.getState().questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
questions.forEach((question, index) => {
updateQuestion(question.id, question => {
question.page = index;
}, true);
});
};
export const reorderQuestions = (
sourceIndex: number,
destinationIndex: number,
@ -101,6 +112,8 @@ export const reorderQuestions = (
sourceIndex,
destinationIndex,
});
updateQuestionOrders();
};
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
@ -125,6 +138,7 @@ let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
skipQueue = false,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.type !== null && q.content.id === questionId);
@ -139,21 +153,31 @@ export const updateQuestion = (
});
// clearTimeout(requestTimeoutId);
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const request = async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.type !== null && q.content.id === questionId);
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
const response = await questionApi.edit(questionToEditQuestionRequest(q));
try {
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
setQuestionBackendId(questionId, response.updated);
} catch (error) {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
}
};
if (skipQueue) {
request();
return;
}
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(request);
// }, REQUEST_DEBOUNCE);
};
@ -263,13 +287,13 @@ export const changeQuestionType = (
type: QuestionType,
) => {
updateQuestion(questionId, question => {
const oldId = question.content.id
const oldRule = question.content.rule
oldRule.main = []
const oldId = question.content.id;
const oldRule = question.content.rule;
oldRule.main = [];
question.type = type;
question.content = defaultQuestionByType[type].content;
question.content.id = oldId
question.content.rule = oldRule
question.content.id = oldId;
question.content.rule = oldRule;
});
};
@ -277,7 +301,8 @@ export const createTypedQuestion = async (
questionId: string,
type: QuestionType,
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
const questions = useQuestionsStore.getState().questions;
const question = questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
@ -287,7 +312,7 @@ export const createTypedQuestion = async (
type,
title: question.title,
description: question.description,
page: 0,
page: questions.length,
required: false,
content: JSON.stringify(defaultQuestionByType[type].content),
});
@ -310,8 +335,8 @@ export const createTypedQuestion = async (
});
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
const { questions } = useQuestionsStore.getState()
const question = questions.find(q => q.id === questionId);
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null) {
@ -394,9 +419,9 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId
copiedQuestion.content.id = frontId
copiedQuestion.content.rule = { main: [], parentId: "", default: "" }
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = { main: [], parentId: "", default: "" };
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -405,6 +430,8 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
questionId,
quizId,
});
updateQuestionOrders();
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
@ -439,7 +466,7 @@ export const getQuestionByContentId = (questionContentId: string | null) => {
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
export const updateDragQuestionContentId = (contentId?: string) => {
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
}
};
export const clearRuleForAll = () => {
const { questions } = useQuestionsStore.getState()
@ -472,15 +499,22 @@ export const updateEditSomeQuestion = (contentId?: string) => {
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId })
}
export const createFrontResult = (quizId: number) => setProducedState(state => {
export const createFrontResult = (quizId: number, parentContentId?: string) => setProducedState(state => {
const frontId = nanoid()
const content = JSON.parse(JSON.stringify(defaultQuestionByType["result"].content))
content.id = frontId
if (parentContentId) content.rule.parentId = parentContentId
state.questions.push({
id: nanoid(),
id: frontId,
quizId,
type: "result",
title: "",
description: "",
deleted: false,
expanded: true,
page: 101,
required: true,
content
});
}, {
type: "createFrontResult",
@ -494,7 +528,7 @@ export const createBackResult = async (
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
if (question.type !== "result") throw new Error("Cannot upgrade already typed question");
try {
const createdQuestion = await questionApi.create({
@ -523,9 +557,3 @@ export const createBackResult = async (
enqueueSnackbar("Не удалось создать вопрос");
}
});
export const updateResult = () => {
}

@ -9,6 +9,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
export function useQuestions() {
console.log("вызываю вопросы")
const quiz = useCurrentQuiz();
const { isLoading, error, isValidating } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions,

@ -107,7 +107,7 @@ const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuiz = async (
export const updateQuiz = (
quizId: string | null | undefined,
updateFn: (quiz: Quiz) => void,
) => {

@ -7,12 +7,14 @@ import {
Radio,
RadioGroup,
Tooltip,
Typography,
Typography, useTheme,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuizQuestionEmoji } from "model/questionTypes/emoji";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
interface Props {
question: QuizQuestionEmoji;
@ -20,45 +22,78 @@ interface Props {
export default function Emoji({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const theme = useTheme();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth>
<FormLabel id="quiz-question-radio-group" data-cy="question-title">{question.title}</FormLabel>
<FormLabel id="quiz-question-radio-group" data-cy="question-title" sx={{fontSize: "24px", fontWeight: 500, marginBottom: "25px"}}>{question.title}</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
sx={{display: "flex",
flexDirection: "row", gap: "42px"}}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<FormControlLabel
key={index}
value={variant.answer}
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
<Box
key={index}
sx={{
borderRadius: "12px",
border: `1px solid ${theme.palette.grey2.main}`,
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px"
}}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }} data-cy="variant-answer">
<Typography>{`${variant.extendedText} ${variant.answer}`}</Typography>
{variant.hints && (
<Tooltip title="Подсказка" placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
>
<Box
sx={{ display: "flex", alignItems: "center", height: "193px", background: "#ffffff" }}
>
<Box sx={{ width: "100%", display: "flex", justifyContent: "center" }}>
{variant.extendedText && (
<Typography fontSize={"100px"}>{`${variant.extendedText}`}</Typography>
)}
</Box>
</Box>
}
/>
<FormControlLabel
key={index}
value={variant.answer}
sx={{
margin: 0,
padding: "15px",
color: "#4D4D4D",
display: "flex",
gap: "10px",
background: "#f2f3f7"
}}
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
checkedIcon={<RadioCheck />} icon={<RadioIcon />}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }} data-cy="variant-answer">
<Typography>{`${variant.answer}`}</Typography>
{variant.hints && (
<Tooltip title="Подсказка" placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
</Box>
))}
</RadioGroup>
</FormControl>

@ -1,4 +1,4 @@
import { useState, ChangeEvent, useEffect } from "react";
import { useState, ChangeEvent } from "react";
import {
Box,
FormControl,
@ -7,122 +7,129 @@ import {
Radio,
RadioGroup,
Tooltip,
Typography,
Typography, useTheme,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuestionVariant } from "model/questionTypes/shared";
import type { QuizQuestionVarImg } from "model/questionTypes/varimg";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
interface Props {
question: QuizQuestionVarImg;
question: QuizQuestionVarImg;
}
export default function Varimg({ question }: Props) {
const [selectedVariantIndex, setSelectedVariantIndex] = useState<number>(-1);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setSelectedVariantIndex(
question.content.variants.findIndex(
(variant) => variant.answer === event.target.value
)
);
};
const currentVariant: QuestionVariant | undefined =
question.content.variants[selectedVariantIndex];
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 2,
}}
>
<FormControl>
<FormLabel
id="quiz-question-radio-group"
data-cy="question-title"
>
{question.title}
</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={currentVariant?.answer ?? ""}
onChange={handleChange}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<FormControlLabel
key={index}
value={variant.answer}
data-cy="variant-answer"
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{variant.answer}</Typography>
{variant.hints && (
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
))}
</RadioGroup>
</FormControl>
<Box
sx={{
border: "1px solid #E3E3E3",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{currentVariant ?
currentVariant.extendedText ? (
<img
src={currentVariant.extendedText}
data-cy="variant-image"
alt="question variant"
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
) : (
<Typography p={2}>Картинка отсутствует</Typography>
) : question.content.back ? (
<img
src={question.content.back}
data-cy="variant-image"
alt="question variant"
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
) : (
<Typography p={2}>Выберите вариант</Typography>
)
}
</Box>
</Box>
const [selectedVariantIndex, setSelectedVariantIndex] = useState<number>(-1);
const theme = useTheme();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setSelectedVariantIndex(
question.content.variants.findIndex(
(variant) => variant.answer === event.target.value
)
);
};
const currentVariant: QuestionVariant | undefined =
question.content.variants[selectedVariantIndex];
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: "40px",
}}
>
<FormControl sx={{maxWidth: "900px",
width: "100%", gap: "25px"}}>
<FormLabel
id="quiz-question-radio-group"
data-cy="question-title"
sx={{fontSize: "24px", fontWeight: 500}}
>
{question.title}
</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={currentVariant?.answer ?? ""}
onChange={handleChange}
sx={{gap: "20px"}}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<FormControlLabel
key={index}
value={variant.answer}
data-cy="variant-answer"
sx={{
margin: 0,
borderRadius: "5px",
padding: "15px",
color: "#4D4D4D",
border: `1px solid ${theme.palette.grey2.main}`,
display: "flex",
}}
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
checkedIcon={<RadioCheck />} icon={<RadioIcon />}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography>{variant.answer}</Typography>
{variant.hints && (
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
))}
</RadioGroup>
</FormControl>
<Box
sx={{
border: "1px solid #E3E3E3",
maxWidth: "400px",
height: "400px",
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "12px",
marginTop: "50px",
overflow: "hidden",
}}
>
{currentVariant?.extendedText ? (
<img
src={currentVariant.extendedText}
data-cy="variant-image"
alt="question variant"
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
) : (
<Typography p={2}>
{selectedVariantIndex === -1
? "Выберите вариант"
: "Картинка отсутствует"}
</Typography>
)}
</Box>
</Box>
);
}

@ -7,6 +7,7 @@ import {
useTheme,
} from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import YoutubeEmbedIframe from "./YoutubeEmbedIframe";
export default function QuizPreviewLayout() {
@ -52,18 +53,17 @@ export default function QuizPreviewLayout() {
gap: "20px",
}}
>
{quiz.config.startpage.background.type === "image" &&
quiz.config.startpage.logo && (
<img
src={quiz.config.startpage.logo}
style={{
height: "30px",
maxWidth: "50px",
objectFit: "cover",
}}
alt=""
/>
)}
{quiz.config.startpage.logo && (
<img
src={quiz.config.startpage.logo}
style={{
height: "30px",
maxWidth: "50px",
objectFit: "cover",
}}
alt=""
/>
)}
<Typography sx={{ fontSize: "12px" }}>
{quiz.config.info.orgname}
</Typography>
@ -122,14 +122,7 @@ export default function QuizPreviewLayout() {
)}
{quiz.config.startpage.background.type === "video" &&
quiz.config.startpage.background.video && (
<video
src={quiz.config.startpage.background.video}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
<YoutubeEmbedIframe videoUrl={quiz.config.startpage.background.video} />
)}
</Box>
)}

@ -0,0 +1,34 @@
import { Box } from "@mui/material";
interface Props {
videoUrl: string;
}
export default function YoutubeEmbedIframe({ videoUrl }: Props) {
const extractYoutubeVideoId = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi;
const videoId = extractYoutubeVideoId.exec(videoUrl)?.[1];
if (!videoId) return null;
const embedUrl = `https://www.youtube.com/embed/${videoId}?controls=0&autoplay=1&modestbranding=0&showinfo=0&disablekb=1&mute=1&loop=1`;
return (
<Box sx={{
width: "100%",
height: "100%",
"& iframe": {
width: "100%",
height: "100%",
}
}}>
<iframe
src={embedUrl}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
/>
</Box>
);
}

@ -4,8 +4,8 @@ import InstallQuiz from "../pages/InstallQuiz/InstallQuiz";
import FormQuestionsPage from "../pages/Questions/Form/FormQuestionsPage";
import QuestionsPage from "../pages/Questions/QuestionsPage";
import { QuestionsMap } from "../pages/QuestionsMap";
import { Result } from "../pages/ResultPage/Result";
import { Setting } from "../pages/ResultPage/Setting";
import { ResultPage } from "../pages/ResultPage/ResultPage";
import { ResultSettings } from "../pages/ResultPage/ResultSettings";
import StartPageSettings from "../pages/startPage/StartPageSettings";
import StepOne from "../pages/startPage/stepOne";
import Steptwo from "../pages/startPage/steptwo";
@ -31,10 +31,7 @@ export default function SwitchStepPages({
return <StartPageSettings />;
}
case 1: return quizType === "form" ? <FormQuestionsPage /> : <QuestionsPage />;
case 2: {
if (!quizResults) return <Result />;
return <Setting />;
}
case 2: return <ResultPage />;
case 3: return <QuestionsMap />;
case 4: return <ContactFormPage />;
case 5: return <InstallQuiz />;