make quiz view store component store

move design list to separate file
This commit is contained in:
nflnkr 2024-04-03 15:42:12 +03:00
parent 73e6a95902
commit 979d0e7138
22 changed files with 2740 additions and 2731 deletions

@ -16,6 +16,7 @@ import { ErrorBoundary } from "react-error-boundary";
import useSWR from "swr";
import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
import { QuizViewContext, createQuizViewStore } from "@/stores/quizView";
moment.locale("ru");
@ -28,6 +29,7 @@ type Props = {
};
export default function QuizAnswerer({ quizSettings, quizId, preview = false }: Props) {
const [quizViewStore] = useState(createQuizViewStore);
const [rootContainerWidth, setRootContainerWidth] = useState<number>(() => window.innerWidth);
const rootContainerRef = useRef<HTMLDivElement>(null);
const { data, error, isLoading } = useSWR(quizSettings ? null : ["quizData", quizId], params => getQuizData(params[1]), {
@ -61,33 +63,35 @@ export default function QuizAnswerer({ quizSettings, quizId, preview = false }:
if (!quizSettings) throw new Error("Quiz data is null");
return (
<RootContainerWidthContext.Provider value={rootContainerWidth}>
<QuizDataContext.Provider value={{ ...quizSettings, quizId, preview }}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<CssBaseline />
<Box
ref={rootContainerRef}
sx={{
width: "100%",
height: "100%",
}}
<QuizViewContext.Provider value={quizViewStore}>
<RootContainerWidthContext.Provider value={rootContainerWidth}>
<QuizDataContext.Provider value={{ ...quizSettings, quizId, preview }}>
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="ru" localeText={localeText}>
<ThemeProvider theme={lightTheme}>
<SnackbarProvider
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={handleComponentError}
<CssBaseline />
<Box
ref={rootContainerRef}
sx={{
width: "100%",
height: "100%",
}}
>
<ViewPublicationPage />
</ErrorBoundary>
</Box>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</QuizDataContext.Provider>
</RootContainerWidthContext.Provider>
<ErrorBoundary
FallbackComponent={ApologyPage}
onError={handleComponentError}
>
<ViewPublicationPage />
</ErrorBoundary>
</Box>
</SnackbarProvider>
</ThemeProvider>
</LocalizationProvider>
</QuizDataContext.Provider>
</RootContainerWidthContext.Provider>
</QuizViewContext.Provider>
);
}

@ -25,7 +25,7 @@ import {quizThemes} from "@utils/themes/Publication/themePublication";
import {enqueueSnackbar} from "notistack";
import {useRootContainerSize} from "../../contexts/RootContainerWidthContext";
import {useQuizData} from "@contexts/QuizDataContext";
import {DESIGN_LIST} from "@/components/ViewPublicationPage/Question";
import { DESIGN_LIST } from "@/utils/designList";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)

@ -21,161 +21,128 @@ import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication";
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";
import { DESIGN_LIST } from "@/utils/designList";
type Props = {
currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null;
nextButton: 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: "",
currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null;
nextButton: ReactNode;
prevButton: ReactNode;
};
export const Question = ({
currentQuestion,
currentQuestionStepNumber,
nextButton,
prevButton,
currentQuestion,
currentQuestionStepNumber,
nextButton,
prevButton,
}: Props) => {
const theme = useTheme();
const { settings, show_badge } = useQuizData();
const theme = useTheme();
const { settings, show_badge } = useQuizData();
return (
<Box
sx={{
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
>
<Box
sx={{
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "transparent"
: "linear-gradient(90deg,#272626, transparent)"
: theme.palette.background.default,
overflow: "hidden"
}}
>
return (
<Box
sx={{
height: "calc(100% - 75px)",
overflow: "auto",
width: "100%"
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
>
<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 && (
<Link target="_blank" href="https://quiz.pena.digital" sx={{mt: "20px", textAlign: "end"}}>
{quizThemes[settings.cfg.theme].isLight ? (
<NameplateLogoFQ
style={{ fontSize: "34px", width: "200px", height: "auto" }}
/>
) : (
<NameplateLogoFQDark
style={{ fontSize: "34px", width: "200px", height: "auto" }}
/>
)}
</Link>
)}
</Box>
<Box
sx={{
height: "100%",
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "transparent"
: "linear-gradient(90deg,#272626, transparent)"
: theme.palette.background.default,
overflow: "hidden"
}}
>
<Box
sx={{
height: "calc(100% - 75px)",
overflow: "auto",
width: "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 && (
<Link target="_blank" href="https://quiz.pena.digital" sx={{ mt: "20px", textAlign: "end" }}>
{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>
<Footer
stepNumber={currentQuestionStepNumber}
prevButton={prevButton}
nextButton={nextButton}
/>
</Box>
</Box>
);
);
};
function QuestionByType({
question,
stepNumber,
question,
stepNumber,
}: {
question: RealTypedQuizQuestion;
stepNumber: number | null;
question: RealTypedQuizQuestion;
stepNumber: number | null;
}) {
switch (question.type) {
case "variant":
return <Variant currentQuestion={question} />;
case "images":
return <Images currentQuestion={question} />;
case "varimg":
return <Varimg currentQuestion={question} />;
case "emoji":
return <Emoji currentQuestion={question} />;
case "text":
return <Text currentQuestion={question} stepNumber={stepNumber} />;
case "select":
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);
}
switch (question.type) {
case "variant":
return <Variant currentQuestion={question} />;
case "images":
return <Images currentQuestion={question} />;
case "varimg":
return <Varimg currentQuestion={question} />;
case "emoji":
return <Emoji currentQuestion={question} />;
case "text":
return <Text currentQuestion={question} stepNumber={stepNumber} />;
case "select":
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);
}
}

@ -5,27 +5,28 @@ import {
useTheme
} from "@mui/material";
import {NameplateLogo} from "@icons/NameplateLogo";
import { NameplateLogo } from "@icons/NameplateLogo";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import {useQuizData} from "@contexts/QuizDataContext";
import {quizThemes} from "@utils/themes/Publication/themePublication";
import {useRootContainerSize} from "../../contexts/RootContainerWidthContext";
import type {QuizQuestionResult} from "../../model/questionTypes/result";
import {setCurrentQuizStep} from "@stores/quizView";
import {DESIGN_LIST} from "@/components/ViewPublicationPage/Question";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useRootContainerSize } from "../../contexts/RootContainerWidthContext";
import type { QuizQuestionResult } from "../../model/questionTypes/result";
import { useQuizViewStore } from "@/stores/quizView";
import { DESIGN_LIST } from "@/utils/designList";
type ResultFormProps = {
resultQuestion: QuizQuestionResult;
};
export const ResultForm = ({resultQuestion}: ResultFormProps) => {
export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 1000;
const {settings, show_badge, quizId} = useQuizData();
const spec = settings.cfg.spec
console.log(quizThemes[settings.cfg.theme].isLight)
const { settings, show_badge, quizId } = useQuizData();
const setCurrentQuizStep = useQuizViewStore(state => state.setCurrentQuizStep);
const spec = settings.cfg.spec;
console.log(quizThemes[settings.cfg.theme].isLight);
return (
<Box
@ -73,15 +74,15 @@ export const ResultForm = ({resultQuestion}: ResultFormProps) => {
}}
>
{settings.cfg.startpage.logo &&
<img
src={settings.cfg.startpage.logo}
style={{
height: "40px",
maxWidth: "110px",
objectFit: "cover",
}}
alt=""
/>
<img
src={settings.cfg.startpage.logo}
style={{
height: "40px",
maxWidth: "110px",
objectFit: "cover",
}}
alt=""
/>
}
<Typography
sx={{
@ -133,19 +134,19 @@ export const ResultForm = ({resultQuestion}: ResultFormProps) => {
)
}
{resultQuestion.description !== "" &&
resultQuestion.description !== " " && (
<Typography
sx={{
fontSize: "23px",
fontWeight: 700,
m: "20px 0",
color: theme.palette.text.primary,
wordBreak: "break-word"
}}
>
{resultQuestion.description}
</Typography>
)}
resultQuestion.description !== " " && (
<Typography
sx={{
fontSize: "23px",
fontWeight: 700,
m: "20px 0",
color: theme.palette.text.primary,
wordBreak: "break-word"
}}
>
{resultQuestion.description}
</Typography>
)}
<Typography
sx={{
@ -187,37 +188,37 @@ export const ResultForm = ({resultQuestion}: ResultFormProps) => {
}}
>
{show_badge &&
<Box
component={Link}
target={"_blank"}
href={
`https://${window.location.hostname.includes("s") ? "s" : ""}quiz.pena.digital/squiz/quiz/logo?q=${quizId}`
}
sx={{
display: "flex",
alignItems: "center",
mt: "15px",
gap: "10px",
textDecoration: "none",
mb: "15px"
}}
>
<NameplateLogo
style={{
fontSize: "23px",
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
}}
/>
<Typography
<Box
component={Link}
target={"_blank"}
href={
`https://${window.location.hostname.includes("s") ? "s" : ""}quiz.pena.digital/squiz/quiz/logo?q=${quizId}`
}
sx={{
fontSize: "14px",
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
mt: "15px",
gap: "10px",
textDecoration: "none",
mb: "15px"
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
<NameplateLogo
style={{
fontSize: "23px",
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
}}
/>
<Typography
sx={{
fontSize: "14px",
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap",
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
}
</Box>
@ -234,16 +235,16 @@ export const ResultForm = ({resultQuestion}: ResultFormProps) => {
p:
(
settings.cfg.resultInfo.showResultForm === "before" &&
!Boolean(settings.cfg.score)
!settings.cfg.score
) ||
(
settings.cfg.resultInfo.showResultForm === "after" &&
resultQuestion.content.redirect
)
(
settings.cfg.resultInfo.showResultForm === "after" &&
resultQuestion.content.redirect
)
? "20px" : "0",
}}
>
{settings.cfg.resultInfo.showResultForm === "before" && !Boolean(settings.cfg.score) && (
{settings.cfg.resultInfo.showResultForm === "before" && !settings.cfg.score && (
<Button
onClick={() => setCurrentQuizStep("contactform")}
variant="contained"
@ -257,15 +258,15 @@ export const ResultForm = ({resultQuestion}: ResultFormProps) => {
</Button>
)}
{settings.cfg.resultInfo.showResultForm === "after" &&
resultQuestion.content.redirect && (
<Button
href={resultQuestion.content.redirect}
variant="contained"
sx={{p: "10px 20px", width: "auto", height: "50px"}}
>
{resultQuestion.content.hint.text || "Перейти на сайт"}
</Button>
)}
resultQuestion.content.redirect && (
<Button
href={resultQuestion.content.redirect}
variant="contained"
sx={{ p: "10px 20px", width: "auto", height: "50px" }}
>
{resultQuestion.content.hint.text || "Перейти на сайт"}
</Button>
)}
</Box>
</Box>
</Box>

@ -5,209 +5,215 @@ 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,
QuizStartpageAlignType,
QuizStartpageType,
} from "@model/settingsData";
import { DESIGN_LIST } from "@/utils/designList";
type StartPageDesktopProps = {
quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType;
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,
alignType,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => {
const { settings } = useQuizData();
const { settings } = useQuizData();
return (
<Box
id="pain"
sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
height: "100%",
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>
);
return (
<Box
id="pain"
sx={{
display: "flex",
flexDirection: alignType === "left" ? "row" : "row-reverse",
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
"&::-webkit-scrollbar": { width: 0 },
overflowY: "auto",
}}
>
<Box
sx={{
width: "40%",
height: "100%",
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,
alignType,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => (
<>
<Box
sx={{
height: "100%",
width: "40%",
padding: "16px",
margin:
alignType === "center"
? "0 auto"
: alignType === "left"
? "0"
: "0 0 0 auto",
}}
>
<Box
sx={{
padding: "16px",
minHeight: "calc(100% - 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
sx={{
height: "100%",
width: "40%",
padding: "16px",
margin:
alignType === "center"
? "0 auto"
: alignType === "left"
? "0"
: "0 0 0 auto",
"&::-webkit-scrollbar": { width: 0 },
overflowY: "auto",
}}
>
<Box
sx={{
padding: "16px",
minHeight: "calc(100% - 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>
</>
);
const CenteredLayout = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
}: LayoutProps) => {
const isTablet = useRootContainerSize() < 1100;
const isTablet = useRootContainerSize() < 1100;
const { settings } = useQuizData();
return (
<Box
sx={{
overflow: "auto",
padding: "10px 25px 25px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
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 && (
return (
<Box
sx={{
width: "100%",
maxWidth: "844px",
height: isTablet ? "530px" : "306px",
display: "flex",
justifyContent: "center",
"& > img": { width: "100%", borderRadius: "12px" },
}}
sx={{
overflow: "auto",
padding: "10px 25px 25px",
display: "flex",
flexDirection: "column",
alignItems: "center",
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
"&::-webkit-scrollbar": { width: 0 },
overflowY: "auto",
}}
>
{backgroundBlock}
{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>
)}
{quizMainBlock}
</Box>
);
);
};
export const StartPageDesktop = ({
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
quizHeaderBlock,
quizMainBlock,
backgroundBlock,
startpageType,
alignType,
}: StartPageDesktopProps) => {
switch (startpageType) {
case null:
case "standard": {
return (
<StandartLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
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 "expanded": {
return (
<ExpandedLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "centered": {
return (
<CenteredLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
case "centered": {
return (
<CenteredLayout
alignType={alignType}
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={quizMainBlock}
backgroundBlock={backgroundBlock}
/>
);
}
default:
notReachable(startpageType);
}
default:
notReachable(startpageType);
}
};

@ -4,9 +4,9 @@ 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";
import { DESIGN_LIST } from "@/utils/designList";
type StartPageMobileProps = {
quizHeaderBlock: JSX.Element;

@ -1,11 +1,11 @@
import {
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useTheme,
Box,
Button,
ButtonBase,
Link,
Paper,
Typography,
useTheme,
} from "@mui/material";
import { QuizPreviewLayoutByType } from "./QuizPreviewLayoutByType";
@ -13,365 +13,366 @@ 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 { useQuizViewStore } from "@/stores/quizView";
import { DESIGN_LIST } from "@/utils/designList";
export const StartPageViewPublication = () => {
const theme = useTheme();
const { settings, show_badge, quizId } = useQuizData();
const { isMobileDevice } = useUADevice();
const isMobile = useRootContainerSize() < 700;
const isTablet = useRootContainerSize() < 800;
const theme = useTheme();
const { settings, show_badge, quizId } = useQuizData();
const { isMobileDevice } = useUADevice();
const setCurrentQuizStep = useQuizViewStore(state => state.setCurrentQuizStep);
const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
};
const isMobile = useRootContainerSize() < 700;
const isTablet = useRootContainerSize() < 800;
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: "100%",
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;
const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
};
return (
<Paper
className="settings-preview-draghandle"
sx={{
borderRadius: 0,
height: "100%",
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",
}}
>
{settings.cfg.startpage.logo &&
<img
src={settings.cfg.startpage.logo}
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"
const background =
settings.cfg.startpage.background.type === "image" ? (
<img
src={
settings.cfg.startpage.background.desktop ||
DESIGN_LIST[settings.cfg.theme] ||
""
}
>
<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,
alt=""
style={{
display: "block",
width:
isMobile || settings.cfg.startpageType === "expanded"
? "100%"
: undefined,
height: "100%",
minWidth: "100%",
maxHeight: "100%",
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",
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,
"& iframe": {
width: "100%",
height: "100%",
transform:
settings.cfg.startpageType === "centered"
? ""
: settings.cfg.startpageType === "expanded"
? "scale(1.5)"
: "scale(2.4)",
},
}}
>
{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>
/>
) : null
) : null;
{show_badge && (
<Box
component={Link}
target={"_blank"}
href={
`https://${window.location.hostname.includes("s") ? "s" : ""}quiz.pena.digital/squiz/quiz/logo?q=${quizId}`
}
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
textDecoration: "none",
}}
>
<NameplateLogo
style={{
fontSize: "23px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: quizThemes[settings.cfg.theme].isLight
? "#151515"
: "#FFFFFF",
}}
/>
<Typography
sx={{
fontSize: "14px",
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>
);
return (
<Paper
className="settings-preview-draghandle"
sx={{
borderRadius: 0,
height: "100%",
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",
}}
>
{settings.cfg.startpage.logo &&
<img
src={settings.cfg.startpage.logo}
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://${window.location.hostname.includes("s") ? "s" : ""}quiz.pena.digital/squiz/quiz/logo?q=${quizId}`
}
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
textDecoration: "none",
}}
>
<NameplateLogo
style={{
fontSize: "23px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: quizThemes[settings.cfg.theme].isLight
? "#151515"
: "#FFFFFF",
}}
/>
<Typography
sx={{
fontSize: "14px",
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>
);
};

@ -16,7 +16,7 @@ import PrevButton from "./tools/PrevButton";
export default function ViewPublicationPage() {
const { settings, recentlyCompleted, quizId, preview } = useQuizData();
const { answers } = useQuizViewStore();
const answers = useQuizViewStore(state => state.answers);
let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep);
const {
currentQuestion,

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

@ -1,14 +1,14 @@
import {
Box,
FormControl,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
Box,
FormControl,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -25,195 +25,196 @@ polyfillCountryFlagEmojis();
import { useState } from "react";
type EmojiProps = {
currentQuestion: QuizQuestionEmoji;
currentQuestion: QuizQuestionEmoji;
};
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const { answers } = useQuizViewStore();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
onChange={({ target }) => {
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer,
currentQuestion.content.variants[Number(target.value)].points || 0
);
}}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={index}
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
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
"&:hover": {borderColor: theme.palette.primary.main},
}}
// 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,
preview
});
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,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
cursor: "pointer"
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
onChange={({ target }) => {
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer,
currentQuestion.content.variants[Number(target.value)].points || 0
);
}}
>
<Box
sx={{
width: "100%",
sx={{
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",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-162px",
right: "12px",
}}
/>
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography
sx={{
wordBreak: "break-word",
lineHeight: "normal",
}}
>
{variant.answer}
</Typography>
</Box>
}
/>
</FormControl>
))}
>
<Box
sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={index}
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
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
"&:hover": { borderColor: theme.palette.primary.main },
}}
// 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,
preview
});
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,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
cursor: "pointer"
}}
>
<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",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-162px",
right: "12px",
}}
/>
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography
sx={{
wordBreak: "break-word",
lineHeight: "normal",
}}
>
{variant.answer}
</Typography>
</Box>
}
/>
</FormControl>
))}
</Box>
</RadioGroup>
</Box>
</RadioGroup>
</Box>
);
);
};

@ -7,7 +7,7 @@ import {
Typography,
useTheme
} from "@mui/material";
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import CloseBold from "@icons/CloseBold";
import UploadIcon from "@icons/UploadIcon";
@ -29,7 +29,8 @@ type FileProps = {
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const { answers } = useQuizViewStore();
const answers = useQuizViewStore(state => state.answers);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { quizId, preview } = useQuizData();
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
const [isSending, setIsSending] = useState<boolean>(false);
@ -43,8 +44,8 @@ export const File = ({ currentQuestion }: FileProps) => {
const uploadFile = async (file: File | undefined) => {
if (isSending) return;
if (!file) return;
console.log(file.size)
console.log(MAX_FILE_SIZE)
console.log(file.size);
console.log(MAX_FILE_SIZE);
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some(

@ -1,13 +1,13 @@
import {
Box,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
Box,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -20,174 +20,176 @@ import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type ImagesProps = {
currentQuestion: QuizQuestionImages;
currentQuestion: QuizQuestionImages;
};
export const Images = ({ currentQuestion }: ImagesProps) => {
const { quizId, preview } = useQuizData();
const { answers } = useQuizViewStore();
const theme = useTheme();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer;
const { settings } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
const { quizId, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const theme = useTheme();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer;
const { settings } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
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: "12px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
"&:hover": {borderColor: theme.palette.primary.main},
background: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
}}
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,
preview
});
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,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
<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",
borderRadius: "12px 12px 0 0"
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === 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",
justifyContent: "center",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
>
<Box
sx={{
position: "absolute",
top: "-297px",
right: 0
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet
? isMobile
? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
/>
}
label={variant.answer}
/>
</Box>
))}
>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
"&:hover": { borderColor: theme.palette.primary.main },
background: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
}}
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,
preview
});
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,
preview
});
} 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",
borderRadius: "12px 12px 0 0"
}}
/>
)}
</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",
justifyContent: "center",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0
}}
/>
}
label={variant.answer}
/>
</Box>
))}
</Box>
</RadioGroup>
</Box>
</RadioGroup>
</Box>
);
);
};

@ -5,7 +5,7 @@ import { useDebouncedCallback } from "use-debounce";
import { CustomSlider } from "@ui_kit/CustomSlider";
import CustomTextField from "@ui_kit/CustomTextField";
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
@ -18,458 +18,460 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { ChangeEvent, SyntheticEvent } from "react";
type NumberProps = {
currentQuestion: QuizQuestionNumber;
currentQuestion: QuizQuestionNumber;
};
export const Number = ({ currentQuestion }: NumberProps) => {
const { settings, quizId, preview } = useQuizData();
const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000");
const [reversedInputValue, setReversedInputValue] = useState<string>("0");
const [reversedMinRange, setReversedMinRange] = useState<string>("0");
const [reversedMaxRange, setReversedMaxRange] =
useState<string>("100000000000");
const theme = useTheme();
const { answers } = useQuizViewStore();
const [isSending, setIsSending] = useState<boolean>(false);
const { settings, quizId, preview } = useQuizData();
const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000");
const [reversedInputValue, setReversedInputValue] = useState<string>("0");
const [reversedMinRange, setReversedMinRange] = useState<string>("0");
const [reversedMaxRange, setReversedMaxRange] =
useState<string>("100000000000");
const theme = useTheme();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const [isSending, setIsSending] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 650;
const [minBorder, maxBorder] = currentQuestion.content.range
.split("—")
.map(window.Number);
const min = minBorder < maxBorder ? minBorder : maxBorder;
const max = minBorder < maxBorder ? maxBorder : minBorder;
const reversed = minBorder > maxBorder;
const isMobile = useRootContainerSize() < 650;
const [minBorder, maxBorder] = currentQuestion.content.range
.split("—")
.map(window.Number);
const min = minBorder < maxBorder ? minBorder : maxBorder;
const max = minBorder < maxBorder ? maxBorder : minBorder;
const reversed = minBorder > maxBorder;
useEffect(() => {
console.log("reversed:", reversed);
}, [reversed]);
useEffect(() => {
console.log("reversed:", reversed);
}, [reversed]);
const sendAnswerToBackend = async (value: string, noUpdate = false) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: quizId,
preview
});
const sendAnswerToBackend = async (value: string, noUpdate = false) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: quizId,
preview
});
if (!noUpdate) {
updateAnswer(currentQuestion.id, value, 0);
}
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
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) {
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]);
if (!noUpdate) {
updateAnswer(currentQuestion.id, value, 0);
}
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
} else {
setIsSending(false);
};
const updateValueDebounced = useDebouncedCallback(async (value: string) => {
if (reversed) {
setReversedInputValue(String(max + min - window.Number(answer)));
} else {
setInputValue(answer);
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;
}
}
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
const newValue =
window.Number(value) < window.Number(minRange)
? minRange
: window.Number(value) > window.Number(maxRange)
? maxRange
: value;
if (currentQuestion.content.chooseRange) {
setReversedMinRange(String(currentQuestion.content.start));
setReversedMaxRange(String(min));
}
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]);
setReversedInputValue(String(currentQuestion.content.start));
setInputValue(String(currentQuestion.content.start));
}
}, []);
const newMinValue =
window.Number(value.split("—")[0]) > max
? String(max)
: value.split("—")[0];
const onSliderChange = (_: Event, value: number | number[]) => {
const range = Array.isArray(value)
? `${value[0]}${value[1]}`
: String(value);
setReversedMinRange(
crowded ? String(max + min - window.Number(newMinValue)) : newMinValue
);
updateAnswer(
currentQuestion.id,
`${newMinRange}${value.split("—")[1]}`,
0
);
await sendAnswerToBackend(
`${newMinValue}${value.split("—")[1]}`,
true
);
updateAnswer(currentQuestion.id, range, 0);
};
return;
}
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]);
const newMinValue = crowded
? maxRange
: window.Number(value.split("—")[0]) < min
? String(min)
: value.split("—")[0];
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
setReversedMinRange(newMinReversedValue);
setReversedMaxRange(newMaxReversedValue);
await sendAnswerToBackend(
`${newMinReversedValue}${newMaxReversedValue}`,
true
);
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]);
return;
}
const newMaxValue =
window.Number(value.split("—")[1]) < min
? String(min)
: value.split("—")[1];
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[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;
}
return;
}
if (reversed) {
setReversedInputValue(String(max + min - window.Number(value)));
} else {
setInputValue(String(value));
}
const newMaxValue = crowded
? minRange
: window.Number(value.split("—")[1]) > max
? String(max)
: value.split("—")[1];
await sendAnswerToBackend(String(value));
};
setMaxRange(newMaxValue);
await sendAnswerToBackend(`${value.split("—")[0]}${newMaxValue}`);
},
1000
);
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const changeValueLabelFormat = (value: number) => {
if (!reversed) {
return value;
}
const sliderValue =
answer ||
(reversed
? max + min - currentQuestion.content.start + "—" + max
: currentQuestion.content.start + "—" + max);
const [minSliderBorder, maxSliderBorder] = sliderValue
.split("—")
.map(window.Number);
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 (value === minSliderBorder) {
return max + min - minSliderBorder;
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
return max + min - maxSliderBorder;
};
if (currentQuestion.content.chooseRange) {
setReversedMinRange(String(currentQuestion.content.start));
setReversedMaxRange(String(min));
}
const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = target.value.replace(/\D/g, "");
setReversedInputValue(String(currentQuestion.content.start));
setInputValue(String(currentQuestion.content.start));
}
}, []);
if (reversed) {
setReversedInputValue(value);
} else {
setInputValue(value);
}
const onSliderChange = (_: Event, value: number | number[]) => {
const range = Array.isArray(value)
? `${value[0]}${value[1]}`
: String(value);
updateValueDebounced(value);
};
updateAnswer(currentQuestion.id, range, 0);
};
const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
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]);
if (reversed) {
setReversedMinRange(newValue);
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
setReversedMinRange(newMinReversedValue);
setReversedMaxRange(newMaxReversedValue);
await sendAnswerToBackend(
`${newMinReversedValue}${newMaxReversedValue}`,
true
);
if (window.Number(newValue) <= window.Number(reversedMaxRange)) {
const value = max + min - window.Number(reversedMaxRange);
updateMinRangeDebounced(`${value}${value}`, true);
return;
}
return;
}
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
updateMinRangeDebounced(
`${newValue}${max + min - window.Number(reversedMaxRange)}`
);
return;
}
return;
}
if (reversed) {
setReversedInputValue(String(max + min - window.Number(value)));
} else {
setInputValue(String(value));
}
setMinRange(newValue);
await sendAnswerToBackend(String(value));
};
if (window.Number(newValue) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
const changeValueLabelFormat = (value: number) => {
if (!reversed) {
return value;
}
return;
}
const [minSliderBorder, maxSliderBorder] = sliderValue
.split("—")
.map(window.Number);
updateMinRangeDebounced(`${newValue}${maxRange}`);
};
if (value === minSliderBorder) {
return max + min - minSliderBorder;
}
const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
return max + min - maxSliderBorder;
};
if (reversed) {
setReversedMaxRange(newValue);
const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = target.value.replace(/\D/g, "");
if (window.Number(newValue) >= window.Number(reversedMinRange)) {
const value = max + min - window.Number(reversedMinRange);
updateMaxRangeDebounced(`${value}${value}`, true);
if (reversed) {
setReversedInputValue(value);
} else {
setInputValue(value);
}
return;
}
updateValueDebounced(value);
};
updateMaxRangeDebounced(
`${max + min - window.Number(reversedMinRange)}${newValue}`
);
const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
return;
}
if (reversed) {
setReversedMinRange(newValue);
setMaxRange(newValue);
if (window.Number(newValue) <= window.Number(reversedMaxRange)) {
const value = max + min - window.Number(reversedMaxRange);
updateMinRangeDebounced(`${value}${value}`, true);
if (window.Number(newValue) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return;
}
return;
}
updateMinRangeDebounced(
`${newValue}${max + min - window.Number(reversedMaxRange)}`
);
updateMaxRangeDebounced(`${minRange}${newValue}`);
};
return;
}
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",
padding: "0 30px",
}}
>
<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,
borderRadius: "8px",
minWidth: "60px",
height: "36px"
},
}}
/>
setMinRange(newValue);
{!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"
},
}}
/>
)}
if (window.Number(newValue) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
{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>
);
return;
}
updateMinRangeDebounced(`${newValue}${maxRange}`);
};
const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
if (reversed) {
setReversedMaxRange(newValue);
if (window.Number(newValue) >= window.Number(reversedMinRange)) {
const value = max + min - window.Number(reversedMinRange);
updateMaxRangeDebounced(`${value}${value}`, true);
return;
}
updateMaxRangeDebounced(
`${max + min - window.Number(reversedMinRange)}${newValue}`
);
return;
}
setMaxRange(newValue);
if (window.Number(newValue) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true);
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",
padding: "0 30px",
}}
>
<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,
borderRadius: "8px",
minWidth: "60px",
height: "36px"
},
}}
/>
{!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>
);
};

@ -5,7 +5,7 @@ import {
useTheme
} from "@mui/material";
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
@ -59,7 +59,8 @@ const buttonRatingForm = [
export const Rating = ({ currentQuestion }: RatingProps) => {
const { quizId, preview } = useQuizData();
const { answers } = useQuizViewStore();
const answers = useQuizViewStore(state => state.answers);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 750;
@ -117,7 +118,7 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
height: "50px",
gap: isMobile ? undefined : "15px",
opacity: "1!important",
"& .MuiRating-root.Mui-disabled": {opacity: "1!important"}
"& .MuiRating-root.Mui-disabled": { opacity: "1!important" }
}}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.primary.main, isMobile ? 30 : isTablet ? 40 : 50)}

@ -2,7 +2,7 @@ import { Box, Typography, useTheme } from "@mui/material";
import { Select as SelectComponent } from "../tools//Select";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
@ -12,86 +12,88 @@ import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type SelectProps = {
currentQuestion: QuizQuestionSelect;
currentQuestion: QuizQuestionSelect;
};
export const Select = ({ currentQuestion }: SelectProps) => {
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const { answers } = useQuizViewStore();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
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",
}}
>
<SelectComponent
disabled={isSending}
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
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(255,255,255, 0.3)"
: "transparent",
},
}}
onChange={async (_, value) => {
setIsSending(true);
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
return setIsSending(false);
}
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",
}}
>
<SelectComponent
disabled={isSending}
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
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(255,255,255, 0.3)"
: "transparent",
},
}}
onChange={async (_, value) => {
setIsSending(true);
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
return setIsSending(false);
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: String(
currentQuestion.content.variants[Number(value)].answer
),
qid: quizId,
preview
});
try {
await sendAnswer({
questionId: currentQuestion.id,
body: String(
currentQuestion.content.variants[Number(value)].answer
),
qid: quizId,
preview
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
updateAnswer(currentQuestion.id, String(value), 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}}
/>
</Box>
</Box>
);
setIsSending(false);
}}
/>
</Box>
</Box>
);
};

@ -1,14 +1,14 @@
import {
Box,
TextField as MuiTextField,
TextFieldProps,
Typography,
useTheme,
Box,
TextField as MuiTextField,
TextFieldProps,
Typography,
useTheme,
} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import { updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
@ -22,265 +22,265 @@ import type { QuizQuestionText } from "../../../model/questionTypes/text";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
type TextProps = {
currentQuestion: QuizQuestionText;
stepNumber: number | null;
currentQuestion: QuizQuestionText;
stepNumber: number | null;
};
const Orientation = [
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ 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: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
];
export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const theme = useTheme();
const { settings, preview } = useQuizData();
const spec = settings.cfg.spec;
const { quizId } = useQuizData();
const { answers } = useQuizViewStore();
const isMobile = useRootContainerSize() < 650;
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
const { settings, preview } = useQuizData();
const spec = settings.cfg.spec;
const { quizId } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
const inputHC = useDebouncedCallback(async (text) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: text,
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
const inputHC = useDebouncedCallback(async (text) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: text,
qid: quizId,
preview
});
} catch (e) {
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 {
currentQuestion: QuizQuestionText;
answer: any;
inputHC: (a: string) => void;
stepNumber?: number | null;
currentQuestion: QuizQuestionText;
answer: any;
inputHC: (a: string) => void;
stepNumber?: number | null;
}
const TextNormal = ({ currentQuestion, answer, inputHC }: Props) => {
const isMobile = useRootContainerSize() < 650;
const theme = useTheme();
const { settings } = useQuizData();
const isMobile = useRootContainerSize() < 650;
const theme = useTheme();
const { settings } = useQuizData();
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
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(255,255,255, 0.3)"
: "transparent",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF"
},
"&:focus-visible": { borderColor: theme.palette.primary.main },
}}
/>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box
sx={{
maxWidth: "400px",
width: "100%",
height: "300px",
margin: "15px",
}}
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
{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(255,255,255, 0.3)"
: "transparent",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF"
},
"&: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>
);
};
const TextSpecial = ({
currentQuestion,
answer,
inputHC,
stepNumber,
currentQuestion,
answer,
inputHC,
stepNumber,
}: Props) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isHorizontal = Orientation[Number(stepNumber) - 1].horizontal;
const { settings } = useQuizData();
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isHorizontal = Orientation[Number(stepNumber) - 1].horizontal;
const { settings } = useQuizData();
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
return (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
gap: "20px",
}}
>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{isHorizontal &&
currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
{
<TextField
autoFocus={true}
multiline
maxRows={4}
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}}
inputProps={{
maxLength: 400,
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
return (
<Box
sx={{
width: "100%",
"& .MuiOutlinedInput-root": {
backgroundColor: settings.cfg.design
? "rgba(154,154,175, 0.2)"
: "#FFFFFF",
},
"&:focus-visible": {
borderColor: theme.palette.primary.main,
},
display: "flex",
flexDirection: isMobile ? "column" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
/>
}
</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>
);
>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
gap: "20px",
}}
>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{isHorizontal &&
currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
{
<TextField
autoFocus={true}
multiline
maxRows={4}
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
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,19 @@
import {
Box,
Checkbox,
FormControlLabel,
FormGroup,
TextField as MuiTextField,
Radio,
RadioGroup,
TextFieldProps,
Typography,
useTheme,
Box,
Checkbox,
FormControlLabel,
FormGroup,
TextField as MuiTextField,
Radio,
RadioGroup,
TextFieldProps,
Typography,
useTheme,
} from "@mui/material";
import { FC, useEffect, useState } from "react";
import {
deleteAnswer,
updateAnswer,
updateOwnVariant,
useQuizViewStore,
useQuizViewStore,
} from "@stores/quizView";
import { CheckboxIcon } from "@icons/Checkbox";
@ -36,270 +33,275 @@ import moment from "moment";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
type VariantProps = {
currentQuestion: QuizQuestionVariant;
currentQuestion: QuizQuestionVariant;
};
export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const { answers, ownVariants } = useQuizViewStore();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find(
(variant) => variant.id === currentQuestion.id
);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const answers = useQuizViewStore(state => state.answers);
const ownVariants = useQuizViewStore(state => state.ownVariants);
const updateOwnVariant = useQuizViewStore(state => state.updateOwnVariant);
const [isSending, setIsSending] = useState(false);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find(
(variant) => variant.id === currentQuestion.id
);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
const [isSending, setIsSending] = useState(false);
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
}, []);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
if (moment.isMoment(answer))
throw new Error("Answer is Moment in Variant question");
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
}, []);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
gap: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Group
name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
width: "100%",
gap: "20px",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VariantItem
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
answer={answer}
index={index}
isSending={isSending}
setIsSending={setIsSending}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
isSending={isSending}
setIsSending={setIsSending}
/>
)}
</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=""
/>
if (moment.isMoment(answer))
throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
gap: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Group
name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
width: "100%",
gap: "20px",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VariantItem
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
answer={answer}
index={index}
isSending={isSending}
setIsSending={setIsSending}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
isSending={isSending}
setIsSending={setIsSending}
/>
)}
</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 = ({
currentQuestion,
variant,
answer,
index,
own = false,
isSending,
setIsSending,
currentQuestion,
variant,
answer,
index,
own = false,
isSending,
setIsSending,
}: {
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
isSending: boolean;
setIsSending: (a: boolean) => void;
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
isSending: boolean;
setIsSending: (a: boolean) => void;
}) => {
const theme = useTheme();
const { settings, quizId, preview } = useQuizData();
const theme = useTheme();
const { settings, quizId, preview } = useQuizData();
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
return (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
margin: "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&:hover": {borderColor: theme.palette.primary.main},
"&.MuiFormControl-root": {
width: "100%",
},
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={
<CheckboxIcon checked color={theme.palette.primary.main} />
return (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
margin: "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&:hover": { borderColor: theme.palette.primary.main },
"&.MuiFormControl-root": {
width: "100%",
},
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={
<CheckboxIcon checked color={theme.palette.primary.main} />
}
icon={<CheckboxIcon />}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
)
}
icon={<CheckboxIcon />}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
const variantId = currentQuestion.content.variants[index].id;
console.log(answer);
setIsSending(true);
const variantId = currentQuestion.content.variants[index].id;
console.log(answer);
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
qid: quizId,
preview
});
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
qid: quizId,
preview
});
updateAnswer(
currentQuestion.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
updateAnswer(
currentQuestion.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
return;
}
setIsSending(false);
return;
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
qid: quizId,
preview
});
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
qid: quizId,
preview
});
updateAnswer(
currentQuestion.id,
variantId,
answer === variantId
? 0
: currentQuestion.content.variants[index].points || 0
);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
updateAnswer(
currentQuestion.id,
variantId,
answer === variantId
? 0
: currentQuestion.content.variants[index].points || 0
);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
if (answer === variantId) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
if (answer === variantId) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
}}
/>
);
setIsSending(false);
}}
/>
);
};

@ -1,12 +1,12 @@
import {
Box,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
Box,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -21,202 +21,205 @@ import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { useState } from "react";
type VarimgProps = {
currentQuestion: QuizQuestionVarImg;
currentQuestion: QuizQuestionVarImg;
};
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { settings, quizId, preview } = useQuizData();
const { answers } = useQuizViewStore();
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const [isSending, setIsSending] = useState<boolean>(false);
const { settings, quizId, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const variant = currentQuestion.content.variants.find(
({ id }) => answer === id
);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const [isSending, setIsSending] = useState<boolean>(false);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: "30px",
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: "20px",
"&:focus": {color: theme.palette.text.primary },
"&:active": {color: theme.palette.text.primary }
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
disabled={isSending}
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const variant = currentQuestion.content.variants.find(
({ id }) => answer === id
);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: 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: 0,
"&:hover": {borderColor: theme.palette.primary.main},
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: "30px",
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: "20px",
"&:focus": { color: theme.palette.text.primary },
"&:active": { color: theme.palette.text.primary }
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: 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: 0,
"&:hover": { borderColor: theme.palette.primary.main },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
}}
labelPlacement="start"
value={index}
onClick={async (event) => {
event.preventDefault();
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,
preview
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
}}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={variant.answer}
/>
))}
</Box>
</RadioGroup>
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
<Box
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF30",
color: theme.palette.text.primary,
}
}}
labelPlacement="start"
value={index}
onClick={async (event) => {
event.preventDefault();
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,
preview
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
}}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={variant.answer}
/>
))}
</Box>
</RadioGroup>
{/* {(variant?.extendedText || currentQuestion.content.back) && ( */}
<Box
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF30",
color: theme.palette.text.primary,
textAlign: "center",
}}
>
{answer ? (
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 ? (
"Выберите вариант ответа ниже"
) : (
"Выберите вариант ответа слева"
)}
textAlign: "center",
}}
>
{answer ? (
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>
);
);
};

@ -1,123 +1,102 @@
import { QuestionVariant } from "@model/questionTypes/shared";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import type { Moment } from "moment";
import { QuizStep } from "@model/settingsData";
type QuestionAnswer = {
questionId: string;
answer: string | string[] | Moment;
};
type OwnVariant = {
id: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: QuestionAnswer[];
ownVariants: OwnVariant[];
pointsSum: number;
points: Record<string, number>;
currentQuizStep: QuizStep;
}
export const useQuizViewStore = create<QuizViewStore>()(
devtools(
(set, get) => ({
answers: [],
ownVariants: [],
points: {},
pointsSum: 0,
currentQuizStep: "startpage",
}),
{
name: "quizView",
enabled: import.meta.env.DEV,
trace: import.meta.env.DEV,
}
)
);
function setProducedState<A extends string | { type: string; }>(
recipe: (state: QuizViewStore) => void,
action?: A,
) {
useQuizViewStore.setState(state => produce(state, recipe), false, action);
}
const calcPoints = () => {
const storePoints = useQuizViewStore.getState().points;
let sum = Object.values(storePoints).reduce((accumulator, currentValue) => accumulator + currentValue)
console.log("сумма ", sum)
useQuizViewStore.setState({ pointsSum: sum })
}
export const updateAnswer = (
questionId: string,
answer: string | string[] | Moment,
points: number
) => {
setProducedState(state => {
const index = state.answers.findIndex(answer => questionId === answer.questionId);
if (index < 0) {
state.answers.push({ questionId, answer });
} else {
state.answers[index] = { questionId, answer };
}
}, {
type: "updateAnswer",
questionId,
answer
})
const storePoints = useQuizViewStore.getState().points;
useQuizViewStore.setState({ points: { ...storePoints, ...{ [questionId]: points } } })
calcPoints()
};
export const deleteAnswer = (questionId: string) => useQuizViewStore.setState(state => ({
answers: state.answers.filter(answer => questionId !== answer.questionId)
}), false, {
type: "deleteAnswer",
questionId
});
export const updateOwnVariant = (id: string, answer: string) => setProducedState(state => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: nanoid(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
state.ownVariants[index].variant.answer = answer;
}
}, {
type: "updateOwnVariant",
id,
answer
});
export const deleteOwnVariant = (id: string) => useQuizViewStore.setState(state => ({
ownVariants: state.ownVariants.filter((variant) => variant.id !== id)
}), false, {
type: "deleteOwnVariant",
id
});
export const setCurrentQuizStep = (currentQuizStep: QuizStep) => useQuizViewStore.setState({
currentQuizStep
}, false, {
type: "setCurrentQuizStep",
currentQuizStep
});
import { QuestionVariant } from "@model/questionTypes/shared";
import { QuizStep } from "@model/settingsData";
import type { Moment } from "moment";
import { nanoid } from "nanoid";
import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand";
import { immer } from "zustand/middleware/immer";
type QuestionAnswer = {
questionId: string;
answer: string | string[] | Moment;
};
type OwnVariant = {
id: string;
variant: QuestionVariant;
};
interface QuizViewStore {
answers: QuestionAnswer[];
ownVariants: OwnVariant[];
pointsSum: number;
points: Record<string, number>;
currentQuizStep: QuizStep;
}
interface QuizViewActions {
updateAnswer: (questionId: string, answer: string | string[] | Moment, points: number) => void;
deleteAnswer: (questionId: string) => void;
updateOwnVariant: (id: string, answer: string) => void;
deleteOwnVariant: (id: string) => void;
setCurrentQuizStep: (step: QuizStep) => void;
}
export const QuizViewContext = createContext<ReturnType<typeof createQuizViewStore> | null>(null);
export function useQuizViewStore<U>(selector: (state: QuizViewStore & QuizViewActions) => U): U {
const context = useContext(QuizViewContext);
if (!context) throw new Error("QuizViewStore context is null");
return useStore(context, selector);
}
export const createQuizViewStore = () => createStore<QuizViewStore & QuizViewActions>()(
immer(
(set, get) => ({
answers: [],
ownVariants: [],
points: {},
pointsSum: 0,
currentQuizStep: "startpage",
updateAnswer(questionId, answer, points) {
set(state => {
const index = state.answers.findIndex(answer => questionId === answer.questionId);
if (index < 0) {
state.answers.push({ questionId, answer });
} else {
state.answers[index] = { questionId, answer };
}
state.points = { ...state.points, ...{ [questionId]: points } };
state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value);
});
},
deleteAnswer(questionId) {
set(state => {
state.answers = state.answers.filter(answer => questionId !== answer.questionId);
});
},
updateOwnVariant(id, answer) {
set(state => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: nanoid(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
state.ownVariants[index].variant.answer = answer;
}
});
},
deleteOwnVariant(id) {
set(state => {
state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id);
});
},
setCurrentQuizStep(step) {
set({ currentQuizStep: step });
},
})
)
);

36
lib/utils/designList.ts Normal file

@ -0,0 +1,36 @@
import type { QuizTheme } from "@model/settingsData";
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";
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: "",
};

@ -1,205 +1,206 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { setCurrentQuizStep, useQuizViewStore } from "@stores/quizView";
import { useCallback, useDebugValue, useMemo, useState } from "react";
import { isResultQuestionEmpty } from "../../components/ViewPublicationPage/tools/checkEmptyData";
import moment from "moment";
import { useQuizData } from "@contexts/QuizDataContext";
import { enqueueSnackbar } from "notistack";
export function useQuestionFlowControl() {
const { settings, questions } = useQuizData();
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>(getFirstQuestion);
const { answers, pointsSum } = useQuizViewStore(state => state);
const linearQuestionIndex = questions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled
? questions.indexOf(currentQuestion)
: null;
function getFirstQuestion() {
if (questions.length === 0) throw new Error("No questions found");
if (settings.cfg.haveRoot) {
const nextQuestion = questions.find(
question => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot
);
if (!nextQuestion) throw new Error("Root question not found");
return nextQuestion;
}
return questions[0];
}
const nextQuestionIdPointsLogic = () => {
return questions.find(question =>
question.type === "result" && question.content.rule.parentId === "line"
);
};
const nextQuestionIdMainLogic = () => {
const questionAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
//Если ответ существует и не является объектом момента
if (questionAnswer && !moment.isMoment(questionAnswer.answer)) {
//Ответы должны храниться в массиве
const userAnswers = Array.isArray(questionAnswer.answer) ? questionAnswer.answer : [questionAnswer.answer];
//Сравниваем список условий ветвления и выбираем подходящее
for (const branchingRule of currentQuestion.content.rule.main) {
if (userAnswers.some(answer => branchingRule.rules[0].answers.includes(answer))) {
return branchingRule.next;
}
}
}
if (!currentQuestion.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
const defaultNextQuestionId = currentQuestion.content.rule.default;
if (defaultNextQuestionId.length > 1 && defaultNextQuestionId !== " ") return defaultNextQuestionId;
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
["date", "page", "text", "number"].includes(currentQuestion.type)
&& currentQuestion.content.rule.children.length === 1
) return currentQuestion.content.rule.children[0];
}
//ничё не нашли, ищем резулт
return questions.find(q => {
return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id;
})?.id;
};
const calculateNextQuestionId = useMemo(() => {
if (Boolean(settings.cfg.score)) {
return nextQuestionIdPointsLogic();
}
return nextQuestionIdMainLogic();
}, [answers, currentQuestion, questions]);
const prevQuestion = linearQuestionIndex !== null
? questions[linearQuestionIndex - 1]
: questions.find(q =>
q.id === currentQuestion.content.rule.parentId
|| q.content.id === currentQuestion.content.rule.parentId
);
const findResultPointsLogic = () => {
const results = questions
.filter(e => e.type === "result" && e.content.rule.minScore !== undefined && e.content.rule.minScore <= pointsSum);
const numbers = results.map(e => e.type === "result" && e.content.rule.minScore !== undefined ? e.content.rule.minScore : 0);
const indexOfNext = Math.max(...numbers);
console.log(results);
console.log(numbers);
console.log(indexOfNext);
return results[numbers.indexOf(indexOfNext)];
};
const getNextQuestion = () => {
let next;
console.log(11111111111);
//Искать можно двумя логиками. Основной и балловой
if (Boolean(settings.cfg.score)) {
//Балловая
console.log(222222222);
//Ищем линейно
if (linearQuestionIndex !== null) {
console.log(33333333333);
next = questions[linearQuestionIndex + 1];
console.log(4444444);
console.log("перед ифом", next);
if (next?.type === "result" || next == undefined) next = findResultPointsLogic();
console.log(5555555555);
}
} else {
console.log(6666666);
//Основная
if (linearQuestionIndex !== null) {
console.log(777777777);
//Ищем линейно
next = questions[linearQuestionIndex + 1] ?? questions.find(question =>
question.type === "result" && question.content.rule.parentId === "line"
);
} else {
console.log(88888888888888);
//Ищем ветвлением
next = questions.find(q => q.id === calculateNextQuestionId || q.content.id === calculateNextQuestionId);
}
}
console.log("next", next);
if (!next && currentQuestion.type !== "result") throw new Error("Не найден следующий вопрос");
return next;
};
const nextQuestion = getNextQuestion();
const showResult = useCallback(() => {
if (nextQuestion?.type !== "result") throw new Error("Current question is not result");
setCurrentQuestion(nextQuestion);
if (
settings.cfg.resultInfo.showResultForm === "after"
|| isResultQuestionEmpty(nextQuestion)
) setCurrentQuizStep("contactform");
}, [nextQuestion, settings.cfg.resultInfo.showResultForm]);
const showResultAfterContactForm = useCallback(() => {
if (currentQuestion.type !== "result") throw new Error("Current question is not result");
if (isResultQuestionEmpty(currentQuestion)) {
enqueueSnackbar("Данные отправлены");
return;
}
setCurrentQuizStep("question");
}, [currentQuestion]);
const moveToPrevQuestion = useCallback(() => {
if (!prevQuestion) throw new Error("Previous question not found");
setCurrentQuestion(prevQuestion);
}, [prevQuestion]);
const moveToNextQuestion = useCallback(() => {
if (!nextQuestion) throw new Error("Next question not found");
if (nextQuestion.type === "result") return showResult();
setCurrentQuestion(nextQuestion);
}, [nextQuestion, showResult]);
const isPreviousButtonEnabled = Boolean(prevQuestion);
const isNextButtonEnabled = useMemo(() => {
const hasAnswer = answers.some(({ questionId }) => questionId === currentQuestion.id);
if ("required" in currentQuestion.content && currentQuestion.content.required) {
return hasAnswer;
}
return Boolean(nextQuestion);
}, [answers, currentQuestion.content, currentQuestion.id, nextQuestion]);
useDebugValue({
linearQuestionIndex,
currentQuestion: currentQuestion,
prevQuestion: prevQuestion,
nextQuestion: nextQuestion,
});
return {
currentQuestion,
currentQuestionStepNumber: linearQuestionIndex === null ? null : linearQuestionIndex + 1,
isNextButtonEnabled,
isPreviousButtonEnabled,
moveToPrevQuestion,
moveToNextQuestion,
showResultAfterContactForm,
};
}
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useQuizViewStore } from "@stores/quizView";
import { useCallback, useDebugValue, useMemo, useState } from "react";
import { isResultQuestionEmpty } from "../../components/ViewPublicationPage/tools/checkEmptyData";
import moment from "moment";
import { useQuizData } from "@contexts/QuizDataContext";
import { enqueueSnackbar } from "notistack";
export function useQuestionFlowControl() {
const { settings, questions } = useQuizData();
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>(getFirstQuestion);
const answers = useQuizViewStore(state => state.answers);
const pointsSum = useQuizViewStore(state => state.pointsSum);
const setCurrentQuizStep = useQuizViewStore(state => state.setCurrentQuizStep);
const linearQuestionIndex = questions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled
? questions.indexOf(currentQuestion)
: null;
function getFirstQuestion() {
if (questions.length === 0) throw new Error("No questions found");
if (settings.cfg.haveRoot) {
const nextQuestion = questions.find(
question => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot
);
if (!nextQuestion) throw new Error("Root question not found");
return nextQuestion;
}
return questions[0];
}
const nextQuestionIdPointsLogic = () => {
return questions.find(question =>
question.type === "result" && question.content.rule.parentId === "line"
);
};
const nextQuestionIdMainLogic = () => {
const questionAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
//Если ответ существует и не является объектом момента
if (questionAnswer && !moment.isMoment(questionAnswer.answer)) {
//Ответы должны храниться в массиве
const userAnswers = Array.isArray(questionAnswer.answer) ? questionAnswer.answer : [questionAnswer.answer];
//Сравниваем список условий ветвления и выбираем подходящее
for (const branchingRule of currentQuestion.content.rule.main) {
if (userAnswers.some(answer => branchingRule.rules[0].answers.includes(answer))) {
return branchingRule.next;
}
}
}
if (!currentQuestion.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
const defaultNextQuestionId = currentQuestion.content.rule.default;
if (defaultNextQuestionId.length > 1 && defaultNextQuestionId !== " ") return defaultNextQuestionId;
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
["date", "page", "text", "number"].includes(currentQuestion.type)
&& currentQuestion.content.rule.children.length === 1
) return currentQuestion.content.rule.children[0];
}
//ничё не нашли, ищем резулт
return questions.find(q => {
return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id;
})?.id;
};
const calculateNextQuestionId = useMemo(() => {
if (settings.cfg.score) {
return nextQuestionIdPointsLogic();
}
return nextQuestionIdMainLogic();
}, [answers, currentQuestion, questions]);
const prevQuestion = linearQuestionIndex !== null
? questions[linearQuestionIndex - 1]
: questions.find(q =>
q.id === currentQuestion.content.rule.parentId
|| q.content.id === currentQuestion.content.rule.parentId
);
const findResultPointsLogic = () => {
const results = questions
.filter(e => e.type === "result" && e.content.rule.minScore !== undefined && e.content.rule.minScore <= pointsSum);
const numbers = results.map(e => e.type === "result" && e.content.rule.minScore !== undefined ? e.content.rule.minScore : 0);
const indexOfNext = Math.max(...numbers);
console.log(results);
console.log(numbers);
console.log(indexOfNext);
return results[numbers.indexOf(indexOfNext)];
};
const getNextQuestion = () => {
let next;
console.log(11111111111);
//Искать можно двумя логиками. Основной и балловой
if (settings.cfg.score) {
//Балловая
console.log(222222222);
//Ищем линейно
if (linearQuestionIndex !== null) {
console.log(33333333333);
next = questions[linearQuestionIndex + 1];
console.log(4444444);
console.log("перед ифом", next);
if (next?.type === "result" || next == undefined) next = findResultPointsLogic();
console.log(5555555555);
}
} else {
console.log(6666666);
//Основная
if (linearQuestionIndex !== null) {
console.log(777777777);
//Ищем линейно
next = questions[linearQuestionIndex + 1] ?? questions.find(question =>
question.type === "result" && question.content.rule.parentId === "line"
);
} else {
console.log(88888888888888);
//Ищем ветвлением
next = questions.find(q => q.id === calculateNextQuestionId || q.content.id === calculateNextQuestionId);
}
}
console.log("next", next);
if (!next && currentQuestion.type !== "result") throw new Error("Не найден следующий вопрос");
return next;
};
const nextQuestion = getNextQuestion();
const showResult = useCallback(() => {
if (nextQuestion?.type !== "result") throw new Error("Current question is not result");
setCurrentQuestion(nextQuestion);
if (
settings.cfg.resultInfo.showResultForm === "after"
|| isResultQuestionEmpty(nextQuestion)
) setCurrentQuizStep("contactform");
}, [nextQuestion, settings.cfg.resultInfo.showResultForm]);
const showResultAfterContactForm = useCallback(() => {
if (currentQuestion.type !== "result") throw new Error("Current question is not result");
if (isResultQuestionEmpty(currentQuestion)) {
enqueueSnackbar("Данные отправлены");
return;
}
setCurrentQuizStep("question");
}, [currentQuestion]);
const moveToPrevQuestion = useCallback(() => {
if (!prevQuestion) throw new Error("Previous question not found");
setCurrentQuestion(prevQuestion);
}, [prevQuestion]);
const moveToNextQuestion = useCallback(() => {
if (!nextQuestion) throw new Error("Next question not found");
if (nextQuestion.type === "result") return showResult();
setCurrentQuestion(nextQuestion);
}, [nextQuestion, showResult]);
const isPreviousButtonEnabled = Boolean(prevQuestion);
const isNextButtonEnabled = useMemo(() => {
const hasAnswer = answers.some(({ questionId }) => questionId === currentQuestion.id);
if ("required" in currentQuestion.content && currentQuestion.content.required) {
return hasAnswer;
}
return Boolean(nextQuestion);
}, [answers, currentQuestion.content, currentQuestion.id, nextQuestion]);
useDebugValue({
linearQuestionIndex,
currentQuestion: currentQuestion,
prevQuestion: prevQuestion,
nextQuestion: nextQuestion,
});
return {
currentQuestion,
currentQuestionStepNumber: linearQuestionIndex === null ? null : linearQuestionIndex + 1,
isNextButtonEnabled,
isPreviousButtonEnabled,
moveToPrevQuestion,
moveToNextQuestion,
showResultAfterContactForm,
};
}

@ -1,6 +1,6 @@
{
"name": "@frontend/squzanswerer",
"version": "1.0.16",
"version": "1.0.17",
"type": "module",
"main": "./dist-package/index.js",
"module": "./dist-package/index.js",