Merge branch 'dev' into 'staging'

Dev

See merge request frontend/squzanswerer!93
This commit is contained in:
Nastya 2024-03-11 20:54:49 +00:00
commit 9862b439f0
40 changed files with 3422 additions and 2452 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@ -16,6 +16,7 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import { useQuizData } from "@contexts/QuizDataContext"; import { useQuizData } from "@contexts/QuizDataContext";
import { DESIGN_LIST } from "@/components/ViewPublicationPage/Question";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590) const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
@ -153,6 +154,11 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
}, },
scrollbarWidth: "none", scrollbarWidth: "none",
msOverflowStyle: "none", msOverflowStyle: "none",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}} }}
> >
<Box <Box
@ -160,6 +166,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
width: isWide && !isMobile ? "100%" : isMobile ? undefined : "530px", width: isWide && !isMobile ? "100%" : isMobile ? undefined : "530px",
borderRadius: "4px", borderRadius: "4px",
height: "100%", height: "100%",
minHeight: "100vh",
display: isWide && !isMobile ? "flex" : undefined, display: isWide && !isMobile ? "flex" : undefined,
}} }}
> >
@ -238,6 +245,13 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
disabled={!(ready && !fire)} disabled={!(ready && !fire)}
variant="contained" variant="contained"
onClick={handleShowResultsClick} onClick={handleShowResultsClick}
sx={{
border: `1px solid ${theme.palette.primary.main}`,
"&:disabled": {
border: "1px solid #9A9AAF",
color: "#9A9AAF"
}
}}
> >
{settings.cfg.formContact?.button || "Получить результаты"} {settings.cfg.formContact?.button || "Получить результаты"}
</Button> </Button>
@ -416,6 +430,9 @@ const CustomInput = ({ title, desc, Icon, onChange, id }: {
onChange={onChange} onChange={onChange}
sx={{ sx={{
width: isMobile ? "300px" : "350px", width: isMobile ? "300px" : "350px",
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF"
}
}} }}
placeholder={desc} placeholder={desc}
InputProps={{ InputProps={{

@ -1,88 +1,71 @@
import { useQuizData } from "@contexts/QuizDataContext";
import { Box, Typography, useTheme } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import { useQuizData } from "@contexts/QuizDataContext";
import Stepper from "@ui_kit/Stepper";
type FooterProps = { type FooterProps = {
stepNumber: number | null; stepNumber: number | null;
nextButton: ReactNode; nextButton: ReactNode;
prevButton: ReactNode; prevButton: ReactNode;
}; };
export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => { export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { questions } = useQuizData(); const { questions, settings } = useQuizData();
console.log(questions) const questionsAmount = questions.filter(
({ type }) => type !== "result"
).length;
return (
<Box
sx={{
position: "relative",
padding: "15px 0",
borderTop: `1px solid ${theme.palette.grey[400]}`,
height: "75px",
display: "flex",
background: settings.cfg.design
? "rgba(154,154,175, 0.2)"
: "transparent",
}}
>
<Box
sx={{
width: "100%",
maxWidth: "1410px",
padding: "10px",
margin: "0 auto",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{/*{mode[settings.cfg.theme] ? (*/}
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/}
{stepNumber !== null && (
<Box sx={{ flexGrow: 1 }}>
<Typography sx={{ color: theme.palette.text.primary }}>
Вопрос {stepNumber} из {questionsAmount}
</Typography>
<Stepper activeStep={stepNumber} steps={questionsAmount} />
</Box>
)}
return (
<Box <Box
sx={{ sx={{
position: "relative", display: "flex",
padding: "15px 0", alignItems: "center",
borderTop: `1px solid ${theme.palette.grey[400]}`, gap: "10px",
height: '75px', marginRight: "auto",
display: "flex", // color: theme.palette.grey1.main,
}} }}
> >
<Box {/* <Typography>Шаг</Typography>
sx={{
width: "100%",
maxWidth: "1000px",
padding: "0 10px",
margin: "0 auto",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{/*{mode[settings.cfg.theme] ? (*/}
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/}
{stepNumber !== null &&
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
color: theme.palette.text.primary,
}}
>
<Typography>Шаг</Typography>
<Typography
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "50%",
width: "30px",
height: "30px",
color: "#FFF",
background: theme.palette.primary.main,
}}
>
{stepNumber}
</Typography>
<Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}>
{questions.filter(q => q.type !== "result").length}
</Typography>
</Box>
}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
// color: theme.palette.grey1.main,
}}
>
{/* <Typography>Шаг</Typography>
<Typography <Typography
sx={{ sx={{
display: "flex", display: "flex",
@ -97,15 +80,15 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
}} }}
> >
{stepNumber} */} {stepNumber} */}
{/* </Typography> */} {/* </Typography> */}
{/* <Typography>Из</Typography> {/* <Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}> <Typography sx={{ fontWeight: "bold" }}>
{questions.length} {questions.length}
</Typography> */} </Typography> */}
</Box>
{prevButton}
{nextButton}
</Box>
</Box> </Box>
); {prevButton}
{nextButton}
</Box>
</Box>
);
}; };

@ -20,78 +20,154 @@ import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark"; import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
import { notReachable } from "@utils/notReachable"; import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { ReactNode } from "react";
import Desgin1 from "@icons/designs/design1.jpg";
import Desgin2 from "@icons/designs/design2.jpg";
import Desgin3 from "@icons/designs/design3.jpg";
import Desgin4 from "@icons/designs/design4.jpg";
import Desgin5 from "@icons/designs/design5.jpg";
import Desgin6 from "@icons/designs/design6.jpg";
import Desgin7 from "@icons/designs/design7.jpg";
import Desgin8 from "@icons/designs/design8.jpg";
import Desgin9 from "@icons/designs/design9.jpg";
import Desgin10 from "@icons/designs/design10.jpg";
import type { ReactNode } from "react";
import type { QuizTheme } from "@model/settingsData";
type Props = { type Props = {
currentQuestion: RealTypedQuizQuestion; currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null; currentQuestionStepNumber: number | null;
nextButton: ReactNode; nextButton: ReactNode;
prevButton: ReactNode; prevButton: ReactNode;
};
export const DESIGN_LIST: Record<QuizTheme, string> = {
Design1: Desgin1,
Design2: Desgin2,
Design3: Desgin3,
Design4: Desgin4,
Design5: Desgin5,
Design6: Desgin6,
Design7: Desgin7,
Design8: Desgin8,
Design9: Desgin9,
Design10: Desgin10,
StandardTheme: "",
StandardDarkTheme: "",
PinkTheme: "",
PinkDarkTheme: "",
BlackWhiteTheme: "",
OliveTheme: "",
YellowTheme: "",
GoldDarkTheme: "",
PurpleTheme: "",
BlueTheme: "",
BlueDarkTheme: "",
}; };
export const Question = ({ export const Question = ({
currentQuestion, currentQuestion,
currentQuestionStepNumber, currentQuestionStepNumber,
nextButton, nextButton,
prevButton, prevButton,
}: Props) => { }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { settings, show_badge } = useQuizData(); const { settings, show_badge } = useQuizData();
console.log(currentQuestionStepNumber)
return (
<Box sx={{
backgroundColor: theme.palette.background.default,
height: "100%",
}}>
<Box sx={{
height: "calc(100% - 75px)",
width: "100%",
maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
overflow: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}>
<QuestionByType key={currentQuestion.id} question={currentQuestion} stepNumber={currentQuestionStepNumber} />
{show_badge ? (quizThemes[settings.cfg.theme].isLight ? ( return (
<Link target={"_blank"} href={"https://quiz.pena.digital"}> <Box
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} /> sx={{
</Link> backgroundPosition: "center",
) : ( backgroundSize: "cover",
<Link target={"_blank"} href={"https://quiz.pena.digital"}> backgroundImage: settings.cfg.design
<NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} /> ? `url(${DESIGN_LIST[settings.cfg.theme]})`
</Link> : null,
)) : <></>} }}
>
</Box> <Box
<Footer sx={{
stepNumber={currentQuestionStepNumber} background: settings.cfg.design
prevButton={prevButton} ? quizThemes[settings.cfg.theme].isLight
nextButton={nextButton} ? "transparent"
/> : "linear-gradient(90deg,#272626, transparent)"
: theme.palette.background.default,
}}
>
<Box
sx={{
height: "calc(100% - 75px)",
width: "100%",
minHeight: "calc(100vh - 75px)",
maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
overflow: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<QuestionByType
key={currentQuestion.id}
question={currentQuestion}
stepNumber={currentQuestionStepNumber}
/>
{show_badge && (
<Link target="_blank" href="https://quiz.pena.digital">
{quizThemes[settings.cfg.theme].isLight ? (
<NameplateLogoFQ
style={{ fontSize: "34px", width: "200px", height: "auto" }}
/>
) : (
<NameplateLogoFQDark
style={{ fontSize: "34px", width: "200px", height: "auto" }}
/>
)}
</Link>
)}
</Box> </Box>
); <Footer
stepNumber={currentQuestionStepNumber}
prevButton={prevButton}
nextButton={nextButton}
/>
</Box>
</Box>
);
}; };
function QuestionByType({ question, stepNumber }: { function QuestionByType({
question: RealTypedQuizQuestion; question,
stepNumber: number | null; stepNumber,
}: {
question: RealTypedQuizQuestion;
stepNumber: number | null;
}) { }) {
switch (question.type) { switch (question.type) {
case "variant": return <Variant currentQuestion={question} />; case "variant":
case "images": return <Images currentQuestion={question} />; return <Variant currentQuestion={question} />;
case "varimg": return <Varimg currentQuestion={question} />; case "images":
case "emoji": return <Emoji currentQuestion={question} />; return <Images currentQuestion={question} />;
case "text": return <Text currentQuestion={question} stepNumber={stepNumber}/>; case "varimg":
case "select": return <Select currentQuestion={question} />; return <Varimg currentQuestion={question} />;
case "date": return <Date currentQuestion={question} />; case "emoji":
case "number": return <Number currentQuestion={question} />; return <Emoji currentQuestion={question} />;
case "file": return <File currentQuestion={question} />; case "text":
case "page": return <Page currentQuestion={question} />; return <Text currentQuestion={question} stepNumber={stepNumber} />;
case "rating": return <Rating currentQuestion={question} />; case "select":
default: notReachable(question); return <Select currentQuestion={question} />;
} case "date":
return <Date currentQuestion={question} />;
case "number":
return <Number currentQuestion={question} />;
case "file":
return <File currentQuestion={question} />;
case "page":
return <Page currentQuestion={question} />;
case "rating":
return <Rating currentQuestion={question} />;
default:
notReachable(question);
}
} }

@ -13,7 +13,7 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext"; import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import type { QuizQuestionResult } from "../../model/questionTypes/result"; import type { QuizQuestionResult } from "../../model/questionTypes/result";
import { setCurrentQuizStep } from "@stores/quizView"; import { setCurrentQuizStep } from "@stores/quizView";
import { DESIGN_LIST } from "@/components/ViewPublicationPage/Question";
type ResultFormProps = { type ResultFormProps = {
resultQuestion: QuizQuestionResult; resultQuestion: QuizQuestionResult;
@ -22,6 +22,7 @@ type ResultFormProps = {
export const ResultForm = ({ resultQuestion }: ResultFormProps) => { export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 1000;
const { settings, show_badge } = useQuizData(); const { settings, show_badge } = useQuizData();
const spec = settings.cfg.spec const spec = settings.cfg.spec
@ -33,19 +34,26 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
height: "100%", height: "100%",
minHeight: "100vh",
width: "100%", width: "100%",
pt: "28px", pt: "28px",
overflow: "auto", overflow: "auto",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
}} backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "start", alignItems: "start",
width: isMobile ? "100%" : "490px", maxWidth: "844px",
padding: isMobile ? "0 16px" : undefined, width: "100%",
padding: isMobile ? "0 16px" : isTablet ? "0 78px" : undefined,
}} }}
> >
{ {
@ -65,8 +73,10 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
component="img" component="img"
src={resultQuestion.content.back} src={resultQuestion.content.back}
sx={{ sx={{
width: spec ? "100%" : isMobile ? "100%" : "490px", width: "100%",
height: spec ? "auto" : isMobile ? "100%" : "280px", maxWidth: "844px",
height: spec ? "auto" : isMobile ? "100%" : "306px",
borderRadius: "8px"
}} }}
></Box> ></Box>
) )

@ -1,497 +0,0 @@
import { Box, Button, ButtonBase, Link, Paper, Typography, useTheme } from "@mui/material";
import { useUADevice } from "../../utils/hooks/useUADevice";
import { notReachable } from "../../utils/notReachable";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { NameplateLogo } from "@icons/NameplateLogo";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import { setCurrentQuizStep } from "@stores/quizView";
export const StartPageViewPublication = () => {
const theme = useTheme();
const { settings, show_badge } = useQuizData();
const { isMobileDevice } = useUADevice();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 800;
const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
};
console.log(settings.cfg.startpage.background.type);
const background =
settings.cfg.startpage.background.type === "image" ? (
settings.cfg.startpage.background.desktop ? (
<img
src={settings.cfg.startpage.background.desktop}
alt=""
style={{
width: (isMobile || settings.cfg.startpageType === "expanded") ? "100%" : undefined,
height: "100%",
objectFit: "cover",
overflow: "hidden",
}}
/>
) : null
) : settings.cfg.startpage.background.type === "video" ? (
settings.cfg.startpage.background.video ? (
<YoutubeEmbedIframe
videoUrl={settings.cfg.startpage.background.video}
containerSX={{
width:
settings.cfg.startpageType === "centered"
? "550px"
: "100%",
height:
settings.cfg.startpageType === "centered"
? "275px"
: "100%",
borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform:
settings.cfg.startpageType === "centered"
? ""
: settings.cfg.startpageType === "expanded"
? "scale(1.5)"
: "scale(2.4)",
},
}}
/>
) : null
) : null;
return (
<Paper
className="settings-preview-draghandle"
sx={{
borderRadius: 0,
height: "100%",
width: "100%",
background:
settings.cfg.startpageType === "expanded" && !isMobile
? settings.cfg.startpage.position === "left"
? "linear-gradient(90deg,#272626,transparent)"
: settings.cfg.startpage.position === "center"
? "linear-gradient(180deg,transparent,#272626)"
: "linear-gradient(270deg,#272626,transparent)"
: theme.palette.background.default,
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
}}
>
<QuizPreviewLayoutByType
quizHeaderBlock={
<Box p={settings.cfg.startpageType === "standard" ? "" : "16px"}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "7px",
}}
>
{settings.cfg.startpage.logo && (
<img
src={settings.cfg.startpage.logo}
style={{
height: "37px",
maxWidth: "43px",
objectFit: "cover",
}}
alt=""
/>
)}
<Typography
sx={{
fontSize: "14px",
color: settings.cfg.startpageType === "expanded"
&& !isMobile ? "white" : theme.palette.text.primary,
wordBreak: "break-word"
}}
>{settings.cfg.info.orgname}</Typography>
</Box>
<Link mb="16px" href={settings.cfg.info.site}>
<Typography
sx={{
fontSize: "16px",
color: theme.palette.primary.main,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: isTablet ? "200px" : "300px"
}}>
{settings.cfg.info.site}
</Typography>
</Link>
</Box>
}
quizMainBlock={
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems:
settings.cfg.startpageType === "centered"
? "center"
: settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "center"
? "center"
: "start"
: "start",
mt: "28px",
width: "100%",
}}
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center" ? "center" : "-moz-initial",
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
}}
>
{settings.name}
</Typography>
<Typography
sx={{
fontSize: "16px",
m: "16px 0",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center" ? "center" : "-moz-initial",
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
}}
>
{settings.cfg.startpage.description}
</Typography>
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
width: settings.cfg.startpageType === "standard" ? "100%" : "auto",
}}
onClick={() => setCurrentQuizStep("question")}
>
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
mt: "46px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
flexDirection: "column"
}}
>
<Box sx={{ maxWidth: isTablet ? "240px" : "300px", marginBottom: "10px", }}>
{settings.cfg.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
)}
<Typography sx={{
width: "100%",
overflowWrap: "break-word",
fontSize: "12px",
textAlign: "end",
maxHeight: "120px",
overflow: "auto",
"&::-webkit-scrollbar": { width: 0 },
color:
settings.cfg.startpageType === "expanded" && !isMobile
? "white"
: theme.palette.text.primary,
}}>
{settings.cfg.info.law}
</Typography>
</Box>
{show_badge &&
<Box
component={Link}
target={"_blank"}
href={"https://quiz.pena.digital"}
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
textDecoration: "none"
}}
>
<NameplateLogo style={{ fontSize: "34px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#FFFFFF" : (quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF") }} />
<Typography sx={{ fontSize: "20px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#F5F7FF" : (quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF"), whiteSpace: "nowrap", }}>
Сделано на PenaQuiz
</Typography>
</Box>
}
</Box>
</>
}
backgroundBlock={background}
startpageType={settings.cfg.startpageType}
alignType={settings.cfg.startpage.position}
/>
</Paper>
);
};
function QuizPreviewLayoutByType({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
}: {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
}) {
const isMobile = useRootContainerSize() < 650;
function StartPageMobile() {
return (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: "100%",
overflowY: "auto",
overflowX: "hidden",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
}
}}
>
{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
id="pain"
sx={{
display: "flex",
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
flexGrow: 1,
justifyContent: isMobile ? "flex-end" : undefined,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
overflow: "auto"
}}
>
<Box
sx={{
width: isMobile ? "100%" : "40%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: isMobile ? "80%" : undefined
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
width: isMobile ? "100%" : "60%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)}
</>
);
}
case "expanded": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
overflow: "auto",
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "40%",
position: "relative",
padding: "16px",
zIndex: 3,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
position: "absolute",
zIndex: -1,
left: 0,
top: 0,
height: "100%",
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)
}
</>
);
}
case "centered": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
overflow: "auto",
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
{quizHeaderBlock}
{backgroundBlock && (
<Box
sx={{
width: "60%",
height: "275px",
// overflow: "hidden",
display: "flex",
justifyContent: "center"
}}
>
{backgroundBlock}
</Box>
)}
{quizMainBlock}
</Box>
)
}
</>
);
}
default:
notReachable(startpageType);
}
}
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
left: "start",
center: "center",
right: "end",
};

@ -0,0 +1,44 @@
import { StartPageDesktop } from "./StartPageDesktop";
import { StartPageMobile } from "./StartPageMobile";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type {
QuizStartpageAlignType,
QuizStartpageType,
} from "@model/settingsData";
type QuizPreviewLayoutByTypeProps = {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
};
export const QuizPreviewLayoutByType = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
}: QuizPreviewLayoutByTypeProps) => {
const isMobile = useRootContainerSize() < 700;
return isMobile ? (
<StartPageMobile
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
startpageType={startpageType}
/>
) : (
<StartPageDesktop
alignType={alignType}
startpageType={startpageType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
};

@ -0,0 +1,214 @@
import { Box } from "@mui/material";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizData } from "@contexts/QuizDataContext";
import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { DESIGN_LIST } from "@/components/ViewPublicationPage/Question";
import type {
QuizStartpageAlignType,
QuizStartpageType,
} from "@model/settingsData";
type StartPageDesktopProps = {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
};
type LayoutProps = Omit<StartPageDesktopProps, "startpageType">;
const StandartLayout = ({
alignType,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => {
const { settings } = useQuizData();
return (
<Box
id="pain"
sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
minHeight: "100vh",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
>
<Box
sx={{
width: "40%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "linear-gradient(90deg,#272626,transparent)"
: null,
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
{settings.cfg.startpage.background.desktop && (
<Box sx={{ width: "60%", overflow: "hidden" }}>{backgroundBlock}</Box>
)}
</Box>
);
};
const ExpandedLayout = ({
alignType,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => (
<Box>
<Box
sx={{
zIndex: 3,
minHeight: "100vh",
width: "40%",
padding: "16px",
margin:
alignType === "center"
? "0 auto"
: alignType === "left"
? "0"
: "0 0 0 auto",
}}
>
<Box
sx={{
padding: "16px",
minHeight: "calc(100vh - 32px)",
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
borderRight: alignType === "left" ? "1px solid #9A9AAF80" : null,
borderLeft: alignType === "right" ? "1px solid #9A9AAF80" : null,
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
</Box>
<Box
sx={{
position: "absolute",
zIndex: -1,
left: 0,
top: 0,
height: "100%",
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
);
const CenteredLayout = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => {
const isTablet = useRootContainerSize() < 1100;
const { settings } = useQuizData();
return (
<Box
sx={{
overflow: "auto",
padding: "10px 25px 25px",
display: "flex",
flexDirection: "column",
alignItems: "center",
minHeight: "100vh",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
"&::-webkit-scrollbar": { width: 0 },
}}
>
{quizHeaderBlock}
{backgroundBlock && settings.cfg.startpage.background.desktop && (
<Box
sx={{
width: "100%",
maxWidth: "844px",
height: isTablet ? "530px" : "306px",
display: "flex",
justifyContent: "center",
"& > img": { width: "100%", borderRadius: "12px" },
}}
>
{backgroundBlock}
</Box>
)}
{quizMainBlock}
</Box>
);
};
export const StartPageDesktop = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
}: StartPageDesktopProps) => {
switch (startpageType) {
case null:
case "standard": {
return (
<StandartLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "expanded": {
return (
<ExpandedLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "centered": {
return (
<CenteredLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
default:
notReachable(startpageType);
}
};

@ -0,0 +1,265 @@
import { Box } from "@mui/material";
import { useQuizData } from "@contexts/QuizDataContext";
import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { DESIGN_LIST } from "@/components/ViewPublicationPage/Question";
import type { QuizStartpageType } from "@model/settingsData";
type StartPageMobileProps = {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
};
type MobileLayoutProps = Omit<StartPageMobileProps, "startpageType">;
const StandartMobileLayout = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: MobileLayoutProps) => {
const { settings } = useQuizData();
return (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
minHeight: "100vh",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: "100%",
overflowY: "auto",
overflowX: "hidden",
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "linear-gradient(90deg,#272626,transparent)"
: null,
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
}}
>
{quizHeaderBlock}
<Box
sx={{
height: "80%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
}}
>
{quizMainBlock}
</Box>
</Box>
{settings.cfg.startpage.background.desktop && (
<Box sx={{ width: "100%", overflow: "hidden" }}>{backgroundBlock}</Box>
)}
</Box>
);
};
const ExpandedMobileLayout = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: MobileLayoutProps) => (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
minHeight: "100vh",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
zIndex: 3,
width: "100%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
height: "100%",
overflowY: "auto",
overflowX: "hidden",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
}}
>
{quizHeaderBlock}
<Box
sx={{
padding: "16px",
height: "80%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
}}
>
{quizMainBlock}
</Box>
</Box>
<Box
sx={{
zIndex: -1,
position: "absolute",
left: 0,
top: 0,
width: "100%",
minHeight: "100vh",
overflow: "hidden",
"& > img": {
display: "block",
minHeight: "100vh",
},
}}
>
{backgroundBlock}
</Box>
</Box>
);
const CenteredMobileLayout = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: MobileLayoutProps) => {
const {settings} = useQuizData();
return (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
minHeight: "100vh",
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
"&::-webkit-scrollbar": {width: 0},
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
padding: "10px 25px 25px",
height: "100%",
overflowY: "auto",
overflowX: "hidden",
"&::-webkit-scrollbar": {width: "4px"},
"&::-webkit-scrollbar-thumb": {backgroundColor: "#b8babf"},
}}
>
{quizHeaderBlock}
{settings.cfg.startpage.background.desktop && (
<Box
sx={{
width: "100%",
overflow: "hidden",
"& > img": {width: "100%", borderRadius: "12px"},
}}
>
{backgroundBlock}
</Box>
)}
<Box
sx={{
height: "80%",
display: "flex",
flexGrow: 1,
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
}}
>
{quizMainBlock}
</Box>
</Box>
</Box>
);
}
export const StartPageMobile = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
}: StartPageMobileProps) => {
switch (startpageType) {
case null:
case "standard": {
return (
<StandartMobileLayout
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "expanded": {
return (
<ExpandedMobileLayout
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "centered": {
return (
<CenteredMobileLayout
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
default:
notReachable(startpageType);
}
};

@ -0,0 +1,374 @@
import {
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useTheme,
} from "@mui/material";
import { QuizPreviewLayoutByType } from "./QuizPreviewLayoutByType";
import YoutubeEmbedIframe from "../tools/YoutubeEmbedIframe";
import { useQuizData } from "@contexts/QuizDataContext";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { setCurrentQuizStep } from "@stores/quizView";
import { useUADevice } from "@utils/hooks/useUADevice";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { DESIGN_LIST } from "../Question";
import { NameplateLogo } from "@icons/NameplateLogo";
import PenaLogo from "@icons/PenaLogo.png";
export const StartPageViewPublication = () => {
const theme = useTheme();
const { settings, show_badge } = useQuizData();
const { isMobileDevice } = useUADevice();
const isMobile = useRootContainerSize() < 700;
const isTablet = useRootContainerSize() < 800;
const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
};
const background =
settings.cfg.startpage.background.type === "image" ? (
<img
src={
settings.cfg.startpage.background.desktop ||
DESIGN_LIST[settings.cfg.theme] ||
""
}
alt=""
style={{
display: "block",
width:
isMobile || settings.cfg.startpageType === "expanded"
? "100%"
: undefined,
height: "100%",
minWidth: "100%",
maxHeight: "100vh",
objectFit: "cover",
overflow: "hidden",
}}
/>
) : settings.cfg.startpage.background.type === "video" ? (
settings.cfg.startpage.background.video ? (
<YoutubeEmbedIframe
videoUrl={settings.cfg.startpage.background.video}
containerSX={{
width: settings.cfg.startpageType === "centered" ? "550px" : "100%",
height:
settings.cfg.startpageType === "centered" ? "275px" : "100%",
borderRadius:
settings.cfg.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform:
settings.cfg.startpageType === "centered"
? ""
: settings.cfg.startpageType === "expanded"
? "scale(1.5)"
: "scale(2.4)",
},
}}
/>
) : null
) : null;
return (
<Paper
className="settings-preview-draghandle"
sx={{
borderRadius: 0,
// height: "100%",
minHeight: "100vh",
width: "100%",
background:
settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "left"
? "linear-gradient(90deg,#272626,transparent)"
: settings.cfg.startpage.position === "center"
? "linear-gradient(180deg,transparent,#272626)"
: "linear-gradient(270deg,#272626,transparent)"
: theme.palette.background.default,
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
}}
>
<QuizPreviewLayoutByType
quizHeaderBlock={
<Box
sx={{
margin:
settings.cfg.startpageType === "centered" ? "0 auto" : null,
padding: settings.cfg.startpageType === "standard" ? "" : "16px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: "30px",
mb: "7px",
}}
>
<img
src={settings.cfg.startpage.logo || PenaLogo}
style={{
height: "40px",
maxWidth: "110px",
objectFit: "cover",
}}
alt=""
/>
<Typography
sx={{
fontSize: "14px",
color:
settings.cfg.startpageType === "expanded"
? "white"
: theme.palette.text.primary,
wordBreak: "break-word",
}}
>
{settings.cfg.info.orgname}
</Typography>
</Box>
</Box>
}
quizMainBlock={
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
flexGrow: settings.cfg.startpageType === "centered" ? 0 : 1,
alignItems:
settings.cfg.startpageType === "centered"
? "center"
: settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "center"
? "center"
: "start"
: "start",
marginTop: "28px",
width: "100%",
}}
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
overflowWrap: "break-word",
width: "100%",
textAlign:
settings.cfg.startpageType === "centered" ||
settings.cfg.startpage.position === "center"
? "center"
: "-moz-initial",
color:
settings.cfg.startpageType === "expanded"
? "white"
: theme.palette.text.primary,
}}
>
{settings.name}
</Typography>
<Typography
sx={{
fontSize: "16px",
margin: "16px 0 30px",
overflowWrap: "break-word",
width: "100%",
textAlign:
settings.cfg.startpageType === "centered" ||
settings.cfg.startpage.position === "center"
? "center"
: "-moz-initial",
color:
settings.cfg.startpageType === "expanded"
? "white"
: theme.palette.text.primary,
}}
>
{settings.cfg.startpage.description}
</Typography>
<Box
width={
settings.cfg.startpageType === "standard" ? "100%" : "auto"
}
>
<Button
variant="contained"
sx={{
fontSize: "18px",
padding: "10px 30px",
width: "auto",
background: theme.palette.primary.main,
}}
onClick={() => setCurrentQuizStep("question")}
>
{settings.cfg.startpage.button.trim()
? settings.cfg.startpage.button
: "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
mt: "46px",
display: "flex",
flexGrow:
settings.cfg.startpageType === "centered"
? isMobile
? 0
: 1
: 0,
gap: "20px",
alignItems: "flex-end",
justifyContent: "space-between",
width: "100%",
flexWrap: "wrap",
}}
>
<Box sx={{ maxWidth: "300px" }}>
{settings.cfg.info.site && (
<Link mb="16px" href={settings.cfg.info.site}>
<Typography
sx={{
fontSize: "16px",
color: theme.palette.primary.main,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: isTablet ? "200px" : "300px",
}}
>
{settings.cfg.info.site}
</Typography>
</Link>
)}
{settings.cfg.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
<Typography
sx={{
fontSize: "16px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography
sx={{
fontSize: "16px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography
sx={{
fontSize: "16px",
marginTop: "5px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
)}
<Typography
sx={{
width: "100%",
overflowWrap: "break-word",
fontSize: "12px",
maxHeight: "120px",
overflow: "auto",
marginTop: "5px",
"&::-webkit-scrollbar": { width: 0 },
color:
settings.cfg.startpageType === "expanded"
? "white"
: theme.palette.text.primary,
}}
>
{settings.cfg.info.law}
</Typography>
</Box>
{show_badge && (
<Box
component={Link}
target={"_blank"}
href={"https://quiz.pena.digital"}
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
textDecoration: "none",
}}
>
<NameplateLogo
style={{
fontSize: "34px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: quizThemes[settings.cfg.theme].isLight
? "#151515"
: "#FFFFFF",
}}
/>
<Typography
sx={{
color:
settings.cfg.startpageType === "expanded"
? "#F5F7FF"
: quizThemes[settings.cfg.theme].isLight
? "#4D4D4D"
: "#F5F7FF",
whiteSpace: "nowrap",
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
)}
</Box>
</>
}
backgroundBlock={background}
startpageType={settings.cfg.startpageType}
alignType={settings.cfg.startpage.position}
/>
</Paper>
);
};

@ -11,88 +11,103 @@ import { Question } from "./Question";
import { ResultForm } from "./ResultForm"; import { ResultForm } from "./ResultForm";
import { StartPageViewPublication } from "./StartPageViewPublication"; import { StartPageViewPublication } from "./StartPageViewPublication";
export default function ViewPublicationPage() { export default function ViewPublicationPage() {
const { settings, recentlyCompleted } = useQuizData(); const { settings, recentlyCompleted } = useQuizData();
let currentQuizStep = useQuizViewStore(state => state.currentQuizStep); let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep);
const isMobileMini = useRootContainerSize() < 382; const isMobileMini = useRootContainerSize() < 382;
const { const {
currentQuestion, currentQuestion,
currentQuestionStepNumber, currentQuestionStepNumber,
isNextButtonEnabled, isNextButtonEnabled,
isPreviousButtonEnabled, isPreviousButtonEnabled,
moveToPrevQuestion, moveToPrevQuestion,
moveToNextQuestion, moveToNextQuestion,
showResultAfterContactForm, showResultAfterContactForm,
} = useQuestionFlowControl(); } = useQuestionFlowControl();
useEffect(function setFaviconAndTitle() { useEffect(
const link = document.querySelector('link[rel="icon"]'); function setFaviconAndTitle() {
if (link && settings.cfg.startpage.favIcon) { const link = document.querySelector('link[rel="icon"]');
link.setAttribute("href", settings.cfg.startpage.favIcon); if (link && settings.cfg.startpage.favIcon) {
} link.setAttribute("href", settings.cfg.startpage.favIcon);
}
document.title = settings.name; document.title = settings.name;
}, [settings]); },
[settings]
);
if (recentlyCompleted) throw new Error("Quiz already completed"); if (recentlyCompleted) throw new Error("Quiz already completed");
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question"; if (currentQuizStep === "startpage" && settings.cfg.noStartPage)
currentQuizStep = "question";
let quizStepElement: ReactElement; let quizStepElement: ReactElement;
switch (currentQuizStep) { switch (currentQuizStep) {
case "startpage": { case "startpage": {
quizStepElement = <StartPageViewPublication />; quizStepElement = <StartPageViewPublication />;
break; break;
}
case "question": {
if (currentQuestion.type === "result") {
quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
break;
}
quizStepElement = (
<Question
currentQuestion={currentQuestion}
currentQuestionStepNumber={currentQuestionStepNumber}
prevButton={
<Button
disabled={!isPreviousButtonEnabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={moveToPrevQuestion}
>
{isMobileMini ? "←" : "← Назад"}
</Button>
}
nextButton={
<Button
disabled={!isNextButtonEnabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={moveToNextQuestion}
>
Далее
</Button>
}
/>
);
break;
}
case "contactform": {
quizStepElement = (
<ContactForm
currentQuestion={currentQuestion}
onShowResult={showResultAfterContactForm}
/>
);
break;
}
default: notReachable(currentQuizStep);
} }
case "question": {
if (currentQuestion.type === "result") {
quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
break;
}
return ( quizStepElement = (
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}> <Question
{quizStepElement} currentQuestion={currentQuestion}
</ThemeProvider> currentQuestionStepNumber={currentQuestionStepNumber}
); prevButton={
} <Button
disabled={!isPreviousButtonEnabled}
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
color: "#FFFFFF",
border: "1px solid #9A9AAF",
background: "rgba(154,154,175, 0.2)",
"&:disabled": {
color: "#9A9AAF",
},
}}
onClick={moveToPrevQuestion}
>
{isMobileMini ? "←" : "← Назад"}
</Button>
}
nextButton={
<Button
disabled={!isNextButtonEnabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={moveToNextQuestion}
>
Далее
</Button>
}
/>
);
break;
}
case "contactform": {
quizStepElement = (
<ContactForm
currentQuestion={currentQuestion}
onShowResult={showResultAfterContactForm}
/>
);
break;
}
default:
notReachable(currentQuizStep);
}
return (
<ThemeProvider
theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}
>
{quizStepElement}
</ThemeProvider>
);
}

@ -14,97 +14,101 @@ import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react"; import { useState } from "react";
type DateProps = { type DateProps = {
currentQuestion: QuizQuestionDate; currentQuestion: QuizQuestionDate;
}; };
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings, quizId } = useQuizData(); const { settings, quizId } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const answer = answers.find( const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
)?.answer as string; )?.answer as string;
const currentAnswer = moment(answer) || moment(); const currentAnswer = moment(answer) || moment();
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
{currentQuestion.title} variant="h5"
</Typography> color={theme.palette.text.primary}
<Box sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<DatePicker
slots={{
openPickerIcon: () => (
<CalendarIcon
sx={{ sx={{
display: "flex", "& path": { stroke: theme.palette.primary.main },
flexDirection: "column", "& rect": { stroke: theme.palette.primary.main },
width: "100%",
marginTop: "20px",
}} }}
> />
<DatePicker ),
slots={{ }}
openPickerIcon: () => ( value={currentAnswer}
<CalendarIcon onChange={async (date) => {
sx={{ if (isSending || !date) return;
"& path": { stroke: theme.palette.primary.main },
"& rect": { stroke: theme.palette.primary.main },
}}
/>
),
}}
value={currentAnswer}
onChange={async (date) => {
if (isSending || !date) return;
setIsSending(true); setIsSending(true);
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: moment(date).format("YYYY.MM.DD"), body: moment(date).format("YYYY.MM.DD"),
qid: quizId, qid: quizId,
}); });
updateAnswer( updateAnswer(currentQuestion.id, date, 0);
currentQuestion.id, } catch (e) {
date, enqueueSnackbar("ответ не был засчитан");
0 }
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false); setIsSending(false);
}} }}
slotProps={{ slotProps={{
openPickerButton: { openPickerButton: {
sx: { sx: {
p: 0, p: 0,
}, },
"data-cy": "open-datepicker", "data-cy": "open-datepicker",
}, },
layout: { layout: {
sx: { backgroundColor: theme.palette.background.default }, sx: { backgroundColor: theme.palette.background.default },
}, },
}} }}
sx={{ sx={{
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
backgroundColor: quizThemes[settings.cfg.theme].isLight backgroundColor: settings.cfg.design
? "white" ? quizThemes[settings.cfg.theme].isLight
: theme.palette.background.default, ? "#F2F3F7"
borderRadius: "10px", : "rgba(154,154,175, 0.2)"
maxWidth: "250px", : quizThemes[settings.cfg.theme].isLight
pr: "22px", ? "white"
"& input": { : theme.palette.background.default,
py: "11px", borderRadius: "10px",
pl: "20px", maxWidth: "250px",
lineHeight: "19px", pr: "22px",
}, "& input": {
"& fieldset": { py: "11px",
borderColor: "#9A9AAF", pl: "20px",
}, lineHeight: "19px",
}, },
}} "& fieldset": {
/> borderColor: "#9A9AAF",
</Box> },
</Box> },
); }}
/>
</Box>
</Box>
);
}; };

@ -1,11 +1,11 @@
import { import {
Box, Box,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
Radio, Radio,
RadioGroup, RadioGroup,
Typography, Typography,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView"; import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
@ -19,169 +19,189 @@ import { enqueueSnackbar } from "notistack";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji"; import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { useQuizData } from "@contexts/QuizDataContext"; import { useQuizData } from "@contexts/QuizDataContext";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { quizThemes } from "@utils/themes/Publication/themePublication";
polyfillCountryFlagEmojis(); polyfillCountryFlagEmojis();
import { useState } from "react"; import { useState } from "react";
type EmojiProps = { type EmojiProps = {
currentQuestion: QuizQuestionEmoji; currentQuestion: QuizQuestionEmoji;
}; };
export const Emoji = ({ currentQuestion }: EmojiProps) => { export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme(); const theme = useTheme();
const { quizId } = useQuizData(); const { quizId, settings } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
({ questionId }) => questionId === currentQuestion.id const [isSending, setIsSending] = useState<boolean>(false);
) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
return ( return (
<Box> <Box>
<Typography <Typography
variant="h5" variant="h5"
color={theme.palette.text.primary} color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }} sx={{ wordBreak: "break-word" }}
>{currentQuestion.title}</Typography> >
<RadioGroup {currentQuestion.title}
name={currentQuestion.id} </Typography>
value={currentQuestion.content.variants.findIndex( <RadioGroup
({ id }) => answer === id name={currentQuestion.id}
)} value={currentQuestion.content.variants.findIndex(
onChange={({ target }) => { ({ id }) => answer === id
updateAnswer( )}
currentQuestion.id, onChange={({ target }) => {
currentQuestion.content.variants[Number(target.value)].answer, updateAnswer(
currentQuestion.content.variants[Number(target.value)].points || 0 currentQuestion.id,
); currentQuestion.content.variants[Number(target.value)].answer,
} currentQuestion.content.variants[Number(target.value)].points || 0
} );
sx={{ }}
display: "flex", sx={{
flexWrap: "wrap", display: "flex",
flexDirection: "row", flexWrap: "wrap",
justifyContent: "space-between", flexDirection: "row",
marginTop: "20px", justifyContent: "space-between",
}} marginTop: "20px",
}}
>
<Box
sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={variant.id}
disabled={isSending}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
> >
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}> <Box
{currentQuestion.content.variants.map((variant, index) => ( sx={{
<FormControl display: "flex",
key={variant.id} alignItems: "center",
disabled={isSending} height: "193px",
sx={{ background: "#ffffff",
borderRadius: "12px", }}
border: `1px solid`, >
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF", <Box
overflow: "hidden", sx={{
maxWidth: "317px", width: "100%",
width: "100%", display: "flex",
height: "255px", justifyContent: "center",
}} }}
> >
<Box {variant.extendedText && (
sx={{ <Typography fontSize={"100px"}>
display: "flex", {variant.extendedText}
alignItems: "center", </Typography>
height: "193px", )}
background: "#ffffff",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize={"100px"}>
{variant.extendedText}
</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
}
},
}}
value={index}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: quizId,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
control={
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{
wordBreak: "break-word",
lineHeight: "normal",
}}>{variant.answer}</Typography>
</Box>
}
/>
</FormControl>
))}
</Box> </Box>
</RadioGroup> </Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body:
currentQuestion.content.variants[index].extendedText +
" " +
currentQuestion.content.variants[index].answer,
qid: quizId,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography
sx={{
wordBreak: "break-word",
lineHeight: "normal",
}}
>
{variant.answer}
</Typography>
</Box>
}
/>
</FormControl>
))}
</Box> </Box>
); </RadioGroup>
</Box>
);
}; };

@ -189,7 +189,7 @@ export const File = ({ currentQuestion }: FileProps) => {
justifyContent: "flex-start", justifyContent: "flex-start",
alignItems: "center", alignItems: "center",
padding: "33px 44px 33px 55px", padding: "33px 44px 33px 55px",
backgroundColor: theme.palette.background.default, backgroundColor: "#F2F3F7",
border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`, border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
borderRadius: "8px", borderRadius: "8px",
}} }}

@ -1,10 +1,10 @@
import { import {
Box, Box,
FormControlLabel, FormControlLabel,
Radio, Radio,
RadioGroup, RadioGroup,
Typography, Typography,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView"; import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
@ -17,149 +17,172 @@ import { useRootContainerSize } from "../../../contexts/RootContainerWidthContex
import type { QuizQuestionImages } from "../../../model/questionTypes/images"; import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { useQuizData } from "@contexts/QuizDataContext"; import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react"; import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type ImagesProps = { type ImagesProps = {
currentQuestion: QuizQuestionImages; currentQuestion: QuizQuestionImages;
}; };
export const Images = ({ currentQuestion }: ImagesProps) => { export const Images = ({ currentQuestion }: ImagesProps) => {
const { quizId } = useQuizData(); const { quizId } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer; const answer = answers.find(
const [isSending, setIsSending] = useState<boolean>(false); ({ questionId }) => questionId === currentQuestion.id
const isTablet = useRootContainerSize() < 1000; )?.answer;
const isMobile = useRootContainerSize() < 500; const { settings } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> <Typography
<RadioGroup variant="h5"
name={currentQuestion.id} color={theme.palette.text.primary}
value={currentQuestion.content.variants.findIndex( sx={{ wordBreak: "break-word" }}
({ id }) => answer === id >
)} {currentQuestion.title}
sx={{ </Typography>
display: "flex", <RadioGroup
flexWrap: "wrap", name={currentQuestion.id}
flexDirection: "row", value={currentQuestion.content.variants.findIndex(
justifyContent: "space-between", ({ id }) => answer === id
marginTop: "20px", )}
}} sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet
? isMobile
? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(154,154,175, 0.2)"
: "transparent",
transition: "opacity 0.5s ease",
opacity: isSending ? 0.5 : 1,
pointerEvents: isSending ? "none" : "auto",
}}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
> >
<Box <Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
sx={{ <Box sx={{ width: "100%", height: "300px" }}>
display: "grid", {variant.extendedText && (
gap: "15px", <img
gridTemplateColumns: isTablet src={variant.extendedText}
? isMobile alt=""
? "repeat(1, 1fr)" style={{
: "repeat(2, 1fr)" display: "block",
: "repeat(3, 1fr)",
width: "100%", width: "100%",
}} height: "100%",
> objectFit: "cover",
{currentQuestion.content.variants.map((variant, index) => ( }}
<Box />
key={index} )}
sx={{
cursor: "pointer",
borderRadius: "5px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
transition: "opacity 0.5s ease",
opacity: isSending ? 0.5 : 1,
pointerEvents: isSending ? "none" : "auto",
}}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
}
},
}}
value={index}
control={
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
}
label={variant.answer}
/>
</Box>
))}
</Box> </Box>
</RadioGroup> </Box>
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={variant.answer}
/>
</Box>
))}
</Box> </Box>
); </RadioGroup>
</Box>
);
}; };

@ -18,445 +18,454 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { ChangeEvent, SyntheticEvent } from "react"; import type { ChangeEvent, SyntheticEvent } from "react";
type NumberProps = { type NumberProps = {
currentQuestion: QuizQuestionNumber; currentQuestion: QuizQuestionNumber;
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const { settings, quizId } = useQuizData(); const { settings, quizId } = useQuizData();
const [inputValue, setInputValue] = useState<string>("0"); const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0"); const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000"); const [maxRange, setMaxRange] = useState<string>("100000000000");
const [reversedInputValue, setReversedInputValue] = useState<string>("0"); const [reversedInputValue, setReversedInputValue] = useState<string>("0");
const [reversedMinRange, setReversedMinRange] = useState<string>("0"); const [reversedMinRange, setReversedMinRange] = useState<string>("0");
const [reversedMaxRange, setReversedMaxRange] = const [reversedMaxRange, setReversedMaxRange] =
useState<string>("100000000000"); useState<string>("100000000000");
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const [minBorder, maxBorder] = currentQuestion.content.range const [minBorder, maxBorder] = currentQuestion.content.range
.split("—") .split("—")
.map(window.Number); .map(window.Number);
const min = minBorder < maxBorder ? minBorder : maxBorder; const min = minBorder < maxBorder ? minBorder : maxBorder;
const max = minBorder < maxBorder ? maxBorder : minBorder; const max = minBorder < maxBorder ? maxBorder : minBorder;
const reversed = minBorder > maxBorder; const reversed = minBorder > maxBorder;
useEffect(() => {
console.log("reversed:", reversed)
}, [reversed])
const sendAnswerToBackend = async (value: string, noUpdate = false) => { useEffect(() => {
setIsSending(true); console.log("reversed:", reversed);
try { }, [reversed]);
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: quizId,
});
if (!noUpdate) { const sendAnswerToBackend = async (value: string, noUpdate = false) => {
updateAnswer(currentQuestion.id, value, 0); setIsSending(true);
} try {
} catch (e) { await sendAnswer({
enqueueSnackbar("ответ не был засчитан"); questionId: currentQuestion.id,
} body: value,
qid: quizId,
});
setIsSending(false); if (!noUpdate) {
}; updateAnswer(currentQuestion.id, value, 0);
}
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
const updateValueDebounced = useDebouncedCallback(async (value: string) => { setIsSending(false);
};
const updateValueDebounced = useDebouncedCallback(async (value: string) => {
if (reversed) {
const newValue =
window.Number(value) < window.Number(min)
? String(min)
: window.Number(value) > window.Number(max)
? String(max)
: value;
setReversedInputValue(newValue);
updateAnswer(
currentQuestion.id,
String(max + min - window.Number(newValue)),
0
);
await sendAnswerToBackend(String(window.Number(newValue)), true);
return;
}
const newValue =
window.Number(value) < window.Number(minRange)
? minRange
: window.Number(value) > window.Number(maxRange)
? maxRange
: value;
setInputValue(newValue);
await sendAnswerToBackend(newValue);
}, 1000);
const updateMinRangeDebounced = useDebouncedCallback(
async (value: string, crowded = false) => {
if (reversed) {
const newMinRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[0]) < min
? min
: max + min - window.Number(value.split("—")[0]);
const newMinValue =
window.Number(value.split("—")[0]) > max
? String(max)
: value.split("—")[0];
setReversedMinRange(
crowded ? String(max + min - window.Number(newMinValue)) : newMinValue
);
updateAnswer(
currentQuestion.id,
`${newMinRange}${value.split("—")[1]}`,
0
);
await sendAnswerToBackend(
`${newMinValue}${value.split("—")[1]}`,
true
);
return;
}
const newMinValue = crowded
? maxRange
: window.Number(value.split("—")[0]) < min
? String(min)
: value.split("—")[0];
setMinRange(newMinValue);
await sendAnswerToBackend(`${newMinValue}${value.split("—")[1]}`);
},
1000
);
const updateMaxRangeDebounced = useDebouncedCallback(
async (value: string, crowded = false) => {
if (reversed) {
const newMaxRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[1]) > max
? max
: max + min - window.Number(value.split("—")[1]);
const newMaxValue =
window.Number(value.split("—")[1]) < min
? String(min)
: value.split("—")[1];
setReversedMaxRange(
crowded ? String(max + min - window.Number(newMaxValue)) : newMaxValue
);
updateAnswer(
currentQuestion.id,
`${value.split("—")[0]}${newMaxRange}`,
0
);
await sendAnswerToBackend(
`${value.split("—")[0]}${newMaxValue}`,
true
);
return;
}
const newMaxValue = crowded
? minRange
: window.Number(value.split("—")[1]) > max
? String(max)
: value.split("—")[1];
setMaxRange(newMaxValue);
await sendAnswerToBackend(`${value.split("—")[0]}${newMaxValue}`);
},
1000
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const sliderValue =
answer ||
(reversed
? max + min - currentQuestion.content.start + "—" + max
: currentQuestion.content.start + "—" + max);
useEffect(() => {
if (answer) {
if (answer.includes("—")) {
if (reversed) { if (reversed) {
const newValue = setReversedMinRange(
window.Number(value) < window.Number(min) String(max + min - window.Number(answer.split("—")[0]))
? String(min) );
: window.Number(value) > window.Number(max) setReversedMaxRange(
? String(max) String(max + min - window.Number(answer.split("—")[1]))
: value; );
setReversedInputValue(newValue);
updateAnswer(
currentQuestion.id,
String(max + min - window.Number(newValue)),
0
);
await sendAnswerToBackend(String(window.Number(newValue)), true);
return;
}
const newValue =
window.Number(value) < window.Number(minRange)
? minRange
: window.Number(value) > window.Number(maxRange)
? maxRange
: value;
setInputValue(newValue);
await sendAnswerToBackend(newValue);
}, 1000);
const updateMinRangeDebounced = useDebouncedCallback(
async (value: string, crowded = false) => {
if (reversed) {
const newMinRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[0]) < min
? min
: max + min - window.Number(value.split("—")[0]);
const newMinValue =
window.Number(value.split("—")[0]) > max
? String(max)
: value.split("—")[0];
setReversedMinRange(
crowded ? String(max + min - window.Number(newMinValue)) : newMinValue
);
updateAnswer(
currentQuestion.id,
`${newMinRange}${value.split("—")[1]}`,
0
);
await sendAnswerToBackend(
`${newMinValue}${value.split("—")[1]}`,
true
);
return;
}
const newMinValue = crowded
? maxRange
: window.Number(value.split("—")[0]) < min
? String(min)
: value.split("—")[0];
setMinRange(newMinValue);
await sendAnswerToBackend(`${newMinValue}${value.split("—")[1]}`);
},
1000
);
const updateMaxRangeDebounced = useDebouncedCallback(
async (value: string, crowded = false) => {
if (reversed) {
const newMaxRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[1]) > max
? max
: max + min - window.Number(value.split("—")[1]);
const newMaxValue =
window.Number(value.split("—")[1]) < min
? String(min)
: value.split("—")[1];
setReversedMaxRange(
crowded ? String(max + min - window.Number(newMaxValue)) : newMaxValue
);
updateAnswer(
currentQuestion.id,
`${value.split("—")[0]}${newMaxRange}`,
0
);
await sendAnswerToBackend(
`${value.split("—")[0]}${newMaxValue}`,
true
);
return;
}
const newMaxValue = crowded
? minRange
: window.Number(value.split("—")[1]) > max
? String(max)
: value.split("—")[1];
setMaxRange(newMaxValue);
await sendAnswerToBackend(`${value.split("—")[0]}${newMaxValue}`);
},
1000
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const sliderValue =
answer ||
(reversed
? max + min - currentQuestion.content.start + "—" + max
: currentQuestion.content.start + "—" + max);
useEffect(() => {
if (answer) {
if (answer.includes("—")) {
if (reversed) {
setReversedMinRange(
String(max + min - window.Number(answer.split("—")[0]))
);
setReversedMaxRange(
String(max + min - window.Number(answer.split("—")[1]))
);
} else {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
}
} else {
if (reversed) {
setReversedInputValue(String(max + min - window.Number(answer)));
} else {
setInputValue(answer);
}
}
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
if (currentQuestion.content.chooseRange) {
setReversedMinRange(String(currentQuestion.content.start));
setReversedMaxRange(String(min));
}
setReversedInputValue(String(currentQuestion.content.start));
setInputValue(String(currentQuestion.content.start));
}
}, []);
const onSliderChange = (_: Event, value: number | number[]) => {
const range = Array.isArray(value)
? `${value[0]}${value[1]}`
: String(value);
updateAnswer(currentQuestion.id, range, 0);
};
const onChangeCommitted = async (
_: Event | SyntheticEvent<Element, Event>,
value: number | number[]
) => {
if (currentQuestion.content.chooseRange && Array.isArray(value)) {
if (reversed) {
const newMinReversedValue = String(max + min - value[0]);
const newMaxReversedValue = String(max + min - value[1]);
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
setReversedMinRange(newMinReversedValue);
setReversedMaxRange(newMaxReversedValue);
await sendAnswerToBackend(
`${newMinReversedValue}${newMaxReversedValue}`,
true
);
return;
}
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
return;
}
if (reversed) {
setReversedInputValue(String(max + min - window.Number(value)));
} else { } else {
setInputValue(String(value)); setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
} }
} else {
await sendAnswerToBackend(String(value));
};
const changeValueLabelFormat = (value: number) => {
if (!reversed) {
return value;
}
const [minSliderBorder, maxSliderBorder] = sliderValue
.split("—")
.map(window.Number);
if (value === minSliderBorder) {
return max + min - minSliderBorder;
}
return max + min - maxSliderBorder;
};
const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = target.value.replace(/\D/g, "");
if (reversed) { if (reversed) {
setReversedInputValue(value); setReversedInputValue(String(max + min - window.Number(answer)));
} else { } else {
setInputValue(value); setInputValue(answer);
} }
}
}
updateValueDebounced(value); if (!answer) {
}; setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => { if (currentQuestion.content.chooseRange) {
const newValue = target.value.replace(/\D/g, ""); setReversedMinRange(String(currentQuestion.content.start));
setReversedMaxRange(String(min));
}
if (reversed) { setReversedInputValue(String(currentQuestion.content.start));
setReversedMinRange(newValue); setInputValue(String(currentQuestion.content.start));
}
}, []);
if (window.Number(newValue) <= window.Number(reversedMaxRange)) { const onSliderChange = (_: Event, value: number | number[]) => {
const value = max + min - window.Number(reversedMaxRange); const range = Array.isArray(value)
updateMinRangeDebounced(`${value}${value}`, true); ? `${value[0]}${value[1]}`
: String(value);
return; updateAnswer(currentQuestion.id, range, 0);
} };
updateMinRangeDebounced( const onChangeCommitted = async (
`${newValue}${max + min - window.Number(reversedMaxRange)}` _: Event | SyntheticEvent<Element, Event>,
); value: number | number[]
) => {
if (currentQuestion.content.chooseRange && Array.isArray(value)) {
if (reversed) {
const newMinReversedValue = String(max + min - value[0]);
const newMaxReversedValue = String(max + min - value[1]);
return; setMinRange(String(value[0]));
} setMaxRange(String(value[1]));
setReversedMinRange(newMinReversedValue);
setReversedMaxRange(newMaxReversedValue);
await sendAnswerToBackend(
`${newMinReversedValue}${newMaxReversedValue}`,
true
);
setMinRange(newValue); return;
}
if (window.Number(newValue) >= window.Number(maxRange)) { setMinRange(String(value[0]));
updateMinRangeDebounced(`${maxRange}${maxRange}`, true); setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
return; return;
} }
updateMinRangeDebounced(`${newValue}${maxRange}`); if (reversed) {
}; setReversedInputValue(String(max + min - window.Number(value)));
} else {
setInputValue(String(value));
}
const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => { await sendAnswerToBackend(String(value));
const newValue = target.value.replace(/\D/g, ""); };
if (reversed) { const changeValueLabelFormat = (value: number) => {
setReversedMaxRange(newValue); if (!reversed) {
return value;
}
if (window.Number(newValue) >= window.Number(reversedMinRange)) { const [minSliderBorder, maxSliderBorder] = sliderValue
const value = max + min - window.Number(reversedMinRange); .split("—")
updateMaxRangeDebounced(`${value}${value}`, true); .map(window.Number);
return; if (value === minSliderBorder) {
} return max + min - minSliderBorder;
}
updateMaxRangeDebounced( return max + min - maxSliderBorder;
`${max + min - window.Number(reversedMinRange)}${newValue}` };
);
return; const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
} const value = target.value.replace(/\D/g, "");
setMaxRange(newValue); if (reversed) {
setReversedInputValue(value);
} else {
setInputValue(value);
}
if (window.Number(newValue) <= window.Number(minRange)) { updateValueDebounced(value);
updateMaxRangeDebounced(`${minRange}${minRange}`, true); };
return; const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
} const newValue = target.value.replace(/\D/g, "");
updateMaxRangeDebounced(`${minRange}${newValue}`); if (reversed) {
}; setReversedMinRange(newValue);
return ( if (window.Number(newValue) <= window.Number(reversedMaxRange)) {
<Box> const value = max + min - window.Number(reversedMaxRange);
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> updateMinRangeDebounced(`${value}${value}`, true);
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
gap: "30px",
paddingRight: isMobile ? "10px" : undefined,
}}
>
<CustomSlider
value={
currentQuestion.content.chooseRange
? sliderValue.split("—").length || 0 > 1
? sliderValue.split("—").map((item) => window.Number(item))
: [min, min + 1]
: window.Number(sliderValue.split("—")[0])
}
min={min}
max={max}
step={currentQuestion.content.step || 1}
onChange={onSliderChange}
onChangeCommitted={onChangeCommitted}
valueLabelFormat={changeValueLabelFormat}
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,
},
}}
/>
{!currentQuestion.content.chooseRange && ( return;
<CustomTextField }
placeholder="0"
value={reversed ? reversedInputValue : inputValue}
onChange={onInputChange}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
},
}}
/>
)}
{currentQuestion.content.chooseRange && ( updateMinRangeDebounced(
<Box `${newValue}${max + min - window.Number(reversedMaxRange)}`
sx={{ );
display: "flex",
gap: "15px", return;
alignItems: "center", }
"& .MuiFormControl-root": { width: "auto" },
}} setMinRange(newValue);
>
<CustomTextField if (window.Number(newValue) >= window.Number(maxRange)) {
placeholder="0" updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
value={reversed ? String(reversedMinRange) : minRange}
onChange={onMinInputChange} return;
sx={{ }
maxWidth: "80px",
borderColor: theme.palette.text.primary, updateMinRangeDebounced(`${newValue}${maxRange}`);
"& .MuiInputBase-input": { };
textAlign: "center",
backgroundColor: quizThemes[settings.cfg.theme].isLight const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
? "white" const newValue = target.value.replace(/\D/g, "");
: theme.palette.background.default,
}, if (reversed) {
}} setReversedMaxRange(newValue);
/>
<Typography color={theme.palette.text.primary}>до</Typography> if (window.Number(newValue) >= window.Number(reversedMinRange)) {
<CustomTextField const value = max + min - window.Number(reversedMinRange);
placeholder="0" updateMaxRangeDebounced(`${value}${value}`, true);
value={reversed ? String(reversedMaxRange) : maxRange}
onChange={onMaxInputChange} return;
sx={{ }
maxWidth: "80px",
borderColor: theme.palette.text.primary, updateMaxRangeDebounced(
"& .MuiInputBase-input": { `${max + min - window.Number(reversedMinRange)}${newValue}`
textAlign: "center", );
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white" return;
: theme.palette.background.default, }
},
}} setMaxRange(newValue);
/>
</Box> if (window.Number(newValue) <= window.Number(minRange)) {
)} updateMaxRangeDebounced(`${minRange}${minRange}`, true);
</Box>
</Box> return;
); }
updateMaxRangeDebounced(`${minRange}${newValue}`);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
gap: "30px",
paddingRight: isMobile ? "10px" : undefined,
}}
>
<CustomSlider
value={
currentQuestion.content.chooseRange
? sliderValue.split("—").length || 0 > 1
? sliderValue.split("—").map((item) => window.Number(item))
: [min, min + 1]
: window.Number(sliderValue.split("—")[0])
}
min={min}
max={max}
step={currentQuestion.content.step || 1}
onChange={onSliderChange}
onChangeCommitted={onChangeCommitted}
valueLabelFormat={changeValueLabelFormat}
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,
},
}}
/>
{!currentQuestion.content.chooseRange && (
<CustomTextField
placeholder="0"
value={reversed ? reversedInputValue : inputValue}
onChange={onInputChange}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderColor: "#9A9AAF"
},
}}
/>
)}
{currentQuestion.content.chooseRange && (
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
"& .MuiFormControl-root": { width: "auto" },
}}
>
<CustomTextField
placeholder="0"
value={reversed ? String(reversedMinRange) : minRange}
onChange={onMinInputChange}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderColor: "#9A9AAF"
},
}}
/>
<Typography color={theme.palette.text.primary}>до</Typography>
<CustomTextField
placeholder="0"
value={reversed ? String(reversedMaxRange) : maxRange}
onChange={onMaxInputChange}
sx={{
maxWidth: "80px",
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderColor: "#9A9AAF"
},
}}
/>
</Box>
)}
</Box>
</Box>
);
}; };

@ -9,74 +9,87 @@ import { enqueueSnackbar } from "notistack";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select"; import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import { useQuizData } from "@contexts/QuizDataContext"; import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react"; import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type SelectProps = { type SelectProps = {
currentQuestion: QuizQuestionSelect; currentQuestion: QuizQuestionSelect;
}; };
export const Select = ({ currentQuestion }: SelectProps) => { export const Select = ({ currentQuestion }: SelectProps) => {
const theme = useTheme(); const theme = useTheme();
const { quizId } = useQuizData(); const { quizId, settings } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
({ questionId }) => questionId === currentQuestion.id
) ?? {};
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> <Typography
<Box variant="h5"
sx={{ color={theme.palette.text.primary}
display: "flex", sx={{ wordBreak: "break-word" }}
flexDirection: "column", >
width: "100%", {currentQuestion.title}
marginTop: "20px", </Typography>
}} <Box
> sx={{
<SelectComponent display: "flex",
disabled={isSending} flexDirection: "column",
placeholder={currentQuestion.content.default} width: "100%",
activeItemIndex={answer ? Number(answer) : -1} marginTop: "20px",
items={currentQuestion.content.variants.map(({ answer }) => answer)} }}
colorMain={theme.palette.primary.main} >
onChange={async (_, value) => { <SelectComponent
setIsSending(true); disabled={isSending}
if (value < 0) { placeholder={currentQuestion.content.default}
deleteAnswer(currentQuestion.id); activeItemIndex={answer ? Number(answer) : -1}
try { items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
sx={{
"& .MuiSelect-select.MuiSelect-outlined": { zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
},
}}
onChange={async (_, value) => {
setIsSending(true);
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
return setIsSending(false);
}
await sendAnswer({ try {
questionId: currentQuestion.id, await sendAnswer({
body: "", questionId: currentQuestion.id,
qid: quizId, body: String(
}); currentQuestion.content.variants[Number(value)].answer
),
qid: quizId,
});
} catch (e) { updateAnswer(currentQuestion.id, String(value), 0);
enqueueSnackbar("ответ не был засчитан"); } catch (e) {
} enqueueSnackbar("ответ не был засчитан");
return setIsSending(false); }
}
try { setIsSending(false);
}}
await sendAnswer({ />
questionId: currentQuestion.id, </Box>
body: String(currentQuestion.content.variants[Number(value)].answer), </Box>
qid: quizId, );
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}}
/>
</Box>
</Box>
);
}; };

@ -1,4 +1,10 @@
import {Box, TextField as MuiTextField, TextFieldProps, Typography, useTheme} from "@mui/material"; import {
Box,
TextField as MuiTextField,
TextFieldProps,
Typography,
useTheme,
} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -8,194 +14,272 @@ import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext"; import { useQuizData } from "@contexts/QuizDataContext";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import {ChangeEvent, FC, useEffect, useState} from "react"; import { ChangeEvent, FC, useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { QuizQuestionText } from "../../../model/questionTypes/text"; import type { QuizQuestionText } from "../../../model/questionTypes/text";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590) const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
type TextProps = { type TextProps = {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
stepNumber: number | null; stepNumber: number | null;
}; };
const Orientation = [ const Orientation = [
{horizontal: true}, { horizontal: true },
{horizontal: false}, { horizontal: false },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: false}, { horizontal: false },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: false}, { horizontal: false },
{horizontal: true}, { horizontal: true },
{horizontal: false}, { horizontal: false },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: false}, { horizontal: false },
{horizontal: false}, { horizontal: false },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
{horizontal: true}, { horizontal: true },
] ];
export const Text = ({ currentQuestion, stepNumber }: TextProps) => { export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizData(); const { settings } = useQuizData();
const spec = settings.cfg.spec const spec = settings.cfg.spec;
const { quizId } = useQuizData(); const { quizId } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } =
const [isSending, setIsSending] = useState<boolean>(false); answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
const inputHC = useDebouncedCallback(async (text) => { const inputHC = useDebouncedCallback(async (text) => {
setIsSending(true); setIsSending(true);
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: text, body: text,
qid: quizId, qid: quizId,
}); });
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}, 400);
useEffect(
() => () => {
inputHC.flush();
},
[inputHC]
);
switch (spec) {
case true:
return <TextSpecial currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} stepNumber={stepNumber}/>;
case undefined:
return <TextNormal currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} />;
default:
return <TextNormal currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} />;
} }
setIsSending(false);
}, 400);
useEffect(
() => () => {
inputHC.flush();
},
[inputHC]
);
switch (spec) {
case true:
return (
<TextSpecial
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
stepNumber={stepNumber}
/>
);
case undefined:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
default:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
}
}; };
interface Props { interface Props {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
answer: any, answer: any;
inputHC: (a: string) => void; inputHC: (a: string) => void;
stepNumber?: number | null; stepNumber?: number | null;
} }
const TextNormal = ({currentQuestion, answer, inputHC}: Props) => { const TextNormal = ({ currentQuestion, answer, inputHC }: Props) => {
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const theme = useTheme(); const theme = useTheme();
return( const { settings } = useQuizData();
<Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: "center",
}}
>
<CustomTextField
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={async ({ target }) => {
updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}}
sx={{
"& .MuiOutlinedInput-root": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF"
},
"&:focus-visible": { borderColor: theme.palette.primary.main },
}}
/>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box <Box
sx={{ sx={{
display: "flex", maxWidth: "400px",
width: "100%", width: "100%",
marginTop: "20px", height: "300px",
flexDirection: isMobile ? "column-reverse" : undefined, margin: "15px",
alignItems: "center" }}
}}
> >
<CustomTextField <img
placeholder={currentQuestion.content.placeholder} key={currentQuestion.id}
value={answer || ""} src={currentQuestion.content.back}
onChange={async ({ target }) => { style={{ width: "100%", height: "100%", objectFit: "cover" }}
updateAnswer(currentQuestion.id, target.value, 0); alt=""
inputHC(target.value); />
}}
sx={{
"&:focus-visible": {
borderColor: theme.palette.primary.main
}
}}
/>
{currentQuestion.content.back && currentQuestion.content.back !== " " && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px", margin: "15px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box> </Box>
</Box> )}
) </Box>
</Box>
);
}; };
const TextSpecial = ({currentQuestion, answer, inputHC, stepNumber}: Props) => { const TextSpecial = ({
const theme = useTheme(); currentQuestion,
const isMobile = useRootContainerSize() < 650; answer,
const isHorizontal = Orientation[Number(stepNumber) -1].horizontal inputHC,
return( stepNumber,
<Box sx={{display: "flex", flexDirection: isMobile? "column" : undefined, alignItems: isMobile ? "center" : undefined,}}> }: Props) => {
<Box const theme = useTheme();
sx={{ const isMobile = useRootContainerSize() < 650;
display: "flex", const isHorizontal = Orientation[Number(stepNumber) - 1].horizontal;
width: "100%", const { settings } = useQuizData();
marginTop: "20px",
flexDirection: "column", return (
alignItems: "center", <Box
gap: "20px" sx={{
}} display: "flex",
> flexDirection: isMobile ? "column" : undefined,
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> alignItems: isMobile ? "center" : undefined,
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && ( }}
<Box sx={{margin: "30px", width: "50vw", maxHeight: "550px" }}> >
<img <Box
key={currentQuestion.id} sx={{
src={currentQuestion.content.back} display: "flex",
style={{ width: "100%", height: "100%", objectFit: "cover" }} width: "100%",
alt="" marginTop: "20px",
/> flexDirection: "column",
</Box> alignItems: "center",
)} gap: "20px",
{ }}
(<TextField >
autoFocus={true} <Typography
multiline variant="h5"
maxRows={4} color={theme.palette.text.primary}
placeholder={currentQuestion.content.placeholder} sx={{ wordBreak: "break-word" }}
value={answer || ""} >
onChange={async ({ target }:ChangeEvent<HTMLInputElement>) => { {currentQuestion.title}
updateAnswer(currentQuestion.id, target.value, 0); </Typography>
inputHC(target.value); {isHorizontal &&
} currentQuestion.content.back &&
} currentQuestion.content.back !== " " && (
inputProps={{maxLength:400}} <Box sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}>
sx={{ <img
width: "100%", key={currentQuestion.id}
"&:focus-visible": { src={currentQuestion.content.back}
borderColor: theme.palette.primary.main style={{ width: "100%", height: "100%", objectFit: "cover" }}
} alt=""
}} />
/>)
}
</Box> </Box>
{!isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && ( )}
<Box sx={{margin: "15px", width: "40vw" }}> {
<img <TextField
key={currentQuestion.id} autoFocus={true}
src={currentQuestion.content.back} multiline
style={{ width: "100%", height: "100%", objectFit: "cover" }} maxRows={4}
alt="" placeholder={currentQuestion.content.placeholder}
/> value={answer || ""}
</Box> onChange={async ({ target }: ChangeEvent<HTMLInputElement>) => {
)} updateAnswer(currentQuestion.id, target.value, 0);
</Box> inputHC(target.value);
) }}
} inputProps={{
maxLength: 400,
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
sx={{
width: "100%",
"& .MuiOutlinedInput-root": {
backgroundColor: settings.cfg.design
? "rgba(154,154,175, 0.2)"
: "#FFFFFF",
},
"&:focus-visible": {
borderColor: theme.palette.primary.main,
},
}}
/>
}
</Box>
{!isHorizontal &&
currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ margin: "15px", width: "40vw" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
);
};

@ -1,22 +1,22 @@
import { import {
Box, Box,
Checkbox, Checkbox,
FormControlLabel, FormControlLabel,
FormGroup, FormGroup,
TextField as MuiTextField, TextField as MuiTextField,
Radio, Radio,
RadioGroup, RadioGroup,
TextFieldProps, TextFieldProps,
Typography, Typography,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { import {
deleteAnswer, deleteAnswer,
updateAnswer, updateAnswer,
updateOwnVariant, updateOwnVariant,
useQuizViewStore useQuizViewStore,
} from "@stores/quizView"; } from "@stores/quizView";
import { CheckboxIcon } from "@icons/Checkbox"; import { CheckboxIcon } from "@icons/Checkbox";
@ -36,245 +36,263 @@ import moment from "moment";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
type VariantProps = { type VariantProps = {
currentQuestion: QuizQuestionVariant; currentQuestion: QuizQuestionVariant;
}; };
export const Variant = ({ currentQuestion }: VariantProps) => { export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const { answers, ownVariants } = useQuizViewStore(); const { answers, ownVariants } = useQuizViewStore();
const { answer } = const { answer } =
answers.find( answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
({ questionId }) => questionId === currentQuestion.id const ownVariant = ownVariants.find(
) ?? {}; (variant) => variant.id === currentQuestion.id
const ownVariant = ownVariants.find( );
(variant) => variant.id === currentQuestion.id
);
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup; const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
useEffect(() => { useEffect(() => {
if (!ownVariant) { if (!ownVariant) {
updateOwnVariant(currentQuestion.id, ""); updateOwnVariant(currentQuestion.id, "");
} }
}, []); }, []);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question"); if (moment.isMoment(answer))
throw new Error("Answer is Moment in Variant question");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> <Typography
<Box sx={{ variant="h5"
display: "flex", gap: "20px", color={theme.palette.text.primary}
flexDirection: isMobile ? "column-reverse" : undefined, alignItems: isMobile ? "center" : undefined sx={{ wordBreak: "break-word" }}
}}> >
<Group {currentQuestion.title}
name={currentQuestion.id.toString()} </Typography>
value={currentQuestion.content.variants.findIndex( <Box
({ id }) => answer === id sx={{
)} display: "flex",
sx={{ gap: "20px",
display: "flex", flexDirection: isMobile ? "column-reverse" : undefined,
flexWrap: "wrap", alignItems: isMobile ? "center" : undefined,
flexDirection: "row", }}
justifyContent: "space-between", >
flexBasis: "100%", <Group
marginTop: "20px", name={currentQuestion.id.toString()}
width: isMobile ? "100%" : undefined value={currentQuestion.content.variants.findIndex(
}} ({ id }) => answer === id
> )}
<Box sx={{
sx={{ display: "flex",
display: "flex", flexWrap: "wrap",
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap", justifyContent: "space-between",
width: "100%", flexBasis: "100%",
gap: "20px", marginTop: "20px",
}} width: isMobile ? "100%" : undefined,
> }}
{currentQuestion.content.variants.map((variant, index) => ( >
<VariantItem <Box
key={variant.id} sx={{
currentQuestion={currentQuestion} display: "flex",
variant={variant} flexDirection: "row",
answer={answer} flexWrap: "wrap",
index={index} width: "100%",
isSending={isSending} gap: "20px",
setIsSending={setIsSending} }}
/> >
))} {currentQuestion.content.variants.map((variant, index) => (
{currentQuestion.content.own && ownVariant && ( <VariantItem
<VariantItem key={variant.id}
own currentQuestion={currentQuestion}
currentQuestion={currentQuestion} variant={variant}
variant={ownVariant.variant} answer={answer}
answer={answer} index={index}
index={currentQuestion.content.variants.length + 2} isSending={isSending}
isSending={isSending} setIsSending={setIsSending}
setIsSending={setIsSending} />
/> ))}
)} {currentQuestion.content.own && ownVariant && (
</Box> <VariantItem
</Group> own
{currentQuestion.content.back && currentQuestion.content.back !== " " && ( currentQuestion={currentQuestion}
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}> variant={ownVariant.variant}
<img answer={answer}
key={currentQuestion.id} index={currentQuestion.content.variants.length + 2}
src={currentQuestion.content.back} isSending={isSending}
style={{ width: "100%", height: "100%", objectFit: "cover" }} setIsSending={setIsSending}
alt="" />
/> )}
</Box> </Box>
)} </Group>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box> </Box>
</Box> )}
); </Box>
</Box>
);
}; };
const VariantItem = ({ const VariantItem = ({
currentQuestion, currentQuestion,
variant, variant,
answer, answer,
index, index,
own = false, own = false,
isSending, isSending,
setIsSending, setIsSending,
}: { }: {
currentQuestion: QuizQuestionVariant; currentQuestion: QuizQuestionVariant;
variant: QuestionVariant; variant: QuestionVariant;
answer: string | string[] | undefined; answer: string | string[] | undefined;
index: number; index: number;
own?: boolean; own?: boolean;
isSending: boolean; isSending: boolean;
setIsSending: (a: boolean) => void; setIsSending: (a: boolean) => void;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { settings, quizId } = useQuizData(); const { settings, quizId } = useQuizData();
return ( return (
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
disabled={isSending} disabled={isSending}
sx={{ sx={{
margin: "0", margin: "0",
borderRadius: "12px", borderRadius: "12px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: "15px", padding: "15px",
border: `1px solid`, border: `1px solid`,
borderColor: answer === variant.id borderColor:
? theme.palette.primary.main answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
: "#9A9AAF", backgroundColor: settings.cfg.design
backgroundColor: quizThemes[settings.cfg.theme].isLight ? quizThemes[settings.cfg.theme].isLight
? "white" ? "#FFFFFF"
: theme.palette.background.default, : "rgba(154,154,175, 0.2)"
display: "flex", : quizThemes[settings.cfg.theme].isLight
maxWidth: "685px", ? "white"
maxHeight: "85px", : theme.palette.background.default,
justifyContent: "space-between", display: "flex",
width: "100%", maxWidth: "685px",
"&.MuiFormControl-root": { maxHeight: "85px",
width: "100%", justifyContent: "space-between",
}, width: "100%",
"& .MuiFormControlLabel-label": { "&.MuiFormControl-root": {
wordBreak: "break-word", width: "100%",
height: variant.answer.length <= 60 ? undefined : "60px", },
overflow: "auto", "& .MuiFormControlLabel-label": {
lineHeight: "normal", wordBreak: "break-word",
"&::-webkit-scrollbar": { height: variant.answer.length <= 60 ? undefined : "60px",
width: "4px", overflow: "auto",
}, lineHeight: "normal",
"&::-webkit-scrollbar-thumb": { "&::-webkit-scrollbar": {
backgroundColor: "#b8babf", width: "4px",
} },
} "&::-webkit-scrollbar-thumb": {
}} backgroundColor: "#b8babf",
value={index} },
labelPlacement="start" },
control={ }}
currentQuestion.content.multi ? value={index}
<Checkbox labelPlacement="start"
checked={!!answer?.includes(variant.id)} control={
checkedIcon={<CheckboxIcon checked color={theme.palette.primary.main} />} currentQuestion.content.multi ? (
icon={<CheckboxIcon />} <Checkbox
/> checked={!!answer?.includes(variant.id)}
: checkedIcon={
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} /> <CheckboxIcon checked color={theme.palette.primary.main} />
} }
label={own ? <TextField label="Другое..." /> : variant.answer} icon={<CheckboxIcon />}
onClick={async (event) => { />
event.preventDefault(); ) : (
if (isSending) return; <Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true); setIsSending(true);
const variantId = currentQuestion.content.variants[index].id; const variantId = currentQuestion.content.variants[index].id;
console.log(answer); console.log(answer);
if (currentQuestion.content.multi) { if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : []; const currentAnswer = typeof answer !== "string" ? answer || [] : [];
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentAnswer.includes(variantId) body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId], : [...currentAnswer, variantId],
qid: quizId, qid: quizId,
}); });
updateAnswer( updateAnswer(
currentQuestion.id, currentQuestion.id,
currentAnswer.includes(variantId) currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId], : [...currentAnswer, variantId],
currentQuestion.content.variants[index].points || 0 currentQuestion.content.variants[index].points || 0
); );
} catch (e) { } catch (e) {
console.log(e); console.log(e);
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("ответ не был засчитан");
} }
setIsSending(false); setIsSending(false);
return; return;
} }
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
qid: quizId, qid: quizId,
}); });
updateAnswer(currentQuestion.id, variantId, updateAnswer(
answer === variantId ? 0 currentQuestion.id,
: variantId,
currentQuestion.content.variants[index].points || 0 answer === variantId
); ? 0
: currentQuestion.content.variants[index].points || 0
);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
} catch (e) { if (answer === variantId) {
console.log(e); try {
enqueueSnackbar("ответ не был засчитан"); await sendAnswer({
} questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
if (answer === variantId) { setIsSending(false);
try { }}
/>
await sendAnswer({ );
questionId: currentQuestion.id,
body: "",
qid: quizId,
});
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
}}
/>
);
}; };

@ -1,10 +1,10 @@
import { import {
Box, Box,
FormControlLabel, FormControlLabel,
Radio, Radio,
RadioGroup, RadioGroup,
Typography, Typography,
useTheme useTheme,
} from "@mui/material"; } from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView"; import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
@ -21,173 +21,194 @@ import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { useState } from "react"; import { useState } from "react";
type VarimgProps = { type VarimgProps = {
currentQuestion: QuizQuestionVarImg; currentQuestion: QuizQuestionVarImg;
}; };
export const Varimg = ({ currentQuestion }: VarimgProps) => { export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { settings, quizId } = useQuizData(); const { settings, quizId } = useQuizData();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
const { answer } = const { answer } =
answers.find( answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
({ questionId }) => questionId === currentQuestion.id const variant = currentQuestion.content.variants.find(
) ?? {}; ({ id }) => answer === id
const variant = currentQuestion.content.variants.find( );
({ id }) => answer === id
);
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography> <Typography
<Box sx={{ variant="h5"
display: "flex", color={theme.palette.text.primary}
marginTop: "20px", sx={{ wordBreak: "break-word" }}
flexDirection: isMobile ? "column-reverse" : undefined, >
gap: isMobile ? "30px" : undefined, {currentQuestion.title}
alignItems: isMobile ? "center" : undefined </Typography>
<Box
sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: isMobile ? "30px" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: isMobile ? "20px" : undefined,
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(154,154,175, 0.2)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
display: "flex",
margin: isMobile ? 0 : undefined,
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
onClick={async (event) => {
event.preventDefault();
}}> setIsSending(true);
<RadioGroup try {
name={currentQuestion.id} await sendAnswer({
value={currentQuestion.content.variants.findIndex( questionId: currentQuestion.id,
({ id }) => answer === id body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
)} qid: quizId,
sx={{ });
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
width: isMobile ? "100%" : undefined
}}
>
<Box sx={{ display: "flex", flexDirection: "column", width: "100%", gap: isMobile ? "20px" : undefined }}>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "5px",
padding: "15px",
color: theme.palette.text.primary,
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: isMobile ? 0 : undefined,
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
}
},
}}
value={index}
onClick={async (event) => {
event.preventDefault();
setIsSending(true); updateAnswer(
try { currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
await sendAnswer({ if (answer === currentQuestion.content.variants[index].id) {
questionId: currentQuestion.id, try {
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`, await sendAnswer({
qid: quizId, questionId: currentQuestion.id,
}); body: "",
qid: quizId,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
updateAnswer( setIsSending(false);
currentQuestion.id, }}
currentQuestion.content.variants[index].id, control={
currentQuestion.content.variants[index].points || 0 <Radio
); checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
} catch (e) { }
enqueueSnackbar("ответ не был засчитан"); icon={<RadioIcon />}
} />
}
label={variant.answer}
if (answer === currentQuestion.content.variants[index].id) { />
try { ))}
</Box>
await sendAnswer({ </RadioGroup>
questionId: currentQuestion.id, {/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
body: "", <Box
qid: quizId, sx={{
}); maxWidth: "450px",
width: "100%",
} catch (e) { height: "450px",
enqueueSnackbar("ответ не был засчитан"); border: "1px solid #9A9AAF",
} borderRadius: "12px",
deleteAnswer(currentQuestion.id); overflow: "hidden",
} display: "flex",
alignItems: "center",
setIsSending(false); justifyContent: "center",
}} backgroundColor: "#9A9AAF12",
control={ color: theme.palette.text.primary,
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} /> textAlign: "center",
} }}
label={variant.answer} >
/> {answer ? (
))} variant?.extendedText ? (
</Box> <img
</RadioGroup> src={variant?.extendedText}
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */} style={{ width: "100%", height: "100%", objectFit: "cover" }}
<Box alt=""
sx={{ />
maxWidth: "450px", ) : (
width: "100%", <BlankImage />
height: "450px", )
border: "1px solid #9A9AAF", ) : currentQuestion.content.back !== " " &&
borderRadius: "12px", currentQuestion.content.back !== null &&
overflow: "hidden", currentQuestion.content.back.length > 0 ? (
display: "flex", <img
alignItems: "center", src={currentQuestion.content.back}
justifyContent: "center", style={{ width: "100%", height: "100%", objectFit: "cover" }}
backgroundColor: "#9A9AAF12", alt=""
color: "#9A9AAF", />
textAlign: "center" ) : currentQuestion.content.replText !== " " &&
}} currentQuestion.content.replText.length > 0 ? (
> currentQuestion.content.replText
{answer ? ( ) : variant?.extendedText || isMobile ? (
variant?.extendedText ? ( "Выберите вариант ответа ниже"
<img ) : (
src={variant?.extendedText} "Выберите вариант ответа слева"
style={{ width: "100%", height: "100%", objectFit: "cover" }} )}
alt=""
/>
) : (
<BlankImage />
)
) : currentQuestion.content.back !== " "
&& currentQuestion.content.back !== null
&& currentQuestion.content.back.length > 0
? (
<img
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
) : (currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0) ? currentQuestion.content.replText : variant?.extendedText || isMobile ? (
"Выберите вариант ответа ниже"
) : (
"Выберите вариант ответа слева"
)}
</Box>
{/* )} */}
</Box>
</Box> </Box>
); {/* )} */}
</Box>
</Box>
);
}; };

@ -121,8 +121,7 @@ export const Select = ({
"& .MuiTypography-root": { "& .MuiTypography-root": {
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
} },
}, },
}} }}
IconComponent={(props) => <ArrowDown {...props} />} IconComponent={(props) => <ArrowDown {...props} />}
@ -139,7 +138,7 @@ export const Select = ({
borderRadius: "5px", borderRadius: "5px",
color: colorPlaceholder, color: colorPlaceholder,
whiteSpace: "normal", whiteSpace: "normal",
wordBreak: "break-word" wordBreak: "break-word",
}} }}
> >
{item} {item}

@ -11,115 +11,126 @@ export type QuizResultsType = true | null;
export type QuizStep = "startpage" | "question" | "contactform"; export type QuizStep = "startpage" | "question" | "contactform";
export type QuizTheme = export type QuizTheme =
| "StandardTheme" | "StandardTheme"
| "StandardDarkTheme" | "StandardDarkTheme"
| "PinkTheme" | "PinkTheme"
| "PinkDarkTheme" | "PinkDarkTheme"
| "BlackWhiteTheme" | "BlackWhiteTheme"
| "OliveTheme" | "OliveTheme"
| "YellowTheme" | "YellowTheme"
| "GoldDarkTheme" | "GoldDarkTheme"
| "PurpleTheme" | "PurpleTheme"
| "BlueTheme" | "BlueTheme"
| "BlueDarkTheme"; | "BlueDarkTheme"
| "Design1"
| "Design2"
| "Design3"
| "Design4"
| "Design5"
| "Design6"
| "Design7"
| "Design8"
| "Design9"
| "Design10";
export type FCField = { export type FCField = {
text: string; text: string;
innerText: string; innerText: string;
key: string; key: string;
required: boolean; required: boolean;
used: boolean; used: boolean;
}; };
export type QuizSettings = { export type QuizSettings = {
questions: AnyTypedQuizQuestion[]; questions: AnyTypedQuizQuestion[];
settings: { settings: {
fp: boolean; fp: boolean;
rep: boolean; rep: boolean;
name: string; name: string;
lim: number; lim: number;
due: number; due: number;
delay: number; delay: number;
pausable: boolean; pausable: boolean;
cfg: QuizConfig; cfg: QuizConfig;
}; };
cnt: number; cnt: number;
recentlyCompleted: boolean; recentlyCompleted: boolean;
show_badge: boolean; show_badge: boolean;
}; };
export interface QuizConfig { export interface QuizConfig {
spec: undefined | true, spec: undefined | true;
type: QuizType; type: QuizType;
noStartPage: boolean; noStartPage: boolean;
startpageType: QuizStartpageType; startpageType: QuizStartpageType;
score?: boolean; score?: boolean;
results: QuizResultsType; results: QuizResultsType;
haveRoot: string | null; haveRoot: string | null;
theme: QuizTheme; theme: QuizTheme;
resultInfo: { design: boolean;
when: "email" | ""; resultInfo: {
share: boolean; when: "email" | "";
replay: boolean; share: boolean;
theme: string; replay: boolean;
reply: string; theme: string;
replname: string; reply: string;
showResultForm: "before" | "after"; replname: string;
showResultForm: "before" | "after";
};
startpage: {
description: string;
button: string;
position: QuizStartpageAlignType;
favIcon: string | null;
logo: string | null;
originalLogo: string | null;
background: {
type: null | "image" | "video";
desktop: string | null;
originalDesktop: string | null;
mobile: string | null;
originalMobile: string | null;
video: string | null;
cycle: boolean;
}; };
startpage: { };
description: string; formContact: {
button: string; title: string;
position: QuizStartpageAlignType; desc: string;
favIcon: string | null; fields: Record<FormContactFieldName, FormContactFieldData>;
logo: string | null; button: string;
originalLogo: string | null; };
background: { info: {
type: null | "image" | "video"; phonenumber: string;
desktop: string | null; clickable: boolean;
originalDesktop: string | null; orgname: string;
mobile: string | null; site: string;
originalMobile: string | null; law?: string;
video: string | null; };
cycle: boolean; meta: string;
};
};
formContact: {
title: string;
desc: string;
fields: Record<FormContactFieldName, FormContactFieldData>;
button: string;
};
info: {
phonenumber: string;
clickable: boolean;
orgname: string;
site: string;
law?: string;
};
meta: string;
} }
export type FormContactFieldName = export type FormContactFieldName =
| "name" | "name"
| "email" | "email"
| "phone" | "phone"
| "text" | "text"
| "address"; | "address";
type FormContactFieldData = { type FormContactFieldData = {
text: string; text: string;
innerText: string; innerText: string;
key: string; key: string;
required: boolean; required: boolean;
used: boolean; used: boolean;
}; };
export interface QuizItems { export interface QuizItems {
description: string; description: string;
id: number; id: number;
page: number; page: number;
required: boolean; required: boolean;
title: string; title: string;
type: string; type: string;
content: unknown; content: unknown;
} }

37
lib/ui_kit/Stepper.tsx Normal file

@ -0,0 +1,37 @@
import { useTheme } from "@mui/material";
import MobileStepper from "@mui/material/MobileStepper";
import { hexToRgba } from "@utils/hexToRgba";
interface Props {
activeStep: number;
steps: number;
}
export default function ProgressMobileStepper({ activeStep, steps }: Props) {
const theme = useTheme();
return (
<MobileStepper
variant="progress"
steps={steps + 1}
position="static"
activeStep={activeStep}
sx={{
width: "100%",
padding: "10px 0 0",
background: "transparent",
"& .MuiLinearProgress-root": {
height: "4px",
background: theme.palette.primary.light,
width: "100%",
},
"& .MuiLinearProgress-bar": { background: theme.palette.primary.main },
"& .MuiMobileStepper-progress": {
background: hexToRgba(theme.palette.primary.main, 0.5),
},
}}
nextButton={<></>}
backButton={<></>}
/>
);
}

7
lib/utils/hexToRgba.ts Normal file

@ -0,0 +1,7 @@
import hexRgb from "hex-rgb";
export const hexToRgba = (hexColor: string, opacity?: number): string => {
const { red, green, blue, alpha } = hexRgb(hexColor);
return `rgba(${red}, ${green}, ${blue}, ${opacity || alpha})`;
};

@ -1,229 +1,431 @@
import { QuizTheme } from "@model/settingsData"; import { createTheme } from "@mui/material";
import { Theme, createTheme } from "@mui/material";
import themePublic from "./genericPublication"; import themePublic from "./genericPublication";
import type { Theme } from "@mui/material";
import type { QuizTheme } from "@model/settingsData";
const StandardTheme = createTheme({ const StandardTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#7E2AEA", main: "#7E2AEA",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#7E2AEA", secondary: "#7E2AEA",
}, },
background: { background: {
default: "#FFFFFF", default: "#FFFFFF",
}, },
} },
}) });
const StandardDarkTheme = createTheme({ const StandardDarkTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#7E2AEA", main: "#7E2AEA",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#FFFFFF", primary: "#FFFFFF",
secondary: "#7E2AEA", secondary: "#7E2AEA",
}, },
background: { background: {
default: "#333647", default: "#333647",
}, },
} },
}) });
const PinkTheme = createTheme({ const PinkTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#D34085", main: "#D34085",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#D34085", secondary: "#D34085",
}, },
background: { background: {
default: "#FFF9FC", default: "#FFF9FC",
}, },
} },
}) });
const PinkDarkTheme = createTheme({ const PinkDarkTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#D34085", main: "#D34085",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#FFFFFF", primary: "#FFFFFF",
secondary: "#D34085", secondary: "#D34085",
}, },
background: { background: {
default: "#333647", default: "#333647",
}, },
} },
}) });
const BlackWhiteTheme = createTheme({ const BlackWhiteTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#4E4D51", main: "#4E4D51",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#FFF9FC", secondary: "#FFF9FC",
}, },
background: { background: {
default: "#FFFFFF", default: "#FFFFFF",
}, },
} },
}) });
const OliveTheme = createTheme({ const OliveTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#758E4F", main: "#758E4F",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#758E4F", secondary: "#758E4F",
}, },
background: { background: {
default: "#F9FBF1", default: "#F9FBF1",
}, },
} },
}) });
const PurpleTheme = createTheme({ const PurpleTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#7E2AEA", main: "#7E2AEA",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#7E2AEA", secondary: "#7E2AEA",
}, },
background: { background: {
default: "#FBF8FF", default: "#FBF8FF",
}, },
} },
}) });
const YellowTheme = createTheme({ const YellowTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#F2B133", main: "#F2B133",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#F2B133", secondary: "#F2B133",
}, },
background: { background: {
default: "#FFFCF6", default: "#FFFCF6",
}, },
} },
}) });
const GoldDarkTheme = createTheme({ const GoldDarkTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#E6AA37", main: "#E6AA37",
}, },
secondary: { secondary: {
main: "#FFFCF6", main: "#FFFCF6",
}, },
text: { text: {
primary: "#FFFFFF", primary: "#FFFFFF",
secondary: "#F2B133", secondary: "#F2B133",
}, },
background: { background: {
default: "#333647", default: "#333647",
}, },
} },
}) });
const BlueTheme = createTheme({ const BlueTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#4964ED", main: "#4964ED",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#333647", primary: "#333647",
secondary: "#4964ED", secondary: "#4964ED",
}, },
background: { background: {
default: "#F5F7FF", default: "#F5F7FF",
}, },
} },
}) });
const BlueDarkTheme = createTheme({ const BlueDarkTheme = createTheme({
...themePublic, ...themePublic,
palette: { palette: {
primary: { primary: {
main: "#07A0C3", main: "#07A0C3",
}, },
secondary: { secondary: {
main: "#252734" main: "#252734",
}, },
text: { text: {
primary: "#FFFFFF", primary: "#FFFFFF",
secondary: "#07A0C3", secondary: "#07A0C3",
}, },
background: { background: {
default: "#333647", default: "#333647",
}, },
} },
}) });
export const quizThemes: Record<QuizTheme, { theme: Theme; isLight: boolean; }> = { const Design1 = createTheme({
...themePublic,
palette: {
primary: {
main: "#F2B133",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#F2B133",
},
background: {
default: "#333647",
},
},
});
const Design2 = createTheme({
...themePublic,
palette: {
primary: {
main: "#3D9A63",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#3D9A63",
},
background: {
default: "#333647",
},
},
});
const Design3 = createTheme({
...themePublic,
palette: {
primary: {
main: "#4B6A99",
},
secondary: {
main: "#252734",
},
text: {
primary: "#252734",
secondary: "#4B6A99",
},
background: {
default: "#F5F7FF",
},
},
});
const Design4 = createTheme({
...themePublic,
palette: {
primary: {
main: "#FF9431",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#FF9431",
},
background: {
default: "#333647",
},
},
});
const Design5 = createTheme({
...themePublic,
palette: {
primary: {
main: "#2D99BA",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#2D99BA",
},
background: {
default: "#333647",
},
},
});
const Design6 = createTheme({
...themePublic,
palette: {
primary: {
main: "#D34085",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#D34085",
},
background: {
default: "#333647",
},
},
});
const Design7 = createTheme({
...themePublic,
palette: {
primary: {
main: "#B47C3B",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#B47C3B",
},
background: {
default: "#333647",
},
},
});
const Design8 = createTheme({
...themePublic,
palette: {
primary: {
main: "#F0B136",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#F0B136",
},
background: {
default: "#333647",
},
},
});
const Design9 = createTheme({
...themePublic,
palette: {
primary: {
main: "#678F48",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#678F48",
},
background: {
default: "#333647",
},
},
});
const Design10 = createTheme({
...themePublic,
palette: {
primary: {
main: "#3666AF",
},
secondary: {
main: "#252734",
},
text: {
primary: "#FFFFFF",
secondary: "#3666AF",
},
background: {
default: "#333647",
},
},
});
export const quizThemes: Record<QuizTheme, { theme: Theme; isLight: boolean }> =
{
StandardTheme: { theme: StandardTheme, isLight: true }, StandardTheme: { theme: StandardTheme, isLight: true },
StandardDarkTheme: { theme: StandardDarkTheme, isLight: false }, StandardDarkTheme: { theme: StandardDarkTheme, isLight: false },
PinkTheme: { theme: PinkTheme, isLight: true }, PinkTheme: { theme: PinkTheme, isLight: true },
@ -235,4 +437,14 @@ export const quizThemes: Record<QuizTheme, { theme: Theme; isLight: boolean; }>
PurpleTheme: { theme: PurpleTheme, isLight: true }, PurpleTheme: { theme: PurpleTheme, isLight: true },
BlueTheme: { theme: BlueTheme, isLight: true }, BlueTheme: { theme: BlueTheme, isLight: true },
BlueDarkTheme: { theme: BlueDarkTheme, isLight: false }, BlueDarkTheme: { theme: BlueDarkTheme, isLight: false },
}; Design1: { theme: Design1, isLight: false },
Design2: { theme: Design2, isLight: false },
Design3: { theme: Design3, isLight: true },
Design4: { theme: Design4, isLight: false },
Design5: { theme: Design5, isLight: false },
Design6: { theme: Design6, isLight: false },
Design7: { theme: Design7, isLight: false },
Design8: { theme: Design8, isLight: false },
Design9: { theme: Design9, isLight: false },
Design10: { theme: Design10, isLight: false },
};

@ -85,6 +85,7 @@
"zustand": "^4.3.8" "zustand": "^4.3.8"
}, },
"dependencies": { "dependencies": {
"country-flag-emoji-polyfill": "^0.1.8" "country-flag-emoji-polyfill": "^0.1.8",
"hex-rgb": "^5.0.0"
} }
} }

@ -7,32 +7,33 @@ import QuizAnswerer from "../lib/components/QuizAnswerer";
import { ApologyPage } from "../lib/components/ViewPublicationPage/ApologyPage"; import { ApologyPage } from "../lib/components/ViewPublicationPage/ApologyPage";
// const defaultQuizId = "45ef7f9c-784d-4e58-badb-f6b337f08ba0"; // branching // const defaultQuizId = "45ef7f9c-784d-4e58-badb-f6b337f08ba0"; // branching
const defaultQuizId = "cde381db-8ccb-402c-b55f-2c814be9bf25"; //looooong header const defaultQuizId = "eb1a0fd5-8484-4c96-8d73-4af747eaf02d"; //looooong header
// const defaultQuizId = "ad7f5a87-b833-4f5b-854e-453706ed655c"; // linear // const defaultQuizId = "ad7f5a87-b833-4f5b-854e-453706ed655c"; // linear
export default function App() { export default function App() {
const quizId = useParams().quizId ?? defaultQuizId; const quizId = useParams().quizId ?? defaultQuizId;
const { data, error, isLoading } = useSWR(["quizData", quizId], params => getQuizData(params[1]), { const { data, error, isLoading } = useSWR(
revalidateOnFocus: false, ["quizData", quizId],
revalidateOnReconnect: false, (params) => getQuizData(params[1]),
shouldRetryOnError: false, {
refreshInterval: 0, revalidateOnFocus: false,
}); revalidateOnReconnect: false,
shouldRetryOnError: false,
refreshInterval: 0,
}
);
if (isLoading) return <LoadingSkeleton />; if (isLoading) return <LoadingSkeleton />;
if (error) return <ApologyPage error={error} />; if (error) return <ApologyPage error={error} />;
if (!data) throw new Error("Quiz data is null"); if (!data) throw new Error("Quiz data is null");
return ( return (
<Box sx={{ <Box
// height: "100dvh", sx={{
height: "600px", height: "100dvh",
width: "600px", }}
}}> >
<QuizAnswerer <QuizAnswerer quizSettings={data} quizId={quizId} />
quizSettings={data} </Box>
quizId={quizId} );
/>
</Box>
);
} }

@ -20,6 +20,9 @@
"strict": true, "strict": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"paths": { "paths": {
"@/*": [
"./lib/*"
],
"@ui_kit/*": [ "@ui_kit/*": [
"./lib/ui_kit/*" "./lib/ui_kit/*"
], ],

@ -3,6 +3,7 @@ import { resolve } from "path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
export const alias = { export const alias = {
"@": resolve(__dirname, "./lib/"),
"@ui_kit": resolve(__dirname, "./lib/ui_kit"), "@ui_kit": resolve(__dirname, "./lib/ui_kit"),
"@icons": resolve(__dirname, "./lib/assets/icons"), "@icons": resolve(__dirname, "./lib/assets/icons"),
"@stores": resolve(__dirname, "./lib/stores"), "@stores": resolve(__dirname, "./lib/stores"),

@ -2269,6 +2269,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hex-rgb@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-5.0.0.tgz#e2c9eb6a37498d66c5a350a221ed4c2c7d1a92d6"
integrity sha512-NQO+lgVUCtHxZ792FodgW0zflK+ozS9X9dwGp9XvvmPlH7pyxd588cn24TD3rmPm/N0AIRXF10Otah8yKqGw4w==
hoist-non-react-statics@^3.3.1: hoist-non-react-statics@^3.3.1:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"