fix some types

remove some ts-ignore's
refactor stores
This commit is contained in:
nflnkr 2024-01-30 19:49:33 +03:00
parent 606472d182
commit af1264a170
25 changed files with 2061 additions and 2122 deletions

@ -1,7 +1,7 @@
import { GetQuizDataResponse } from "@model/api/getQuizData";
import axios from "axios"; import axios from "axios";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import type { GetDataResponse } from "../model/settingsData";
let SESSIONS = ""; let SESSIONS = "";
@ -19,7 +19,7 @@ export const publicationMakeRequest = ({ url, body }: any) => {
}; };
export async function getData(quizId: string): Promise<{ export async function getData(quizId: string): Promise<{
data: GetDataResponse | null; data: GetQuizDataResponse | null;
isRecentlyCompleted: boolean; isRecentlyCompleted: boolean;
error?: string; error?: string;
}> { }> {
@ -29,7 +29,7 @@ export async function getData(quizId: string): Promise<{
: "ef836ff8-35b1-4031-9acf-af5766bac2b2"; : "ef836ff8-35b1-4031-9acf-af5766bac2b2";
try { try {
const { data, headers } = await axios<GetDataResponse>( const { data, headers } = await axios<GetQuizDataResponse>(
`https://s.hbpn.link/answer/settings`, `https://s.hbpn.link/answer/settings`,
{ {
method: "POST", method: "POST",

@ -0,0 +1,55 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { QuizSettings } from "@model/settingsData";
export interface GetQuizDataResponse {
cnt: number;
settings: {
fp: boolean;
rep: boolean;
name: string;
cfg: string;
lim: number;
due: number;
delay: number;
pausable: boolean;
};
items: {
id: number;
title: string;
desc: string;
typ: string;
req: boolean;
p: number;
c: string;
}[];
}
export function parseQuizData(quizDataResponse: GetQuizDataResponse, quizId: string): QuizSettings {
const items: QuizSettings["items"] = quizDataResponse.items.map((item) => {
const content = JSON.parse(item.c);
return {
description: item.desc,
id: item.id,
page: item.p,
required: item.req,
title: item.title,
type: item.typ,
content
} as unknown as AnyTypedQuizQuestion;
});
const settings: QuizSettings["settings"] = {
qid: quizId,
fp: quizDataResponse.settings.fp,
rep: quizDataResponse.settings.rep,
name: quizDataResponse.settings.name,
cfg: JSON.parse(quizDataResponse?.settings.cfg),
lim: quizDataResponse.settings.lim,
due: quizDataResponse.settings.due,
delay: quizDataResponse.settings.delay,
pausable: quizDataResponse.settings.pausable
};
return { cnt: quizDataResponse.cnt, settings, items };
}

@ -1,5 +1,4 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "./questionTypes/shared";
// import { QuizConfig } from "@model/quizSettings";
export type QuizStartpageType = "standard" | "expanded" | "centered" | null; export type QuizStartpageType = "standard" | "expanded" | "centered" | null;
@ -9,55 +8,50 @@ export type QuizType = "quiz" | "form";
export type QuizResultsType = true | null; export type QuizResultsType = true | null;
export type QuizTheme =
| "StandardTheme"
| "StandardDarkTheme"
| "PinkTheme"
| "PinkDarkTheme"
| "BlackWhiteTheme"
| "OliveTheme"
| "YellowTheme"
| "GoldDarkTheme"
| "PurpleTheme"
| "BlueTheme"
| "BlueDarkTheme";
export type FCField = { export type FCField = {
text: string text: string;
innerText: string innerText: string;
key: string key: string;
required: boolean required: boolean;
used: boolean used: boolean;
} };
export interface GetDataResponse {
cnt: number; export type QuizSettings = {
items: AnyTypedQuizQuestion[];
settings: { settings: {
qid: string;
fp: boolean; fp: boolean;
rep: boolean; rep: boolean;
name: string; name: string;
cfg: string;
lim: number; lim: number;
due: number; due: number;
delay: number; delay: number;
pausable: boolean; pausable: boolean;
cfg: QuizConfig;
}; };
items: GetItems[];
}
export type QuestionsStore = {
items: (AnyTypedQuizQuestion)[];
settings: Settings;
cnt: number; cnt: number;
}; };
export interface Settings {
fp: boolean;
rep: boolean;
name: string;
lim: number;
due: number;
delay: number;
pausable: boolean;
cfg: QuizConfig
}
export interface QuizConfig { export interface QuizConfig {
type: QuizType; type: QuizType;
noStartPage: boolean; noStartPage: boolean;
startpageType: QuizStartpageType; startpageType: QuizStartpageType;
results: QuizResultsType; results: QuizResultsType;
haveRoot: string; haveRoot: string;
theme: "StandardTheme" | "StandardDarkTheme" | "PinkTheme" | "PinkDarkTheme" | "BlackWhiteTheme" | "OliveTheme" | "YellowTheme" | "GoldDarkTheme" | "PurpleTheme" | "BlueTheme" | "BlueDarkTheme"; theme: QuizTheme;
resultInfo: { resultInfo: {
when: "email" | ""; when: "email" | "";
share: boolean; share: boolean;
@ -84,7 +78,6 @@ export interface QuizConfig {
cycle: boolean; cycle: boolean;
}; };
}; };
formContact: { formContact: {
title: string; title: string;
desc: string; desc: string;
@ -93,7 +86,7 @@ export interface QuizConfig {
phone: FCField; phone: FCField;
text: FCField; text: FCField;
address: FCField; address: FCField;
button: string button: string;
}; };
info: { info: {
phonenumber: string; phonenumber: string;
@ -105,26 +98,12 @@ export interface QuizConfig {
meta: string; meta: string;
} }
export interface GetItems {
id: number;
title: string;
desc: string;
typ: string;
req: boolean;
p: number;
c: string;
}
export interface QuizItems { export interface QuizItems {
description: string;
description: string; id: number;
id: number; page: number;
page: number; required: boolean;
required: boolean; title: string;
title: string; type: string;
type: string; content: unknown;
content: QuizItemsContent }
}
export interface QuizItemsContent {
}

@ -1,379 +1,363 @@
import { Box, Typography, Button, useMediaQuery, TextField, Link, InputAdornment, useTheme } from "@mui/material"; import AddressIcon from "@icons/ContactFormIcon/AddressIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon"; import EmailIcon from "@icons/ContactFormIcon/EmailIcon";
import NameIcon from "@icons/ContactFormIcon/NameIcon";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon"; import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon";
import TextIcon from "@icons/ContactFormIcon/TextIcon"; import TextIcon from "@icons/ContactFormIcon/TextIcon";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon"; import { Box, Button, InputAdornment, Link, TextField as MuiTextField, TextFieldProps, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useDebouncedCallback } from "use-debounce";
import CustomCheckbox from "@ui_kit/CustomCheckbox"; import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { useEffect, useRef, useState } from "react"; import { FC, useRef, useState } from "react";
import { useQuestionsStore } from "@stores/quizData/store";
import { checkEmptyData } from "./tools/checkEmptyData";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { sendFC } from "@api/quizRelase"; import { sendFC } from "@api/quizRelase";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { modes } from "../../utils/themes/Publication/themePublication";
import { QuizQuestionResult } from "@model/questionTypes/result"; import { QuizQuestionResult } from "@model/questionTypes/result";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
import { ApologyPage } from "./ApologyPage"; import { ApologyPage } from "./ApologyPage";
import { checkEmptyData } from "./tools/checkEmptyData";
import { useQuestionsStore } from "@stores/quizData/store";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
const EMAIL_REGEXP = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu; const EMAIL_REGEXP = /^(([^<>()[\].,:\s@"]+(\.[^<>()[\].,:\s@"]+)*)|(".+"))@(([^<>()[\].,:\s@"]+\.)+[^<>()[\].,:\s@"]{2,})$/iu;
type ContactType =
| "name"
| "email"
| "phone"
| "text"
| "adress";
type ContactFormProps = { type ContactFormProps = {
currentQuestion: any; currentQuestion: any;
showResultForm: boolean; showResultForm: boolean;
setShowContactForm: (show: boolean) => void; setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void; setShowResultForm: (show: boolean) => void;
}; };
const icons = [
{ type: "name", icon: NameIcon, defaultText: "Введите имя", defaultTitle: "имя", backendName: "name" },
{ type: "email", icon: EmailIcon, defaultText: "Введите Email", defaultTitle: "Email", backendName: "email" },
{ type: "phone", icon: PhoneIcon, defaultText: "Введите номер телефона", defaultTitle: "номер телефона", backendName: "phone" },
{ type: "text", icon: TextIcon, defaultText: "Введите фамилию", defaultTitle: "фамилию", backendName: "adress" },
{ type: "address", icon: AddressIcon, defaultText: "Введите адрес", defaultTitle: "адрес", backendName: "adress" },
]
export const ContactForm = ({ export const ContactForm = ({
currentQuestion, currentQuestion,
showResultForm, showResultForm,
setShowContactForm, setShowContactForm,
setShowResultForm, setShowResultForm,
}: ContactFormProps) => { }: ContactFormProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings, items } = useQuestionsStore() const { settings, items } = useQuestionsStore()
const [ready, setReady] = useState(false) const [ready, setReady] = useState(false)
const [name, setName] = useState("") const [name, setName] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
const [phone, setPhone] = useState("") const [phone, setPhone] = useState("")
const [text, setText] = useState("") const [text, setText] = useState("")
const [adress, setAdress] = useState("") const [adress, setAdress] = useState("")
const fireOnce = useRef(true) const fireOnce = useRef(true)
const [fire, setFire] = useState(false) const [fire, setFire] = useState(false)
const isMobile = useMediaQuery(theme.breakpoints.down(850)); const isMobile = useMediaQuery(theme.breakpoints.down(850));
const followNextForm = () => { const resultQuestion: QuizQuestionResult = items.find((question): question is QuizQuestionResult => {
setShowContactForm(false); if (settings?.cfg.haveRoot) { //ветвимся
setShowResultForm(true); return (
}; question.type === "result" &&
const mode = modes; question.content.rule.parentId === currentQuestion.content.id
//@ts-ignore );
const resultQuestion: QuizQuestionResult = items.find((question) => { } else {// не ветвимся
if (settings?.cfg.haveRoot) { //ветвимся return (
return ( question.type === "result" &&
question.type === "result" && question.content.rule.parentId === "line"
//@ts-ignore );
question.content.rule.parentId === currentQuestion.content.id }
) })!;
} else {// не ветвимся
return ( const inputHC = async () => {
question.type === "result" && if (!settings) return;
question.content.rule.parentId === "line"
) const body: Partial<Record<ContactType, string>> = {};
if (name.length > 0) body.name = name;
if (email.length > 0) body.email = email;
if (phone.length > 0) body.phone = phone;
if (text.length > 0) body.text = text;
if (adress.length > 0) body.adress = adress;
if (Object.keys(body).length > 0) {
try {
await sendFC({
questionId: resultQuestion?.id,
body: body,
qid: settings.qid
})
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
}
} }
}
);
const inputHC = async () => {
const body = {}
//@ts-ignore //@ts-ignore
if (name.length > 0) body.name = name const FCcopy: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
//@ts-ignore
if (email.length > 0) body.email = email
//@ts-ignore
if (phone.length > 0) body.phone = phone
//@ts-ignore
if (text.length > 0) body.text = text
//@ts-ignore
if (adress.length > 0) body.adress = adress
if (Object.keys(body).length > 0) {
try {
await sendFC({
questionId: resultQuestion?.id,
body: body,
//@ts-ignore
qid: settings.qid
})
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
const filteredFC: any = {};
for (const i in FCcopy) {
const field = FCcopy[i];
console.log(filteredFC);
if (field.used) {
filteredFC[i] = field;
}
} }
} const isWide = Object.keys(filteredFC).length > 2;
//@ts-ignore
let FCcopy: any = settings?.cfg.formContact.fields || settings?.cfg.formContact;
let filteredFC: any = {} if (!settings) throw new Error("settings is null");
for (let i in FCcopy) { if (!resultQuestion) return <ApologyPage message="не получилось найти результат для этой ветки :(" />
let field = FCcopy[i]
console.log(filteredFC)
if (field.used) {
filteredFC[i] = field
}
}
let isWide = Object.keys(filteredFC).length > 2
console.log(isWide)
if (!resultQuestion) return <ApologyPage message="не получилось найти результат для этой ветки :(" /> return (
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.palette.background.default,
height: "100vh",
overflow: "auto",
"&::-webkit-scrollbar": { width: "0", display: "none", msOverflowStyle: "none" },
scrollbarWidth: "none",
msOverflowStyle: "none"
}}
>
<Box
sx={{
width: isWide && !isMobile ? "100%" : (isMobile ? undefined : "530px"),
borderRadius: "4px",
height: "90vh",
display: isWide && !isMobile ? "flex" : undefined
}}
>
<Box <Box
sx={{ sx={{
width: isWide && !isMobile ? "100%" : undefined, display: "flex",
display: "flex", alignItems: "center",
flexDirection: "column", justifyContent: "center",
alignItems: "center", backgroundColor: theme.palette.background.default,
justifyContent: "center", height: "100vh",
borderRight: isWide && !isMobile ? "1px solid gray" : undefined overflow: "auto",
"&::-webkit-scrollbar": { width: "0", display: "none", msOverflowStyle: "none" },
scrollbarWidth: "none",
msOverflowStyle: "none"
}} }}
> >
<Typography <Box
sx={{ sx={{
textAlign: "center", width: isWide && !isMobile ? "100%" : (isMobile ? undefined : "530px"),
m: "20px 0", borderRadius: "4px",
fontSize: "28px", height: "90vh",
color: theme.palette.text.primary display: isWide && !isMobile ? "flex" : undefined
}}
>
{settings?.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"}
</Typography>
{
settings?.cfg.formContact.desc &&
<Typography
sx={{
color: theme.palette.text.primary,
textAlign: "center",
m: "20px 0",
fontSize: "18px"
}}
>
{settings?.cfg.formContact.desc}
</Typography>
}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
backgroundColor: theme.palette.background.default,
p: "30px"
}}>
<Box
sx={{
display: "flex",
flexDirection: "column",
my: "20px"
}}
>
<Inputs
name={name} setName={setName}
email={email} setEmail={setEmail}
phone={phone} setPhone={setPhone}
text={text} setText={setText}
adress={adress} setAdress={setAdress}
/>
</Box>
{
// resultQuestion &&
// settings?.cfg.resultInfo.when === "after" &&
(
<Button
disabled={!(ready && !fire)}
variant="contained"
onClick={async () => {
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact
if (FC["email"].used === EMAIL_REGEXP.test(email)) {//почта валидна
setFire(true)
if (fireOnce.current) {
if (
name.length > 0 ||
email.length > 0 ||
phone.length > 0 ||
text.length > 0 ||
adress.length > 0
) {
try {
await inputHC()
fireOnce.current = false
enqueueSnackbar("Данные успешно отправлены")
} catch (e) {
enqueueSnackbar("повторите попытку позже")
}
if ((settings?.cfg.resultInfo.showResultForm === "after" || settings?.cfg.resultInfo.when === "email") && !checkEmptyData({ resultData: resultQuestion })) {
setShowContactForm(false)
setShowResultForm(true)
}
} else {
enqueueSnackbar("Пожалуйста, заполните поля")
}
}
setFire(false)
} else {
enqueueSnackbar("введена некорректная почта")
}
}} }}
> >
{settings?.cfg.formContact?.button || "Получить результаты"} <Box
</Button> sx={{
)} width: isWide && !isMobile ? "100%" : undefined,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderRight: isWide && !isMobile ? "1px solid gray" : undefined
}}
>
<Typography
sx={{
textAlign: "center",
m: "20px 0",
fontSize: "28px",
color: theme.palette.text.primary
}}
>
{settings?.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"}
<Box </Typography>
sx={{ {
display: "flex", settings?.cfg.formContact.desc &&
mt: "20px", <Typography
width: isMobile ? "300px" : "450px", sx={{
}} color: theme.palette.text.primary,
> textAlign: "center",
<CustomCheckbox label="" handleChange={({ target }) => { setReady(target.checked) }} checked={ready} colorIcon={theme.palette.primary.main} /> m: "20px 0",
<Typography sx={{ color: theme.palette.text.primary }}> fontSize: "18px"
С&ensp; }}
<Link href={"https://shub.pena.digital/ppdd"} target="_blank"> >
Положением об обработке персональных данных </Link> {settings?.cfg.formContact.desc}
&ensp;и&ensp; </Typography>
<Link href={"https://shub.pena.digital/docs/privacy"} target="_blank"> Политикой конфиденциальности </Link> }
&ensp;ознакомлен </Box>
</Typography>
</Box>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
mt: "20px", justifyContent: "center",
gap: "15px" flexDirection: "column",
}} backgroundColor: theme.palette.background.default,
> p: "30px"
<NameplateLogo style={{ }}>
fontSize: "34px",
//@ts-ignore <Box
color: mode[settings.cfg.theme] ? "#151515" : "#FFFFFF" sx={{
}} /> display: "flex",
<Typography sx={{ flexDirection: "column",
fontSize: "20px", my: "20px"
//@ts-ignore }}
color: mode[settings.cfg.theme] ? "#4D4D4D" : "#F5F7FF", whiteSpace: "nowrap" >
}}> <Inputs
Сделано на PenaQuiz name={name} setName={setName}
</Typography> email={email} setEmail={setEmail}
</Box> phone={phone} setPhone={setPhone}
</Box> text={text} setText={setText}
</Box > adress={adress} setAdress={setAdress}
</Box > />
);
</Box>
{
// resultQuestion &&
// settings?.cfg.resultInfo.when === "after" &&
(
<Button
disabled={!(ready && !fire)}
variant="contained"
onClick={async () => {
//@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact
if (FC["email"].used === EMAIL_REGEXP.test(email)) {//почта валидна
setFire(true)
if (fireOnce.current) {
if (
name.length > 0 ||
email.length > 0 ||
phone.length > 0 ||
text.length > 0 ||
adress.length > 0
) {
try {
await inputHC()
fireOnce.current = false
enqueueSnackbar("Данные успешно отправлены")
} catch (e) {
enqueueSnackbar("повторите попытку позже")
}
if ((settings?.cfg.resultInfo.showResultForm === "after" || settings?.cfg.resultInfo.when === "email") && !checkEmptyData({ resultData: resultQuestion })) {
setShowContactForm(false)
setShowResultForm(true)
}
} else {
enqueueSnackbar("Пожалуйста, заполните поля")
}
}
setFire(false)
} else {
enqueueSnackbar("введена некорректная почта")
}
}}
>
{settings?.cfg.formContact?.button || "Получить результаты"}
</Button>
)}
<Box
sx={{
display: "flex",
mt: "20px",
width: isMobile ? "300px" : "450px",
}}
>
<CustomCheckbox label="" handleChange={({ target }) => { setReady(target.checked) }} checked={ready} colorIcon={theme.palette.primary.main} />
<Typography sx={{ color: theme.palette.text.primary }}>
С&ensp;
<Link href={"https://shub.pena.digital/ppdd"} target="_blank">
Положением об обработке персональных данных </Link>
&ensp;и&ensp;
<Link href={"https://shub.pena.digital/docs/privacy"} target="_blank"> Политикой конфиденциальности </Link>
&ensp;ознакомлен
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: "20px",
gap: "15px"
}}
>
<NameplateLogo style={{
fontSize: "34px",
color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF"
}} />
<Typography sx={{
fontSize: "20px",
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF", whiteSpace: "nowrap"
}}>
Сделано на PenaQuiz
</Typography>
</Box>
</Box>
</Box >
</Box >
);
}; };
const Inputs = ({ const Inputs = ({
name, setName, name, setName,
email, setEmail, email, setEmail,
phone, setPhone, phone, setPhone,
text, setText, text, setText,
adress, setAdress adress, setAdress
}: any) => { }: any) => {
const { settings, items } = useQuestionsStore() const { settings } = useQuestionsStore()
console.log("______________________EMAIL_REGEXP.test(email)")
console.log(EMAIL_REGEXP.test(email))
//@ts-ignore //@ts-ignore
const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact const FC: any = settings?.cfg.formContact.fields || settings?.cfg.formContact
//@ts-ignore //@ts-ignore
const Name = <CustomInput onChange={({ target }) => setName(target.value)} id={name} title={FC["name"].innerText || "Введите имя"} desc={FC["name"].text || "имя"} Icon={NameIcon} /> const Name = <CustomInput onChange={({ target }) => setName(target.value)} id={name} title={FC["name"].innerText || "Введите имя"} desc={FC["name"].text || "имя"} Icon={NameIcon} />
//@ts-ignore //@ts-ignore
const Email = <CustomInput const Email = <CustomInput
error = {!EMAIL_REGEXP.test(email)} error = {!EMAIL_REGEXP.test(email)}
label={!EMAIL_REGEXP.test(email) ? "" : "Некорректная почта"} label={!EMAIL_REGEXP.test(email) ? "" : "Некорректная почта"}
//@ts-ignore //@ts-ignore
onChange={({ target }) => setEmail(target.value)} id={email} title={FC["email"].innerText || "Введите Email"} desc={FC["email"].text || "Email"} Icon={EmailIcon} /> onChange={({ target }) => setEmail(target.value)} id={email} title={FC["email"].innerText || "Введите Email"} desc={FC["email"].text || "Email"} Icon={EmailIcon} />
//@ts-ignore //@ts-ignore
const Phone = <CustomInput onChange={({ target }) => setPhone(target.value)} id={phone} title={FC["phone"].innerText || "Введите номер телефона"} desc={FC["phone"].text || "номер телефона"} Icon={PhoneIcon} /> const Phone = <CustomInput onChange={({ target }) => setPhone(target.value)} id={phone} title={FC["phone"].innerText || "Введите номер телефона"} desc={FC["phone"].text || "номер телефона"} Icon={PhoneIcon} />
//@ts-ignore //@ts-ignore
const Text = <CustomInput onChange={({ target }) => setText(target.value)} id={text} title={FC["text"].innerText || "Введите фамилию"} desc={FC["text"].text || "фамилию"} Icon={TextIcon} /> const Text = <CustomInput onChange={({ target }) => setText(target.value)} id={text} title={FC["text"].innerText || "Введите фамилию"} desc={FC["text"].text || "фамилию"} Icon={TextIcon} />
//@ts-ignore //@ts-ignore
const Adress = <CustomInput onChange={({ target }) => setAdress(target.value)} id={adress} title={FC["address"].innerText || "Введите адрес"} desc={FC["address"].text || "адрес"} Icon={AddressIcon} /> const Adress = <CustomInput onChange={({ target }) => setAdress(target.value)} id={adress} title={FC["address"].innerText || "Введите адрес"} desc={FC["address"].text || "адрес"} Icon={AddressIcon} />
//@ts-ignore //@ts-ignore
if (Object.values(FC).some((data) => data.used)) { if (Object.values(FC).some((data) => data.used)) {
return <> return <>
{FC["name"].used ? Name : <></>} {FC["name"].used ? Name : <></>}
{FC["email"].used ? Email : <></>} {FC["email"].used ? Email : <></>}
{FC["phone"].used ? Phone : <></>} {FC["phone"].used ? Phone : <></>}
{FC["text"].used ? Text : <></>} {FC["text"].used ? Text : <></>}
{FC["address"].used ? Adress : <></>} {FC["address"].used ? Adress : <></>}
</> </>
} else { } else {
return <> return <>
{Name} {Name}
{Email} {Email}
{Phone} {Phone}
</> </>
} }
} }
const CustomInput = ({ title, desc, Icon, onChange }: any) => { const CustomInput = ({ title, desc, Icon, onChange }: any) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600)); const isMobile = useMediaQuery(theme.breakpoints.down(600));
//@ts-ignore
return <Box m="15px 0">
<Typography mb="7px" color={theme.palette.text.primary}>{title}</Typography>
<TextField return (
onChange={onChange} <Box m="15px 0">
sx={{ <Typography mb="7px" color={theme.palette.text.primary}>{title}</Typography>
width: isMobile ? "300px" : "350px",
}} <TextField
placeholder={desc} onChange={onChange}
InputProps={{ sx={{
startAdornment: <InputAdornment position="start"><Icon color="gray" /></InputAdornment>, width: isMobile ? "300px" : "350px",
}} }}
/> placeholder={desc}
</Box> InputProps={{
startAdornment: <InputAdornment position="start"><Icon color="gray" /></InputAdornment>,
}}
/>
</Box>
)
} }

@ -1,322 +1,298 @@
import { useState, useEffect } from "react";
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useCallback, useMemo, useState } from "react";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import { useQuestionsStore } from "@stores/quizData/store";
import { getQuestionById } from "@stores/quizData/actions"; import { getQuestionById } from "@stores/quizData/actions";
import { useQuizViewStore } from "@stores/quizView/store";
import { enqueueSnackbar } from "notistack";
import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
import { modes } from "../../utils/themes/Publication/themePublication"; import { enqueueSnackbar } from "notistack";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import { checkEmptyData } from "./tools/checkEmptyData"; import { checkEmptyData } from "./tools/checkEmptyData";
import type { QuizQuestionResult } from "@model/questionTypes/result"; import type { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuestionsStore } from "@stores/quizData/store";
import { useQuizViewStore } from "@stores/quizView/store";
type FooterProps = { type FooterProps = {
setCurrentQuestion: (step: AnyTypedQuizQuestion) => void; setCurrentQuestion: (step: AnyTypedQuizQuestion) => void;
question: AnyTypedQuizQuestion; question: AnyTypedQuizQuestion;
setShowContactForm: (show: boolean) => void; setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void; setShowResultForm: (show: boolean) => void;
}; };
export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setShowResultForm }: FooterProps) => { export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setShowResultForm }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { settings, items } = useQuestionsStore(); const { settings, items } = useQuestionsStore();
const { answers } = useQuizViewStore(); const answers = useQuizViewStore(state => state.answers);
const mode = modes; const [stepNumber, setStepNumber] = useState(1);
const [stepNumber, setStepNumber] = useState(1); const isMobileMini = useMediaQuery(theme.breakpoints.down(382));
const [disablePreviousButton, setDisablePreviousButton] = useState<boolean>(false); const isLinear = !items.some(({ content }) => content.rule.parentId === "root");
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
const isMobileMini = useMediaQuery(theme.breakpoints.down(382)); const getNextQuestionId = useCallback(() => {
const linear = !items.find(({ content }) => content.rule.parentId === "root"); console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг");
console.log(question);
console.log("От вот этого /|");
let readyBeNextQuestion = "";
useEffect(() => { //вопрос обязателен, анализируем ответ и условия ветвления
// Логика для аргумента disabled у кнопки "Назад" if (answers.length) {
if (linear) { const answer = answers.find(({ questionId }) => questionId === question.id);
const questionIndex = items.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1];
if (previousQuestion) { (question as QuizQuestionBase).content.rule.main.forEach(({ next, rules }) => {
setDisablePreviousButton(false); const longerArray = Math.max(
} else { rules[0].answers.length,
setDisablePreviousButton(true); answer?.answer && Array.isArray(answer?.answer) ? answer?.answer.length : [answer?.answer].length
} );
} else {
if (question?.content.rule.parentId === "root") {
setDisablePreviousButton(true);
} else {
setDisablePreviousButton(false);
}
}
// Логика для аргумента disabled у кнопки "Далее" for (
const answer = answers.find(({ questionId }) => questionId === question.id); let i = 0;
i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива
) {
if (Array.isArray(answer?.answer)) {
if (answer?.answer.find((item) => String(item === rules[0].answers[i]))) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
if ("required" in question.content && question.content.required && answer) { return;
setDisableNextButton(false); }
return; if (String(rules[0].answers[i]) === answer?.answer) {
} readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
});
if ("required" in question.content && question.content.required && !answer) { if (readyBeNextQuestion) return readyBeNextQuestion;
setDisableNextButton(true); }
return; if (!question.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
} console.log("вопрос не обязателен ищем дальше");
const defaultQ = question.content.rule.default;
if (defaultQ.length > 1 && defaultQ !== " ") return defaultQ;
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
(question?.type === "date" ||
question?.type === "text" ||
question?.type === "number" ||
question?.type === "page") && question.content.rule.children.length === 1
) return question.content.rule.children[0];
if (linear) {
return;
}
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
setDisableNextButton(false);
} else {
const nextQuestion = getQuestionById(question.content.rule.default);
if (nextQuestion?.type) {
setDisableNextButton(false);
}
}
}, [question, answers]);
const showResult = (nextQuestion: QuizQuestionResult) => {
const isEmpty = checkEmptyData({ resultData: nextQuestion })
console.log("isEmpty", isEmpty)
if (nextQuestion) {
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "before") {
if (isEmpty) {
setShowContactForm(true); //до+пустая = кидать на ФК
} else {
setShowResultForm(true); //до+заполнена = показать
} }
} //ничё не нашли, ищем резулт
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "after") { console.log("ничё не нашли, ищем резулт ");
if (isEmpty) { return items.find(q => {
setShowContactForm(true); //после+пустая console.log('q.type === "result"', q.type === "result");
console.log('q.content.rule.parentId', q.content.rule.parentId);
console.log('question.content.id', question.content.id);
return q.type === "result" && q.content.rule.parentId === question.content.id;
})?.id;
}, [answers, items, question]);
const isPreviousButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Назад"
if (isLinear) {
const questionIndex = items.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1];
return previousQuestion ? false : true;
} else { } else {
setShowContactForm(true); //после+заполнена = показать ФК return question?.content.rule.parentId === "root" ? true : false;
} }
} }, [items, isLinear, question?.content.rule.parentId, question.id]);
}
}; const isNextButtonDisabled = useMemo(() => {
// Логика для аргумента disabled у кнопки "Далее"
const answer = answers.find(({ questionId }) => questionId === question.id);
const getNextQuestionId = () => { if ("required" in question.content && question.content.required && answer) {
console.log("Смотрим какой вопрос будет дальше. Что у нас сегодня вкусненького? Щя покажу от какого вопроса мы ищем следующий шаг") return false;
console.log(question) }
console.log("От вот этого /|")
let readyBeNextQuestion = "";
//вопрос обязателен, анализируем ответ и условия ветвления if ("required" in question.content && question.content.required && !answer) {
if (answers.length) { return true;
const answer = answers.find(({ questionId }) => questionId === question.id); }
if (isLinear) {
return false;
}
(question as QuizQuestionBase).content.rule.main.forEach(({ next, rules }) => { const nextQuestionId = getNextQuestionId();
let longerArray = Math.max( if (nextQuestionId) {
rules[0].answers.length, return false;
answer?.answer && Array.isArray(answer?.answer) ? answer?.answer.length : [answer?.answer].length } else {
); const nextQuestion = getQuestionById(question.content.rule.default);
for ( if (nextQuestion?.type) {
var i = 0; return false;
i < longerArray; }
i++ // Цикл по всем эле­мен­там бОльшего массива }
) { }, [answers, getNextQuestionId, isLinear, question.content, question.id]);
if (Array.isArray(answer?.answer)) {
if (answer?.answer.find((item) => String(item === rules[0].answers[i]))) { const showResult = (nextQuestion: QuizQuestionResult) => {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны if (!settings) return;
if (!nextQuestion) return;
const isEmpty = checkEmptyData({ resultData: nextQuestion });
if (nextQuestion) {
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "before") {
if (isEmpty) {
setShowContactForm(true); //до+пустая = кидать на ФК
} else {
setShowResultForm(true); //до+заполнена = показать
}
}
if (nextQuestion && settings?.cfg.resultInfo.showResultForm === "after") {
if (isEmpty) {
setShowContactForm(true); //после+пустая
} else {
setShowContactForm(true); //после+заполнена = показать ФК
}
}
}
};
const followPreviousStep = () => {
if (isLinear) {
setStepNumber(q => q - 1);
const questionIndex = items.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1];
if (previousQuestion) {
setCurrentQuestion(previousQuestion);
} }
return; return;
}
if (String(rules[0].answers[i]) === answer?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
});
if (readyBeNextQuestion) return readyBeNextQuestion;
}
if (!question.required) {//вопрос не обязателен и не нашли совпадений между ответами и условиями ветвления
console.log("вопрос не обязателен ищем дальше")
const defaultQ = question.content.rule.default
if (defaultQ.length > 1 && defaultQ !== " ") return defaultQ
//Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
if (
(question?.type === "date" ||
question?.type === "text" ||
question?.type === "number" ||
question?.type === "page") && question.content.rule.children.length === 1
) return question.content.rule.children[0]
}
//ничё не нашли, ищем резулт
console.log("ничё не нашли, ищем резулт ")
return items.find(q => {
console.log('q.type === "result"', q.type === "result")
console.log('q.content.rule.parentId', q.content.rule.parentId)
//@ts-ignore
console.log('question.content.id', question.content.id)
//@ts-ignore
return q.type === "result" && q.content.rule.parentId === question.content.id
})?.id
};
const followPreviousStep = () => {
if (linear) {
setStepNumber(q => q - 1)
const questionIndex = items.findIndex(({ id }) => id === question.id);
const previousQuestion = items[questionIndex - 1];
if (previousQuestion) {
setCurrentQuestion(previousQuestion);
}
return;
}
if (question?.content.rule.parentId !== "root") {
const parent = getQuestionById(question?.content.rule.parentId);
if (parent?.type) {
setCurrentQuestion(parent);
} else {
enqueueSnackbar("не могу получить предыдущий вопрос");
}
} else {
enqueueSnackbar("вы находитесь на первом вопросе");
}
};
const followNextStep = () => {
if (linear) {
setStepNumber(q => q + 1)
const questionIndex = items.findIndex(({ id }) => id === question.id);
const nextQuestion = items[questionIndex + 1];
if (nextQuestion && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion);
} else {
//@ts-ignore
showResult(items.find(q => q.content.rule.parentId === "line"));
}
return;
}
const nextQuestionId = getNextQuestionId();
console.log(nextQuestionId)
if (nextQuestionId) {
const nextQuestion = getQuestionById(nextQuestionId);
console.log(nextQuestion)
if (nextQuestion?.type && nextQuestion.type === "result") {
showResult(nextQuestion);
} else {
//@ts-ignore
setCurrentQuestion(nextQuestion);
}
} else {
enqueueSnackbar("не могу получить последующий вопрос");
}
};
return (
<Box
sx={{
position: "relative",
padding: "15px 0",
borderTop: `1px solid ${theme.palette.grey[400]}`,
height: '75px',
display: "flex"
}}
>
<Box
sx={{
width: "100%",
maxWidth: "1000px",
padding: "0 10px",
margin: "0 auto",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{/*{mode[settings.cfg.theme] ? (*/}
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/}
{linear &&
<>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
color: theme.palette.text.primary,
}}
>
<Typography>Шаг</Typography>
<Typography
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "50%",
width: "30px",
height: "30px",
color: "#FFF",
background: theme.palette.primary.main,
}}
>
{stepNumber}
</Typography>
<Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}>
{items.filter(q => q.type !== "result").length}
</Typography>
</Box>
</>
} }
if (question?.content.rule.parentId !== "root") {
const parent = getQuestionById(question?.content.rule.parentId);
if (parent?.type) {
setCurrentQuestion(parent);
} else {
enqueueSnackbar("не могу получить предыдущий вопрос");
}
} else {
enqueueSnackbar("вы находитесь на первом вопросе");
}
};
const followNextStep = () => {
if (isLinear) {
setStepNumber(q => q + 1);
const questionIndex = items.findIndex(({ id }) => id === question.id);
const nextQuestion = items[questionIndex + 1];
if (nextQuestion && nextQuestion.type !== "result") {
setCurrentQuestion(nextQuestion);
} else {
//@ts-ignore
showResult(items.find(q => q.content.rule.parentId === "line"));
}
return;
}
const nextQuestionId = getNextQuestionId();
if (nextQuestionId) {
const nextQuestion = getQuestionById(nextQuestionId);
if (nextQuestion?.type && nextQuestion.type === "result") {
showResult(nextQuestion);
} else {
//@ts-ignore
setCurrentQuestion(nextQuestion);
}
} else {
enqueueSnackbar("не могу получить последующий вопрос");
}
};
return (
<Box <Box
sx={{ sx={{
display: "flex", position: "relative",
alignItems: "center", padding: "15px 0",
gap: "10px", borderTop: `1px solid ${theme.palette.grey[400]}`,
marginRight: "auto", height: '75px',
// color: theme.palette.grey1.main, display: "flex",
}} }}
> >
{/* <Typography>Шаг</Typography> <Box
sx={{
width: "100%",
maxWidth: "1000px",
padding: "0 10px",
margin: "0 auto",
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
{/*{mode[settings.cfg.theme] ? (*/}
{/* <NameplateLogoFQ style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*):(*/}
{/* <NameplateLogoFQDark style={{ fontSize: "34px", width:"200px", height:"auto" }} />*/}
{/*)}*/}
{isLinear &&
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
color: theme.palette.text.primary,
}}
>
<Typography>Шаг</Typography>
<Typography
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontWeight: "bold",
borderRadius: "50%",
width: "30px",
height: "30px",
color: "#FFF",
background: theme.palette.primary.main,
}}
>
{stepNumber}
</Typography>
<Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}>
{items.filter(q => q.type !== "result").length}
</Typography>
</Box>
}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
marginRight: "auto",
// color: theme.palette.grey1.main,
}}
>
{/* <Typography>Шаг</Typography>
<Typography <Typography
sx={{ sx={{
display: "flex", display: "flex",
@ -331,36 +307,33 @@ export const Footer = ({ setCurrentQuestion, question, setShowContactForm, setSh
}} }}
> >
{stepNumber} */} {stepNumber} */}
{/* </Typography> */} {/* </Typography> */}
{/* <Typography>Из</Typography> {/* <Typography>Из</Typography>
<Typography sx={{ fontWeight: "bold" }}> <Typography sx={{ fontWeight: "bold" }}>
{questions.length} {questions.length}
</Typography> */} </Typography> */}
</Box>
<Button
disabled={isPreviousButtonDisabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px", }}
onClick={followPreviousStep}
>
{isMobileMini ? (
"←"
) : (
"← Назад"
)}
</Button>
<Button
disabled={isNextButtonDisabled}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={followNextStep}
>
Далее
</Button>
</Box>
</Box> </Box>
<Button );
disabled={disablePreviousButton}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px",}}
onClick={followPreviousStep}
>
{isMobileMini ? (
"←"
) : (
"← Назад"
)}
</Button>
<Button
disabled={disableNextButton}
variant="contained"
sx={{ fontSize: "16px", padding: "10px 15px" }}
onClick={followNextStep}
>
Далее
</Button>
</Box>
</Box>
);
}; };

@ -1,141 +1,140 @@
import { useState, useEffect } from "react"; import { Box, useMediaQuery, useTheme } from "@mui/material";
import {Box, useMediaQuery, useTheme} from "@mui/material"; import { useEffect, useState } from "react";
import { useQuestionsStore } from "@stores/quizData/store"
import { getQuestionById } from "@stores/quizData/actions"; import { getQuestionById } from "@stores/quizData/actions";
import { Variant } from "./questions/Variant"; import { ContactForm } from "./ContactForm";
import { Images } from "./questions/Images"; import { Footer } from "./Footer";
import { Varimg } from "./questions/Varimg"; import { ResultForm } from "./ResultForm";
import { Emoji } from "./questions/Emoji";
import { Text } from "./questions/Text";
import { Select } from "./questions/Select";
import { Date } from "./questions/Date"; import { Date } from "./questions/Date";
import { Number } from "./questions/Number"; import { Emoji } from "./questions/Emoji";
import { File } from "./questions/File"; import { File } from "./questions/File";
import { Images } from "./questions/Images";
import { Number } from "./questions/Number";
import { Page } from "./questions/Page"; import { Page } from "./questions/Page";
import { Rating } from "./questions/Rating"; import { Rating } from "./questions/Rating";
import { Footer } from "./Footer"; import { Select } from "./questions/Select";
import { ContactForm } from "./ContactForm"; import { Text } from "./questions/Text";
import { ResultForm } from "./ResultForm"; import { Variant } from "./questions/Variant";
import { Varimg } from "./questions/Varimg";
import type { QuestionType } from "@model/questionTypes/shared";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import {NameplateLogoFQ} from "@icons/NameplateLogoFQ"; import { NameplateLogoFQ } from "@icons/NameplateLogoFQ";
import {NameplateLogoFQDark} from "@icons/NameplateLogoFQDark"; import { NameplateLogoFQDark } from "@icons/NameplateLogoFQDark";
import {modes} from "../../utils/themes/Publication/themePublication"; import { QuizQuestionResult } from "@model/questionTypes/result";
import { useQuestionsStore } from "@stores/quizData/store";
import { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type QuestionProps = { export const Question = () => {
questions: AnyTypedQuizQuestion[]; const theme = useTheme();
}; const settings = useQuestionsStore(state => state.settings);
const questions = useQuestionsStore(state => state.items);
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false);
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const QUESTIONS_MAP: any = { useEffect(() => {
variant: Variant,
images: Images,
varimg: Varimg,
emoji: Emoji,
text: Text,
select: Select,
date: Date,
number: Number,
file: File,
page: Page,
rating: Rating,
};
export const Question = ({ questions }: QuestionProps) => { if (settings?.cfg.haveRoot) {//ветвимся
const { settings } = useQuestionsStore() const nextQuestion = getQuestionById(settings?.cfg.haveRoot || "");
const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const [currentQuestion, setCurrentQuestion] = useState<AnyTypedQuizQuestion>();
const [showContactForm, setShowContactForm] = useState<boolean>(false);
const [showResultForm, setShowResultForm] = useState<boolean>(false);
const mode = modes;
console.log("currentQuestion ", currentQuestion)
useEffect(() => {
if (settings?.cfg.haveRoot) {//ветвимся if (nextQuestion?.type) {
const nextQuestion = getQuestionById(settings?.cfg.haveRoot || ""); setCurrentQuestion(nextQuestion);
return;
}
if (nextQuestion?.type) { } else {//идём прямо
setCurrentQuestion(nextQuestion); setCurrentQuestion(questions[0]);
return; }
}
} else {//идём прямо }, []);
setCurrentQuestion(questions[0]);
}
}, []); if (!settings) throw new Error("settings is null");
if (!currentQuestion || currentQuestion.type === "result") return "не смог отобразить вопрос";
if (!currentQuestion) return <>не смог отобразить вопрос</>; return (
const QuestionComponent =
QUESTIONS_MAP[currentQuestion.type as Exclude<QuestionType, "nonselected">];
return (
<Box
sx={{
backgroundColor: theme.palette.background.default,
height: isMobile ? undefined : "100vh"
}}
>
{!showContactForm && !showResultForm && (
<Box <Box
sx={{ sx={{
height: "calc(100vh - 75px)", backgroundColor: theme.palette.background.default,
width: "100%", height: isMobile ? undefined : "100vh"
maxWidth: "1440px", }}
padding: "40px 25px 20px",
margin: "0 auto",
overflow: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
> >
<QuestionComponent currentQuestion={currentQuestion} /> {!showContactForm && !showResultForm && (
{mode[settings?.cfg.theme] ? ( <Box
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} /> sx={{
) : ( height: "calc(100vh - 75px)",
<NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} /> width: "100%",
)} maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
overflow: "auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between"
}}
>
<QuestionByType question={currentQuestion} />
{quizThemes[settings.cfg.theme].isLight ? (
<NameplateLogoFQ style={{ fontSize: "34px", width: "200px", height: "auto" }} />
) : (
<NameplateLogoFQDark style={{ fontSize: "34px", width: "200px", height: "auto" }} />
)}
</Box>
)}
{showResultForm && settings?.cfg.resultInfo.showResultForm === "before" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showContactForm && (
<ContactForm
currentQuestion={currentQuestion}
showResultForm={showResultForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showResultForm && settings?.cfg.resultInfo.showResultForm === "after" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{!showContactForm && !showResultForm && (
<Footer
question={currentQuestion}
setCurrentQuestion={setCurrentQuestion}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
</Box> </Box>
)} );
{showResultForm && settings?.cfg.resultInfo.showResultForm === "before" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showContactForm && (
<ContactForm
currentQuestion={currentQuestion}
showResultForm={showResultForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{showResultForm && settings?.cfg.resultInfo.showResultForm === "after" && (
<ResultForm
currentQuestion={currentQuestion}
showContactForm={showContactForm}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
{!showContactForm && !showResultForm && (
<Footer
question={currentQuestion}
setCurrentQuestion={setCurrentQuestion}
setShowContactForm={setShowContactForm}
setShowResultForm={setShowResultForm}
/>
)}
</Box>
);
}; };
function QuestionByType({ question }: {
question: Exclude<AnyTypedQuizQuestion, QuizQuestionResult>;
}) {
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} />;
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: return notReachable(question);
}
}

@ -1,234 +1,228 @@
import { import {
Box, Box,
Typography, Button,
Button, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useQuestionsStore } from "@stores/quizData/store";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { modes } from "../../utils/themes/Publication/themePublication"; import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { useQuestionsStore } from "@stores/quizData/store";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useCallback, useEffect, useMemo } from "react";
import type { QuizQuestionResult } from "../../model/questionTypes/result"; import type { QuizQuestionResult } from "../../model/questionTypes/result";
type ResultFormProps = { type ResultFormProps = {
currentQuestion: any; currentQuestion: any;
showContactForm: boolean; showContactForm: boolean;
setShowContactForm: (show: boolean) => void; setShowContactForm: (show: boolean) => void;
setShowResultForm: (show: boolean) => void; setShowResultForm: (show: boolean) => void;
}; };
export const ResultForm = ({ export const ResultForm = ({
currentQuestion, currentQuestion,
showContactForm, showContactForm,
setShowContactForm, setShowContactForm,
setShowResultForm, setShowResultForm,
}: ResultFormProps) => { }: ResultFormProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { settings, items } = useQuestionsStore(); const { settings, items } = useQuestionsStore();
const mode = modes; if (!settings) throw new Error("settings is null");
const searchResult = (): QuizQuestionResult => { const resultQuestion = useMemo(() => {
if (Boolean(settings?.cfg.haveRoot)) { if (settings?.cfg.haveRoot) {
//ищём для ветвления //ищём для ветвления
return (items.find( return (items.find(
(question) => (question): question is QuizQuestionResult =>
question.type === "result" && question.type === "result" &&
question.content.rule.parentId === currentQuestion.content.id question.content.rule.parentId === currentQuestion.content.id
) || ) || items.find(
items.find( (question): question is QuizQuestionResult =>
(question) => question.type === "result" &&
question.type === "result" && question.content.rule.parentId === "line"
question.content.rule.parentId === "line" ));
)) as QuizQuestionResult; } else {
} else { return items.find(
return items.find( (question): question is QuizQuestionResult =>
(question) => question.type === "result" &&
question.type === "result" && question.content.rule.parentId === "line"
question.content.rule.parentId === "line" );
) as QuizQuestionResult; }
} }, [currentQuestion.content.id, items, settings?.cfg.haveRoot]);
};
const resultQuestion = searchResult(); const followNextForm = useCallback(() => {
setShowResultForm(false);
setShowContactForm(true);
},[setShowContactForm, setShowResultForm]);
useEffect(() => {
if (!resultQuestion) {
followNextForm();
}
}, [followNextForm, resultQuestion]);
if (!resultQuestion) return null;
const followNextForm = () => {
setShowResultForm(false);
setShowContactForm(true);
};
console.log(resultQuestion);
if (resultQuestion === null || resultQuestion === undefined) {
followNextForm();
return <></>;
} else {
return ( return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "space-between",
height: "100vh",
width: "100vw",
pt: "28px",
overflow: "auto",
}}
>
<Box <Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
width: isMobile ? "100%" : "490px",
padding: isMobile ? "0 16px" : undefined,
}}
>
{
//@ts-ignore
!resultQuestion?.content.useImage &&
resultQuestion.content.video && (
<YoutubeEmbedIframe
//@ts-ignore
videoUrl={resultQuestion.content.video}
containerSX={{
width: isMobile ? "100%" : "490px",
height: isMobile ? "100%" : "280px",
}}
/>
)
}
{
//@ts-ignore
resultQuestion?.content.useImage && resultQuestion.content.back && (
<Box
component="img"
src={resultQuestion.content.back}
sx={{
width: isMobile ? "100%" : "490px",
height: isMobile ? "100%" : "280px",
}}
></Box>
)
}
{resultQuestion.description !== "" &&
resultQuestion.description !== " " && (
<Typography
sx={{
fontSize: "23px",
fontWeight: 700,
m: "20px 0",
color: theme.palette.text.primary,
}}
>
{resultQuestion.description}
</Typography>
)}
<Typography
sx={{ sx={{
m: "20px 0",
color: theme.palette.text.primary,
}}
>
{resultQuestion.title}
</Typography>
{
//@ts-ignore
resultQuestion.content.text !== "" &&
//@ts-ignore
resultQuestion.content.text !== " " && (
<Typography
sx={{
fontSize: "18px",
m: "20px 0",
color: theme.palette.text.primary,
}}
>
{
//@ts-ignore
resultQuestion.content.text
}
</Typography>
)
}
</Box>
<Box width="100%">
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "end",
px: "20px",
}}
>
<Box
sx={{
display: "flex", display: "flex",
flexDirection: "column",
alignItems: "center", alignItems: "center",
mt: "15px", justifyContent: "space-between",
gap: "10px", height: "100vh",
}} width: "100vw",
> pt: "28px",
<NameplateLogo overflow: "auto",
style={{
fontSize: "34px",
color: mode[settings.cfg.theme] ? "#000000" : "#F5F7FF",
}}
/>
<Typography
sx={{
fontSize: "20px",
//@ts-ignore
color: mode[settings.cfg.theme] ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap",
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
</Box>
<Box
sx={{
boxShadow: "0 0 15px 0 rgba(0,0,0,.08)",
width: "100%",
flexDirection: "column",
display: "flex",
justifyContent: "center",
alignItems: "center",
p: "20px",
}} }}
> >
{settings?.cfg.resultInfo.showResultForm === "before" && ( <Box
<Button
onClick={followNextForm}
variant="contained"
sx={{ sx={{
p: "10px 20px", display: "flex",
width: "210px", flexDirection: "column",
height: "50px", alignItems: "start",
width: isMobile ? "100%" : "490px",
padding: isMobile ? "0 16px" : undefined,
}} }}
> >
{resultQuestion.content.hint.text || "Узнать подробнее"} {
</Button> !resultQuestion?.content.useImage && resultQuestion.content.video && (
)} <YoutubeEmbedIframe
{settings?.cfg.resultInfo.showResultForm === "after" && videoUrl={resultQuestion.content.video}
resultQuestion.content.redirect && ( containerSX={{
<Button width: isMobile ? "100%" : "490px",
href={resultQuestion.content.redirect} height: isMobile ? "100%" : "280px",
variant="contained" }}
sx={{ p: "10px 20px", width: "210px", height: "50px" }} />
)
}
{
resultQuestion?.content.useImage && resultQuestion.content.back && (
<Box
component="img"
src={resultQuestion.content.back}
sx={{
width: isMobile ? "100%" : "490px",
height: isMobile ? "100%" : "280px",
}}
></Box>
)
}
{resultQuestion.description !== "" &&
resultQuestion.description !== " " && (
<Typography
sx={{
fontSize: "23px",
fontWeight: 700,
m: "20px 0",
color: theme.palette.text.primary,
}}
>
{resultQuestion.description}
</Typography>
)}
<Typography
sx={{
m: "20px 0",
color: theme.palette.text.primary,
}}
> >
{resultQuestion.content.hint.text || "Перейти на сайт"} {resultQuestion.title}
</Button> </Typography>
)}
</Box> {
resultQuestion.content.text !== "" &&
resultQuestion.content.text !== " " && (
<Typography
sx={{
fontSize: "18px",
m: "20px 0",
color: theme.palette.text.primary,
}}
>
{
resultQuestion.content.text
}
</Typography>
)
}
</Box>
<Box width="100%">
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "end",
px: "20px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: "15px",
gap: "10px",
}}
>
<NameplateLogo
style={{
fontSize: "34px",
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
}}
/>
<Typography
sx={{
fontSize: "20px",
color: quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF",
whiteSpace: "nowrap",
}}
>
Сделано на PenaQuiz
</Typography>
</Box>
</Box>
<Box
sx={{
boxShadow: "0 0 15px 0 rgba(0,0,0,.08)",
width: "100%",
flexDirection: "column",
display: "flex",
justifyContent: "center",
alignItems: "center",
p: "20px",
}}
>
{settings?.cfg.resultInfo.showResultForm === "before" && (
<Button
onClick={followNextForm}
variant="contained"
sx={{
p: "10px 20px",
width: "210px",
height: "50px",
}}
>
{resultQuestion.content.hint.text || "Узнать подробнее"}
</Button>
)}
{settings?.cfg.resultInfo.showResultForm === "after" &&
resultQuestion.content.redirect && (
<Button
href={resultQuestion.content.redirect}
variant="contained"
sx={{ p: "10px 20px", width: "210px", height: "50px" }}
>
{resultQuestion.content.hint.text || "Перейти на сайт"}
</Button>
)}
</Box>
</Box>
</Box> </Box>
</Box>
); );
}
}; };

@ -1,473 +1,471 @@
import { Box, Button, ButtonBase, Link, Paper, Typography, useMediaQuery, useTheme } from "@mui/material"; import { Box, Button, ButtonBase, Link, Paper, Typography, useMediaQuery, useTheme } from "@mui/material";
import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe"; import YoutubeEmbedIframe from "./tools/YoutubeEmbedIframe";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/quizSettings";
import { notReachable } from "../../utils/notReachable"; import { notReachable } from "../../utils/notReachable";
import { useUADevice } from "../../utils/hooks/useUADevice"; import { useUADevice } from "../../utils/hooks/useUADevice";
import { useQuestionsStore } from "@stores/quizData/store";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { modes } from "../../utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { QuizStartpageAlignType, QuizStartpageType } from "@model/settingsData";
import { useQuestionsStore } from "@stores/quizData/store";
interface Props { interface Props {
setVisualStartPage: (a: boolean) => void; setVisualStartPage: (a: boolean) => void;
} }
export const StartPageViewPublication = ({ setVisualStartPage }: Props) => { export const StartPageViewPublication = ({ setVisualStartPage }: Props) => {
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuestionsStore() const { settings } = useQuestionsStore();
const mode = modes; const { isMobileDevice } = useUADevice();
const { isMobileDevice } = useUADevice(); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
if (!settings) return null; if (!settings) throw new Error("settings is null");
console.log(settings); const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
};
const handleCopyNumber = () => { const background =
navigator.clipboard.writeText(settings.cfg.info.phonenumber); settings.cfg.startpage.background.type === "image" ? (
}; settings.cfg.startpage.background.desktop ? (
const background =
settings.cfg.startpage.background.type === "image" ? (
settings.cfg.startpage.background.desktop ? (
<img
src={settings.cfg.startpage.background.desktop}
alt=""
style={{
width: "100%",
height: "100%",
objectFit: "cover",
overflow: "hidden",
}}
/>
) : null
) : settings.cfg.startpage.background.type === "video" ? (
settings.cfg.startpage.background.video ? (
<YoutubeEmbedIframe
videoUrl={settings.cfg.startpage.background.video}
containerSX={{
width:
settings.cfg.startpageType === "centered"
? "550px"
: settings.cfg.startpageType === "expanded"
? "100vw"
: "100%",
height:
settings.cfg.startpageType === "centered"
? "275px"
: settings.cfg.startpageType === "expanded"
? "100vh"
: "100%",
borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform:
settings.cfg.startpageType === "centered"
? ""
: settings.cfg.startpageType === "expanded"
? "scale(1.5)"
: "scale(2.4)",
},
}}
/>
) : null
) : null;
return (
<Paper
className="settings-preview-draghandle"
sx={{
height: "100vh",
width: "100vw",
background:
settings.cfg.startpageType === "expanded" && !isMobile
? settings.cfg.startpage.position === "left"
? "linear-gradient(90deg,#272626,transparent)"
: settings.cfg.startpage.position === "center"
? "linear-gradient(180deg,transparent,#272626)"
: "linear-gradient(270deg,#272626,transparent)"
: theme.palette.background.default,
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
}}
>
<QuizPreviewLayoutByType
quizHeaderBlock={
<Box p={settings.cfg.startpageType === "standard" ? "" : "16px"}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
mb: "7px",
}}
>
{settings.cfg.startpage.logo && (
<img <img
src={settings.cfg.startpage.logo} src={settings.cfg.startpage.background.desktop}
style={{ alt=""
height: "37px", style={{
maxWidth: "43px", width: "100%",
objectFit: "cover", height: "100%",
}} objectFit: "cover",
alt="" overflow: "hidden",
}}
/> />
)} ) : null
<Typography ) : settings.cfg.startpage.background.type === "video" ? (
sx={{ settings.cfg.startpage.background.video ? (
fontSize: "14px", <YoutubeEmbedIframe
color: settings.cfg.startpageType === "expanded" videoUrl={settings.cfg.startpage.background.video}
&& !isMobile ? "white" : theme.palette.text.primary containerSX={{
}} width:
>{settings.cfg.info.orgname}</Typography> settings.cfg.startpageType === "centered"
</Box> ? "550px"
<Link mb="16px" href={settings.cfg.info.site}> : settings.cfg.startpageType === "expanded"
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}> ? "100vw"
{settings.cfg.info.site} : "100%",
</Typography> height:
</Link> settings.cfg.startpageType === "centered"
</Box> ? "275px"
} : settings.cfg.startpageType === "expanded"
quizMainBlock={ ? "100vh"
<> : "100%",
<Box borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
sx={{ overflow: "hidden",
display: "flex", "& iframe": {
flexDirection: "column", width: "100%",
justifyContent: "center", height: "100%",
alignItems: transform:
settings.cfg.startpageType === "centered" settings.cfg.startpageType === "centered"
? "center" ? ""
: settings.cfg.startpageType === "expanded" : settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "center" ? "scale(1.5)"
? "center" : "scale(2.4)",
: "start" },
: "start", }}
mt: "28px", />
width: "100%", ) : null
}} ) : null;
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" ? "center" : "-moz-initial",
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
}}
>
{settings.name}
</Typography>
<Typography
sx={{
fontSize: "16px",
m: "16px 0",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" ? "center" : "-moz-initial",
}}
>
{settings.cfg.startpage.description}
</Typography>
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
width: settings.cfg.startpageType === "standard" ? "100%" : "auto",
}}
onClick={() => setVisualStartPage(false)}
>
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box return (
sx={{ <Paper
mt: "46px", className="settings-preview-draghandle"
display: "flex", sx={{
alignItems: "center", height: "100vh",
justifyContent: "space-between", width: "100vw",
width: "100%", background:
flexDirection: isMobile ? "column" : "row" settings.cfg.startpageType === "expanded" && !isMobile
}} ? settings.cfg.startpage.position === "left"
> ? "linear-gradient(90deg,#272626,transparent)"
<Box sx={{ maxWidth: "300px" }}> : settings.cfg.startpage.position === "center"
{settings.cfg.info.clickable ? ( ? "linear-gradient(180deg,transparent,#272626)"
isMobileDevice ? ( : "linear-gradient(270deg,#272626,transparent)"
<Link href={`tel:${settings.cfg.info.phonenumber}`}> : theme.palette.background.default,
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
)}
<Typography sx={{ width: "100%",
overflowWrap: "break-word",
fontSize: "12px", textAlign: "end",
color:
settings.cfg.startpageType === "expanded" && !isMobile
? "white"
: theme.palette.text.primary,
}}>
{settings.cfg.info.law}
</Typography>
</Box>
<Box color: settings.cfg.startpageType === "expanded" ? "white" : "black",
sx={{ }}
display: "flex", >
alignItems: "center", <QuizPreviewLayoutByType
gap: "15px" quizHeaderBlock={
}} <Box p={settings.cfg.startpageType === "standard" ? "" : "16px"}>
> <Box
<NameplateLogo style={{ fontSize: "34px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#FFFFFF" : (mode[settings.cfg.theme] ? "#151515" : "#FFFFFF") }} /> sx={{
<Typography sx={{ fontSize: "20px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#F5F7FF" : (mode[settings.cfg.theme] ? "#4D4D4D" : "#F5F7FF"), whiteSpace: "nowrap", }}> display: "flex",
Сделано на PenaQuiz alignItems: "center",
</Typography> gap: "20px",
</Box> mb: "7px",
</Box> }}
</> >
} {settings.cfg.startpage.logo && (
backgroundBlock={background} <img
startpageType={settings.cfg.startpageType} src={settings.cfg.startpage.logo}
alignType={settings.cfg.startpage.position} style={{
/> height: "37px",
</Paper> maxWidth: "43px",
); objectFit: "cover",
}}
alt=""
/>
)}
<Typography
sx={{
fontSize: "14px",
color: settings.cfg.startpageType === "expanded"
&& !isMobile ? "white" : theme.palette.text.primary
}}
>{settings.cfg.info.orgname}</Typography>
</Box>
<Link mb="16px" href={settings.cfg.info.site}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.site}
</Typography>
</Link>
</Box>
}
quizMainBlock={
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems:
settings.cfg.startpageType === "centered"
? "center"
: settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "center"
? "center"
: "start"
: "start",
mt: "28px",
width: "100%",
}}
>
<Typography
sx={{
fontWeight: "bold",
fontSize: "26px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: "1.2",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" ? "center" : "-moz-initial",
color: settings.cfg.startpageType === "expanded" && !isMobile ? "white" : theme.palette.text.primary
}}
>
{settings.name}
</Typography>
<Typography
sx={{
fontSize: "16px",
m: "16px 0",
overflowWrap: "break-word",
width: "100%",
textAlign: settings.cfg.startpageType === "centered" ? "center" : "-moz-initial",
}}
>
{settings.cfg.startpage.description}
</Typography>
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
sx={{
fontSize: "16px",
padding: "10px 15px",
width: settings.cfg.startpageType === "standard" ? "100%" : "auto",
}}
onClick={() => setVisualStartPage(false)}
>
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
mt: "46px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
flexDirection: isMobile ? "column" : "row"
}}
>
<Box sx={{ maxWidth: "300px" }}>
{settings.cfg.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase onClick={handleCopyNumber}>
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography sx={{ fontSize: "16px", color: theme.palette.primary.main }}>
{settings.cfg.info.phonenumber}
</Typography>
)}
<Typography sx={{
width: "100%",
overflowWrap: "break-word",
fontSize: "12px", textAlign: "end",
color:
settings.cfg.startpageType === "expanded" && !isMobile
? "white"
: theme.palette.text.primary,
}}>
{settings.cfg.info.law}
</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "15px"
}}
>
<NameplateLogo style={{ fontSize: "34px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#FFFFFF" : (quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF") }} />
<Typography sx={{ fontSize: "20px", color: settings.cfg.startpageType === "expanded" && !isMobile ? "#F5F7FF" : (quizThemes[settings.cfg.theme].isLight ? "#4D4D4D" : "#F5F7FF"), whiteSpace: "nowrap", }}>
Сделано на PenaQuiz
</Typography>
</Box>
</Box>
</>
}
backgroundBlock={background}
startpageType={settings.cfg.startpageType}
alignType={settings.cfg.startpage.position}
/>
</Paper>
);
}; };
function QuizPreviewLayoutByType({ function QuizPreviewLayoutByType({
quizHeaderBlock, quizHeaderBlock,
quizMainBlock, quizMainBlock,
backgroundBlock, backgroundBlock,
startpageType, startpageType,
alignType, alignType,
}: { }: {
quizHeaderBlock: JSX.Element; quizHeaderBlock: JSX.Element;
quizMainBlock: JSX.Element; quizMainBlock: JSX.Element;
backgroundBlock: JSX.Element | null; backgroundBlock: JSX.Element | null;
startpageType: QuizStartpageType; startpageType: QuizStartpageType;
alignType: QuizStartpageAlignType; alignType: QuizStartpageAlignType;
}) { }) {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
function StartPageMobile() { function StartPageMobile() {
return ( return (
<Box
sx={{
display: "flex",
flexDirection: "column-reverse",
flexGrow: 1,
justifyContent: "flex-end",
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: "80%"
}}
>
{quizHeaderBlock}
<Box
sx={{
height: "80%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%"
}}
>
{quizMainBlock}
</Box>
</Box>
<Box
sx={{
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)
}
switch (startpageType) {
case null:
case "standard": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box <Box
sx={{
display: "flex",
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
flexGrow: 1,
justifyContent: isMobile ? "flex-end" : undefined,
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{ sx={{
width: isMobile ? "100%" : "40%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: isMobile ? "80%" : undefined
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
width: isMobile ? "100%" : "60%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)}
</>
);
}
case "expanded": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "40%",
position: "relative",
padding: "16px",
zIndex: 3,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
position: "absolute",
zIndex: -1,
left: 0,
top: 0,
height: "100%",
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)
}
</>
);
}
case "centered": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
overflow: "hidden",
}}
>
{quizHeaderBlock}
{backgroundBlock && (
<Box
sx={{
width: "60%",
overflow: "hidden",
display: "flex", display: "flex",
justifyContent: "center" flexDirection: "column-reverse",
}} flexGrow: 1,
justifyContent: "flex-end",
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: "80%"
}}
> >
{backgroundBlock} {quizHeaderBlock}
<Box
sx={{
height: "80%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%"
}}
>
{quizMainBlock}
</Box>
</Box>
<Box
sx={{
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box> </Box>
)}
{quizMainBlock}
</Box> </Box>
) );
} }
</>
);
switch (startpageType) {
case null:
case "standard": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
display: "flex",
flexDirection: alignType === "left" ? (isMobile ? "column-reverse" : "row") : "row-reverse",
flexGrow: 1,
justifyContent: isMobile ? "flex-end" : undefined,
height: "100vh",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: isMobile ? "100%" : "40%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "flex-start",
p: "25px",
height: isMobile ? "80%" : undefined
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
width: isMobile ? "100%" : "60%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)}
</>
);
}
case "expanded": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
position: "relative",
display: "flex",
justifyContent: startpageAlignTypeToJustifyContent[alignType],
flexGrow: 1,
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
}}
>
<Box
sx={{
width: "40%",
position: "relative",
padding: "16px",
zIndex: 3,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: alignType === "center" ? "center" : "start",
}}
>
{quizHeaderBlock}
{quizMainBlock}
</Box>
<Box
sx={{
position: "absolute",
zIndex: -1,
left: 0,
top: 0,
height: "100%",
width: "100%",
overflow: "hidden",
}}
>
{backgroundBlock}
</Box>
</Box>
)
}
</>
);
}
case "centered": {
return (
<>
{isMobile ? (
<StartPageMobile />
) : (
<Box
sx={{
padding: "16px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
height: "100%",
"&::-webkit-scrollbar": { width: 0 },
overflow: "hidden",
}}
>
{quizHeaderBlock}
{backgroundBlock && (
<Box
sx={{
width: "60%",
overflow: "hidden",
display: "flex",
justifyContent: "center"
}}
>
{backgroundBlock}
</Box>
)}
{quizMainBlock}
</Box>
)
}
</>
);
}
default:
notReachable(startpageType);
} }
default:
notReachable(startpageType);
}
} }
const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = { const startpageAlignTypeToJustifyContent: Record<QuizStartpageAlignType, "start" | "center" | "end"> = {
left: "start", left: "start",
center: "center", center: "center",
right: "end", right: "end",
}; };

@ -1,123 +1,84 @@
import { getData } from "@api/quizRelase";
import { QuizSettings } from "@model/settingsData";
import { Box, ThemeProvider } from "@mui/material";
import { setQuizData } from "@stores/quizData/actions";
import { useQuestionsStore } from "@stores/quizData/store";
import LoadingSkeleton from "@ui_kit/LoadingSkeleton";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Box, Skeleton, ThemeProvider } from "@mui/material"; import useSWR from "swr";
import { ApologyPage } from "./ApologyPage";
import { StartPageViewPublication } from "./StartPageViewPublication";
import { Question } from "./Question"; import { Question } from "./Question";
import { ApologyPage } from "./ApologyPage" import { StartPageViewPublication } from "./StartPageViewPublication";
import { useQuestionsStore } from "@stores/quizData/store"
import { getData } from "@api/quizRelase"
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useGetSettings } from "../../utils/hooks/useGetSettings";
import { themesPublication } from "../../utils/themes/Publication/themePublication";
import { replaceSpacesToEmptyLines } from "./tools/replaceSpacesToEmptyLines"; import { replaceSpacesToEmptyLines } from "./tools/replaceSpacesToEmptyLines";
import { parseQuizData } from "@model/api/getQuizData";
const QID = const QID =
process.env.NODE_ENV === "production" ? import.meta.env.PROD ?
window.location.pathname.replace(/\//g, '') window.location.pathname.replace(/\//g, '')
: :
"0bed8483-3016-4bca-b8e0-a72c3146f18b" "0bed8483-3016-4bca-b8e0-a72c3146f18b";
export const ViewPage = () => { export const ViewPage = () => {
const { settings, cnt, items } = useQuestionsStore() const { isLoading, error } = useSWR(["quizData", QID], params => getQuizData(params[1]), {
console.log("КВИЗ ", settings) onSuccess: setQuizData,
console.log("ВОПРОСЫ ", items) });
const { settings, items, recentlyСompleted } = useQuestionsStore();
const [visualStartPage, setVisualStartPage] = useState<boolean>();
useEffect(() => {//установка фавиконки
if (!settings) return;
const [visualStartPage, setVisualStartPage] = useState<boolean>(); const link = document.querySelector('link[rel="icon"]');
const [errormessage, setErrormessage] = useState<string>(""); if (link && settings.cfg.startpage.favIcon) {
link.setAttribute("href", settings?.cfg.startpage.favIcon);
useEffect(() => {
async function get() {
try {
let data = await getData(QID)
console.log(data)
//@ts-ignore
const settings = data.settings
console.log(data)
//@ts-ignore
data.settings = {
//@ts-ignore
qid: QID,
fp: settings.fp,
rep: settings.rep,
name: settings.name,
//@ts-ignore
cfg: JSON.parse(data?.settings.cfg),
lim: settings.lim,
due: settings.due,
delay: settings.delay,
pausable: settings.pausable
} }
console.log(data)
//@ts-ignore
data.items = data.items.map((item) => {
const content = JSON.parse(item.c)
return {
description: item.desc,
id: item.id,
page: item.p,
required: item.req,
title: item.title,
type: item.typ,
content
}
}),
console.log(data)
console.log(JSON.stringify({data: data}).replaceAll(/\\\" \\\"/g, '""').replaceAll(/\" \"/g, '""'))
console.log(JSON.parse(JSON.stringify({data: data}).replaceAll(/\\\" \\\"/g, '""').replaceAll(/\" \"/g, '""')).data)
data = replaceSpacesToEmptyLines(data) setVisualStartPage(!settings.cfg.noStartPage);
}, [settings]);
useQuestionsStore.setState(JSON.parse(JSON.stringify({data: data}).replaceAll(/\\\" \\\"/g, '""').replaceAll(/\" \"/g, '""')).data) const questionsCount = items.filter(({ type }) => type !== null && type !== "result").length;
} catch (e) { if (error) {
console.log(error);
//@ts-ignore return <ApologyPage message="Что-то пошло не так" />;
if (e?.response?.status === 423) setErrormessage("квиз не активирован")
}
} }
get() if (isLoading || !settings) return <LoadingSkeleton />;
}, []) if (questionsCount === 0) return <ApologyPage message="Нет созданных вопросов" />;
return (
useEffect(() => {//установка фавиконки <ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"]}>
if (Object.values(settings).length > 0) { {recentlyСompleted ? (
<ApologyPage message="Вы уже прошли этот опрос" />
const link = document.querySelector('link[rel="icon"]'); ) : (
if (link && settings?.cfg.startpage.favIcon) { <Box>
link.setAttribute("href", settings?.cfg.startpage.favIcon); {visualStartPage ? (
} <StartPageViewPublication setVisualStartPage={setVisualStartPage} />
) : (
setVisualStartPage(!settings?.cfg.noStartPage); <Question />
} )}
}, [settings]); </Box>
)}
</ThemeProvider>
const filteredQuestions = ( );
items.filter(({ type }) => type) as AnyTypedQuizQuestion[]
).sort((previousItem, item) => previousItem.page - item.page);
if (errormessage) return <ApologyPage message={errormessage} />
if (visualStartPage === undefined) return <Skeleton sx={{ bgcolor: 'grey', width: "100vw", height: "100vh" }} variant="rectangular" />;
if (cnt === 0 || (cnt === 1 && items[0].type === "result")) return <ApologyPage message="Нет созданных вопросов" />
return (
<ThemeProvider theme={themesPublication?.[settings?.cfg.theme || "StandardTheme"]}>
<Box>
{
visualStartPage ?
<StartPageViewPublication setVisualStartPage={setVisualStartPage} />
:
<Question questions={filteredQuestions} />
}
</Box>
</ThemeProvider>
);
}; };
async function getQuizData(quizId: string) {
const response = await getData(quizId);
const quizDataResponse = response.data;
if (response.error) {
enqueueSnackbar(response.error);
throw new Error(response.error);
}
if (!quizDataResponse) {
throw new Error("Quiz not found");
}
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse, quizId));
return JSON.parse(JSON.stringify({ data: quizSettings }).replaceAll(/\\" \\"/g, '""').replaceAll(/" "/g, '""')).data as QuizSettings & { recentlyСompleted: boolean; };
}

@ -1,7 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DatePicker } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography, useTheme } from "@mui/material"; import { Box, Typography, useTheme } from "@mui/material";
import { modes } from "../../../utils/themes/Publication/themePublication";
import { useQuizViewStore, updateAnswer } from "@stores/quizView/store"; import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
@ -9,6 +8,8 @@ import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon"; import CalendarIcon from "@icons/CalendarIcon";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store"; import { useQuestionsStore } from "@stores/quizData/store";
type DateProps = { type DateProps = {
@ -17,7 +18,6 @@ type DateProps = {
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme(); const theme = useTheme();
const mode = modes;
const { settings } = useQuestionsStore(); const { settings } = useQuestionsStore();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
@ -26,6 +26,8 @@ export const Date = ({ currentQuestion }: DateProps) => {
)?.answer as string; )?.answer as string;
const [day, month, year] = answer?.split(".") || []; const [day, month, year] = answer?.split(".") || [];
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}> <Typography variant="h5" color={theme.palette.text.primary}>
@ -41,7 +43,6 @@ export const Date = ({ currentQuestion }: DateProps) => {
> >
<DatePicker <DatePicker
slots={{ slots={{
//@ts-ignore
openPickerIcon: () => ( openPickerIcon: () => (
<CalendarIcon <CalendarIcon
sx={{ sx={{
@ -72,7 +73,6 @@ export const Date = ({ currentQuestion }: DateProps) => {
day: "2-digit", day: "2-digit",
} }
), ),
//@ts-ignore
qid: settings.qid, qid: settings.qid,
}); });
@ -103,7 +103,7 @@ export const Date = ({ currentQuestion }: DateProps) => {
}} }}
sx={{ sx={{
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
backgroundColor: mode[settings.cfg.theme] backgroundColor: quizThemes[settings.cfg.theme].isLight
? "white" ? "white"
: theme.palette.background.default, : theme.palette.background.default,
borderRadius: "10px", borderRadius: "10px",

@ -1,24 +1,23 @@
import { import {
Box, Box,
Typography, FormControl,
RadioGroup, FormControlLabel,
FormControlLabel, Radio,
Radio, RadioGroup,
useTheme, Typography,
FormControl, useTheme
useMediaQuery
} from "@mui/material"; } from "@mui/material";
import { modes } from "../../../utils/themes/Publication/themePublication";
import { useQuizViewStore, updateAnswer, deleteAnswer } from "@stores/quizView/store"; import { deleteAnswer, updateAnswer, useQuizViewStore } from "@stores/quizView/store";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"
import { enqueueSnackbar } from "notistack";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { useQuestionsStore } from "@stores/quizData/store";
type EmojiProps = { type EmojiProps = {
currentQuestion: QuizQuestionEmoji; currentQuestion: QuizQuestionEmoji;
@ -26,9 +25,7 @@ type EmojiProps = {
export const Emoji = ({ currentQuestion }: EmojiProps) => { export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme(); const theme = useTheme();
const mode = modes;
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { settings } = useQuestionsStore() const { settings } = useQuestionsStore()
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const { answer } = const { answer } =
@ -36,6 +33,8 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
@ -45,7 +44,6 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
({ id }) => answer === id ({ id }) => answer === id
)} )}
onChange={({ target }) =>{ onChange={({ target }) =>{
console.log(currentQuestion.content.variants[Number(target.value)])
updateAnswer( updateAnswer(
currentQuestion.id, currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer currentQuestion.content.variants[Number(target.value)].answer
@ -114,7 +112,6 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -136,7 +133,6 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -147,7 +143,6 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
}} }}
control={ control={
//@ts-ignore
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} /> <Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
} }
label={ label={

@ -23,7 +23,7 @@ type FileProps = {
currentQuestion: QuizQuestionFile; currentQuestion: QuizQuestionFile;
}; };
export const UPLOAD_FILE_DESCRIPTIONS_MAP: Record< const UPLOAD_FILE_DESCRIPTIONS_MAP: Record<
UploadFileType, UploadFileType,
{ title: string; description: string } { title: string; description: string }
> = { > = {
@ -49,6 +49,8 @@ export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => { const uploadFile = async ({ target }: ChangeEvent<HTMLInputElement>) => {
if (!settings) return;
const file = target.files?.[0]; const file = target.files?.[0];
if (file) { if (file) {
@ -60,7 +62,6 @@ export const File = ({ currentQuestion }: FileProps) => {
file: `${file.name}|${URL.createObjectURL(file)}`, file: `${file.name}|${URL.createObjectURL(file)}`,
name: file.name name: file.name
}, },
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })

@ -32,6 +32,8 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(500)); const isMobile = useMediaQuery(theme.breakpoints.down(500));
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
@ -77,7 +79,6 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -98,7 +99,6 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -138,7 +138,6 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
}} }}
value={index} value={index}
control={ control={
//@ts-ignore
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} /> <Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
} }
label={variant.answer} label={variant.answer}

@ -10,218 +10,221 @@ import { useQuizViewStore, updateAnswer } from "@stores/quizView/store";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number"; import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"
import { modes } from "../../../utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuestionsStore } from "@stores/quizData/store";
type NumberProps = { type NumberProps = {
currentQuestion: QuizQuestionNumber; currentQuestion: QuizQuestionNumber;
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuestionsStore()
const [minRange, setMinRange] = useState<string>("0"); const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000"); const [maxRange, setMaxRange] = useState<string>("100000000000");
const theme = useTheme(); const theme = useTheme();
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const updateMinRangeDebounced = useDebouncedCallback(async (value, crowded = false) => { const updateMinRangeDebounced = useDebouncedCallback(async (value, crowded = false) => {
if (crowded) { if (!settings) return null;
setMinRange(maxRange);
} if (crowded) {
setMinRange(maxRange);
}
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: value, body: value,
//@ts-ignore qid: settings.qid
qid: settings.qid })
})
updateAnswer(currentQuestion.id, value); updateAnswer(currentQuestion.id, value);
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан")
} }
}, 1000); }, 1000);
const updateMaxRangeDebounced = useDebouncedCallback(async (value, crowded = false) => { const updateMaxRangeDebounced = useDebouncedCallback(async (value, crowded = false) => {
if (crowded) { if (!settings) return null;
setMaxRange(minRange);
} if (crowded) {
try { setMaxRange(minRange);
}
try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: value, body: value,
//@ts-ignore qid: settings.qid
qid: settings.qid })
})
updateAnswer(currentQuestion.id, value); updateAnswer(currentQuestion.id, value);
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан")
} }
}, 1000); }, 1000);
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const mode = modes; const min = window.Number(currentQuestion.content.range.split("—")[0]);
const min = window.Number(currentQuestion.content.range.split("—")[0]); const max = window.Number(currentQuestion.content.range.split("—")[1]);
const max = window.Number(currentQuestion.content.range.split("—")[1]); const sliderValue = answer || currentQuestion.content.start + "—" + max;
const sliderValue = answer || currentQuestion.content.start + "—" + max;
useEffect(() => { useEffect(() => {
if (answer) { if (answer) {
setMinRange(answer.split("—")[0]); setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]); setMaxRange(answer.split("—")[1]);
} }
if (!answer) { if (!answer) {
setMinRange(String(currentQuestion.content.start)); setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max)); setMaxRange(String(max));
} }
}, []); }, []);
return ( if (!settings) throw new Error("settings is null");
<Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> return (
<Box <Box>
sx={{ <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
display: "flex", <Box
flexDirection: "column", sx={{
width: "100%", display: "flex",
marginTop: "20px", flexDirection: "column",
gap: "30px", width: "100%",
paddingRight: isMobile ? "10px" : undefined marginTop: "20px",
}} gap: "30px",
> paddingRight: isMobile ? "10px" : undefined
<CustomSlider }}
value={ >
currentQuestion.content.chooseRange <CustomSlider
? sliderValue.split("—").length || 0 > 1 value={
? sliderValue.split("—").map((item) => window.Number(item)) currentQuestion.content.chooseRange
: [min, min + 1] ? sliderValue.split("—").length || 0 > 1
: window.Number(sliderValue.split("—")[0]) ? sliderValue.split("—").map((item) => window.Number(item))
} : [min, min + 1]
min={min} : window.Number(sliderValue.split("—")[0])
max={max} }
step={currentQuestion.content.step || 1} min={min}
onChange={async (_, value) => { max={max}
step={currentQuestion.content.step || 1}
onChange={async (_, value) => {
const range = String(value).replace(",", "—").replace (/\D/, ''); const range = String(value).replace(",", "—").replace (/\D/, '');
updateAnswer(currentQuestion.id, range); updateAnswer(currentQuestion.id, range);
updateMinRangeDebounced(range, true); updateMinRangeDebounced(range, true);
}} }}
onChangeCommitted={(_, value) => { onChangeCommitted={(_, value) => {
if (currentQuestion.content.chooseRange) { if (currentQuestion.content.chooseRange) {
const range = value as number[]; const range = value as number[];
setMinRange(String(range[0])); setMinRange(String(range[0]));
setMaxRange(String(range[1])); setMaxRange(String(range[1]));
} }
}} }}
//@ts-ignore sx={{
sx={{ color: theme.palette.primary.main,
color: theme.palette.primary.main, "& .MuiSlider-valueLabel": {
"& .MuiSlider-valueLabel": { background: theme.palette.primary.main,
background: theme.palette.primary.main, }
} }}
}} />
/>
{!currentQuestion.content.chooseRange && ( {!currentQuestion.content.chooseRange && (
<CustomTextField <CustomTextField
placeholder="0" placeholder="0"
value={answer} value={answer}
onChange={async ({ target }) => { onChange={async ({ target }) => {
updateMinRangeDebounced(window.Number(target.value.replace (/\D/, '')) > max updateMinRangeDebounced(window.Number(target.value.replace (/\D/, '')) > max
? String(max) ? String(max)
: window.Number(target.value) < min : window.Number(target.value) < min
? String(min) ? String(min)
: target.value, true); : target.value, true);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", textAlign: "center",
backgroundColor: mode[settings.cfg.theme] ? "white" : theme.palette.background.default, backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
}, },
}} }}
/> />
)} )}
{currentQuestion.content.chooseRange && ( {currentQuestion.content.chooseRange && (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "15px", gap: "15px",
alignItems: "center", alignItems: "center",
"& .MuiFormControl-root": { width: "auto" }, "& .MuiFormControl-root": { width: "auto" },
}} }}
> >
<CustomTextField <CustomTextField
placeholder="0" placeholder="0"
value={minRange} value={minRange}
onChange={({ target }) => { onChange={({ target }) => {
setMinRange(target.value.replace (/\D/, '')); setMinRange(target.value.replace (/\D/, ''));
if (window.Number(target.value) >= window.Number(maxRange)) { if (window.Number(target.value) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true); updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
return; return;
} }
updateMinRangeDebounced(`${target.value}${maxRange}`); updateMinRangeDebounced(`${target.value}${maxRange}`);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", textAlign: "center",
backgroundColor: mode[settings.cfg.theme] ? "white" : theme.palette.background.default, backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
}, },
}} }}
/> />
<Typography color={theme.palette.text.primary}>до</Typography> <Typography color={theme.palette.text.primary}>до</Typography>
<CustomTextField <CustomTextField
placeholder="0" placeholder="0"
value={maxRange} value={maxRange}
onChange={({ target }) => { onChange={({ target }) => {
setMaxRange(target.value.replace (/\D/, '')); setMaxRange(target.value.replace (/\D/, ''));
if (window.Number(target.value) <= window.Number(minRange)) { if (window.Number(target.value) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true); updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return; return;
} }
updateMaxRangeDebounced(`${minRange}${target.value}`); updateMaxRangeDebounced(`${minRange}${target.value}`);
}} }}
sx={{ sx={{
maxWidth: "80px", maxWidth: "80px",
borderColor: theme.palette.text.primary, borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": { "& .MuiInputBase-input": {
textAlign: "center", textAlign: "center",
backgroundColor: mode[settings.cfg.theme] ? "white" : theme.palette.background.default, backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
}, },
}} }}
/> />
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Box>
); );
}; };

@ -69,6 +69,8 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
({ name }) => name === currentQuestion.content.form ({ name }) => name === currentQuestion.content.form
); );
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
@ -98,7 +100,6 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: String(value), body: String(value),
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })

@ -23,6 +23,8 @@ export const Select = ({ currentQuestion }: SelectProps) => {
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
) ?? {}; ) ?? {};
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
@ -38,10 +40,7 @@ export const Select = ({ currentQuestion }: SelectProps) => {
placeholder={currentQuestion.content.default} placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1} activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)} items={currentQuestion.content.variants.map(({ answer }) => answer)}
//@ts-ignore
colorMain={theme.palette.primary.main} colorMain={theme.palette.primary.main}
//@ts-ignore
color={theme.palette.primary.main}
onChange={async(_, value) => { onChange={async(_, value) => {
if (value < 0) { if (value < 0) {
deleteAnswer(currentQuestion.id); deleteAnswer(currentQuestion.id);
@ -50,7 +49,6 @@ export const Select = ({ currentQuestion }: SelectProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -65,7 +63,6 @@ export const Select = ({ currentQuestion }: SelectProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: String(currentQuestion.content.variants[Number(value)].answer), body: String(currentQuestion.content.variants[Number(value)].answer),
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })

@ -21,12 +21,13 @@ export const Text = ({ currentQuestion }: TextProps) => {
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const inputHC = useDebouncedCallback(async (text) => { const inputHC = useDebouncedCallback(async (text) => {
if (!settings) return;
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: text, body: text,
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })

@ -1,249 +1,245 @@
import { useEffect } from "react";
import { import {
Box, Box,
Typography, Checkbox,
RadioGroup, FormControlLabel,
FormGroup, FormGroup,
FormControlLabel, Radio,
Radio, RadioGroup,
Checkbox, TextField as MuiTextField,
TextField, Typography,
useTheme, useTheme,
TextFieldProps,
} from "@mui/material"; } from "@mui/material";
import { FC, useEffect } from "react";
import { import {
useQuizViewStore, deleteAnswer,
updateAnswer, updateAnswer,
deleteAnswer, updateOwnVariant,
updateOwnVariant, useQuizViewStore
deleteOwnVariant,
} from "@stores/quizView/store"; } from "@stores/quizView/store";
import { CheckboxIcon } from "@icons/Checkbox";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { CheckboxIcon } from "@icons/Checkbox";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { useQuestionsStore } from "@stores/quizData/store"
import { modes } from "../../../utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { enqueueSnackbar } from "notistack";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import type { QuizQuestionVariant } from "../../../model/questionTypes/variant";
import { useQuestionsStore } from "@stores/quizData/store";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
type VariantProps = { type VariantProps = {
stepNumber: number; currentQuestion: QuizQuestionVariant;
currentQuestion: QuizQuestionVariant;
}; };
type VariantItemProps = { type VariantItemProps = {
currentQuestion: QuizQuestionVariant; currentQuestion: QuizQuestionVariant;
variant: QuestionVariant; variant: QuestionVariant;
answer: string | string[] | undefined; answer: string | string[] | undefined;
index: number; index: number;
own?: boolean; own?: boolean;
}; };
export const Variant = ({ currentQuestion }: VariantProps) => { export const Variant = ({ currentQuestion }: VariantProps) => {
const { settings } = useQuestionsStore() const theme = useTheme();
const { answers, ownVariants } = useQuizViewStore(); const { answers, ownVariants } = useQuizViewStore();
const mode = modes; const { answer } =
const { answer } = answers.find(
answers.find( ({ questionId }) => questionId === currentQuestion.id
({ questionId }) => questionId === currentQuestion.id ) ?? {};
) ?? {}; const ownVariant = ownVariants.find(
const ownVariant = ownVariants.find( (variant) => variant.id === currentQuestion.id
(variant) => variant.id === currentQuestion.id );
);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup; const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
useEffect(() => { useEffect(() => {
if (!ownVariant) { if (!ownVariant) {
updateOwnVariant(currentQuestion.id, ""); updateOwnVariant(currentQuestion.id, "");
} }
}, []); }, []);
const theme = useTheme(); return (
return ( <Box>
<Box> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Box sx={{ display: "flex" }}>
<Box sx={{ display: "flex" }}> <Group
<Group name={currentQuestion.id.toString()}
name={currentQuestion.id} value={currentQuestion.content.variants.findIndex(
value={currentQuestion.content.variants.findIndex( ({ id }) => answer === id
({ id }) => answer === id )}
)} sx={{
sx={{ display: "flex",
display: "flex", flexWrap: "wrap",
flexWrap: "wrap", flexDirection: "row",
flexDirection: "row", justifyContent: "space-between",
justifyContent: "space-between", flexBasis: "100%",
flexBasis: "100%", marginTop: "20px",
marginTop: "20px", }}
}} >
> <Box
<Box sx={{
sx={{ display: "flex",
display: "flex", flexDirection: "row",
flexDirection: "row", flexWrap: "wrap",
flexWrap: "wrap", width: "100%",
width: "100%", gap: "20px",
gap: "20px", }}
}} >
> {currentQuestion.content.variants.map((variant, index) => (
{currentQuestion.content.variants.map((variant, index) => ( <VariantItem
<VariantItem key={variant.id}
key={variant.id} currentQuestion={currentQuestion}
currentQuestion={currentQuestion} variant={variant}
variant={variant} answer={answer}
answer={answer} index={index}
index={index} />
/> ))}
))} {currentQuestion.content.own && ownVariant && (
{currentQuestion.content.own && ownVariant && ( <VariantItem
<VariantItem own
own currentQuestion={currentQuestion}
currentQuestion={currentQuestion} variant={ownVariant.variant}
variant={ownVariant.variant} answer={answer}
answer={answer} index={currentQuestion.content.variants.length + 2}
index={currentQuestion.content.variants.length + 2} />
/> )}
)} </Box>
</Box> </Group>
</Group> {currentQuestion.content.back && currentQuestion.content.back !== " " && (
{currentQuestion.content.back && currentQuestion.content.back !== " " && ( <Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}> <img
<img key={currentQuestion.id}
key={currentQuestion.id} src={currentQuestion.content.back}
src={currentQuestion.content.back} style={{ width: "100%", height: "100%", objectFit: "cover" }}
style={{ width: "100%", height: "100%", objectFit: "cover" }} alt=""
alt="" />
/> </Box>
</Box> )}
)} </Box>
</Box> </Box>
</Box> );
);
}; };
const VariantItem = ({ const VariantItem = ({
currentQuestion, currentQuestion,
variant, variant,
answer, answer,
index, index,
own = false, own = false,
}: VariantItemProps) => { }: VariantItemProps) => {
const { settings } = useQuestionsStore() const { settings } = useQuestionsStore()
const theme = useTheme(); const theme = useTheme();
const mode = modes;
return ( if (!settings) throw new Error("settings is null");
<FormControlLabel
key={variant.id}
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: mode[settings.cfg.theme]
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&.MuiFormControl-root": {
width: "100%",
},
"& .MuiFormControlLabel-label": {
wordBreak: "break-word"
}
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<CheckboxIcon checked color={theme.palette.primary.main} />}
icon={<CheckboxIcon />}
/>
) :
//@ts-ignore
(<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />
)
}
//@ts-ignore
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={async (event) => {
event.preventDefault();
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) { return (
const currentAnswer = typeof answer !== "string" ? answer || [] : []; <FormControlLabel
key={variant.id}
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: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&.MuiFormControl-root": {
width: "100%",
},
"& .MuiFormControlLabel-label": {
wordBreak: "break-word"
}
}}
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 />} />
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={async (event) => {
event.preventDefault();
const variantId = currentQuestion.content.variants[index].id;
try { if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
await sendAnswer({ try {
questionId: currentQuestion.id,
body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
//@ts-ignore
qid: settings.qid
})
updateAnswer( await sendAnswer({
currentQuestion.id, questionId: currentQuestion.id,
currentAnswer.includes(variantId) body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId) ? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId] : [...currentAnswer, variantId],
); qid: settings.qid
})
} catch (e) { updateAnswer(
enqueueSnackbar("ответ не был засчитан") currentQuestion.id,
} currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId]
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан")
}
return; return;
} }
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
//@ts-ignore qid: settings.qid
qid: settings.qid })
})
updateAnswer(currentQuestion.id, variantId); updateAnswer(currentQuestion.id, variantId);
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан")
} }
if (answer === variantId) { if (answer === variantId) {
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
//@ts-ignore qid: settings.qid
qid: settings.qid })
})
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан")
} }
deleteAnswer(currentQuestion.id); deleteAnswer(currentQuestion.id);
} }
}} }}
/> />
); );
}; };

@ -7,7 +7,6 @@ import {
useTheme, useTheme,
useMediaQuery useMediaQuery
} from "@mui/material"; } from "@mui/material";
import { modes } from "../../../utils/themes/Publication/themePublication";
import gag from "./gag.png" import gag from "./gag.png"
@ -20,6 +19,7 @@ import RadioIcon from "@ui_kit/RadioIcon";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase"; import { sendAnswer } from "@api/quizRelase";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type VarimgProps = { type VarimgProps = {
currentQuestion: QuizQuestionVarImg; currentQuestion: QuizQuestionVarImg;
@ -30,7 +30,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
const { answers } = useQuizViewStore(); const { answers } = useQuizViewStore();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650)); const isMobile = useMediaQuery(theme.breakpoints.down(650));
const mode = modes;
const { answer } = const { answer } =
answers.find( answers.find(
({ questionId }) => questionId === currentQuestion.id ({ questionId }) => questionId === currentQuestion.id
@ -38,6 +38,9 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
const variant = currentQuestion.content.variants.find( const variant = currentQuestion.content.variants.find(
({ id }) => answer === id ({ id }) => answer === id
); );
if (!settings) throw new Error("settings is null");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography> <Typography variant="h5" color={theme.palette.text.primary}>{currentQuestion.title}</Typography>
@ -70,7 +73,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
borderRadius: "5px", borderRadius: "5px",
padding: "15px", padding: "15px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
backgroundColor: mode[settings.cfg.theme] ? "white" : theme.palette.background.default, backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
border: `1px solid`, border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF", borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex", display: "flex",
@ -82,15 +85,14 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
value={index} value={index}
onClick={async(event) => { onClick={async(event) => {
event.preventDefault(); event.preventDefault();
try { try {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer, body: currentQuestion.content.variants[index].answer,
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -98,11 +100,11 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
currentQuestion.id, currentQuestion.id,
currentQuestion.content.variants[index].id currentQuestion.content.variants[index].id
); );
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан") enqueueSnackbar("ответ не был засчитан")
} }
if (answer === currentQuestion.content.variants[index].id) { if (answer === currentQuestion.content.variants[index].id) {
try { try {
@ -110,7 +112,6 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
await sendAnswer({ await sendAnswer({
questionId: currentQuestion.id, questionId: currentQuestion.id,
body: "", body: "",
//@ts-ignore
qid: settings.qid qid: settings.qid
}) })
@ -121,7 +122,6 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
} }
}} }}
control={ control={
//@ts-ignore
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} /> <Radio checkedIcon={<RadioCheck color={theme.palette.primary.main}/>} icon={<RadioIcon />} />
} }
label={variant.answer} label={variant.answer}

@ -1,9 +1,12 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useQuestionsStore } from "./store"; import { useQuestionsStore } from "./store";
import { QuizSettings } from "@model/settingsData";
export const getQuestionById = (questionId: string | null):AnyTypedQuizQuestion | null => { export const getQuestionById = (questionId: string | null): AnyTypedQuizQuestion | null => {
if (questionId === null) return null; if (questionId === null) return null;
//@ts-ignore
return useQuestionsStore.getState().items.find(q => q.id === questionId || q.content.id === questionId) || null; return useQuestionsStore.getState().items.find(q => q.id === questionId || q.content.id === questionId) || null;
}; };
export const setQuizData = (quizData: QuizSettings) => useQuestionsStore.setState(quizData);

@ -1,25 +1,29 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { QuizSettings } from "@model/settingsData";
import { Settings, QuestionsStore } from "@model/settingsData";
import { QuizConfig } from "@model/quizSettings";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
const initialState: QuestionsStore = {
//@ts-ignore type QuizDataStore = {
settings: {}, settings: QuizSettings["settings"] | null;
//@ts-ignore items: QuizSettings["items"];
items: [], cnt: QuizSettings["cnt"];
cnt: 0 recentlyСompleted: boolean;
}; };
export const useQuestionsStore = create<QuestionsStore>()( const initialState: QuizDataStore = {
settings: null,
items: [],
cnt: 0,
recentlyСompleted: false,
};
export const useQuestionsStore = create<QuizDataStore>()(
devtools( devtools(
() => initialState, () => initialState,
{ {
name: "QuestionsStore", name: "QuizDataStore",
enabled: process.env.NODE_ENV === "development", enabled: import.meta.env.DEV,
trace: process.env.NODE_ENV === "development", trace: import.meta.env.DEV,
actionsBlacklist: "ignored",
} }
) )
); );

@ -1,95 +1,92 @@
import { QuestionVariant } from "@model/questionTypes/shared";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
type Answer = { type Answer = {
questionId: string; questionId: string;
answer: string | string[]; answer: string | string[];
}; };
type OwnVariant = { type OwnVariant = {
id: string; id: string;
variant: any; variant: QuestionVariant;
}; };
interface QuizViewStore { interface QuizViewStore {
answers: Answer[]; answers: Answer[];
ownVariants: OwnVariant[]; ownVariants: OwnVariant[];
} }
export const useQuizViewStore = create<QuizViewStore>()( export const useQuizViewStore = create<QuizViewStore>()(
devtools( devtools(
(set, get) => ({ (set, get) => ({
answers: [], answers: [],
ownVariants: [], ownVariants: [],
}), }),
{ {
name: "quizView", name: "quizView",
} enabled: import.meta.env.DEV,
) trace: import.meta.env.DEV,
}
)
); );
export const updateAnswer = (questionId: string, answer: string | string[]) => { function setProducedState<A extends string | { type: string; }>(
const answers = [...useQuizViewStore.getState().answers]; recipe: (state: QuizViewStore) => void,
const answerIndex = answers.findIndex( action: A,
(answer) => questionId === answer.questionId ) {
); useQuizViewStore.setState(state => produce(state, recipe), false, action);
if (answerIndex < 0) {
answers.push({ questionId, answer });
} else {
answers[answerIndex] = { questionId, answer };
}
useQuizViewStore.setState({ answers });
};
export const deleteAnswer = (questionId: string) => {
const answers = [...useQuizViewStore.getState().answers];
const filteredItems = answers.filter(
(answer) => questionId !== answer.questionId
);
useQuizViewStore.setState({ answers: filteredItems });
};
export const updateOwnVariant = (id: string, answer: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const ownVariantIndex = ownVariants.findIndex(
(variant) => variant.id === id
);
if (ownVariantIndex < 0) {
ownVariants.push({
id,
variant: {
id: getRandom(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
ownVariants[ownVariantIndex].variant.answer = answer;
}
useQuizViewStore.setState({ ownVariants });
};
export const deleteOwnVariant = (id: string) => {
const ownVariants = [...useQuizViewStore.getState().ownVariants];
const filteredOwnVariants = ownVariants.filter(
(variant) => variant.id !== id
);
useQuizViewStore.setState({ ownVariants: filteredOwnVariants });
};
function getRandom() {
const min = Math.ceil(1000000);
const max = Math.floor(10000000);
return String(Math.floor(Math.random() * (max - min)) + min);
} }
export const updateAnswer = (questionId: string, answer: string | string[]) => 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
});
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
});

@ -1,4 +1,4 @@
import { FormControlLabel, Checkbox, useTheme, Box, useMediaQuery } from "@mui/material"; import { Checkbox, FormControlLabel } from "@mui/material";
import React from "react"; import React from "react";
import { CheckboxIcon } from "@icons/Checkbox"; import { CheckboxIcon } from "@icons/Checkbox";
@ -15,8 +15,6 @@ interface Props {
} }
export default function CustomCheckbox({ label, handleChange, checked, sx, dataCy, colorIcon }: Props) { export default function CustomCheckbox({ label, handleChange, checked, sx, dataCy, colorIcon }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
return ( return (
<FormControlLabel <FormControlLabel
@ -25,7 +23,6 @@ export default function CustomCheckbox({ label, handleChange, checked, sx, dataC
sx={{ padding: "0px 13px 1px 11px" }} sx={{ padding: "0px 13px 1px 11px" }}
disableRipple disableRipple
icon={<CheckboxIcon />} icon={<CheckboxIcon />}
//@ts-ignore
checkedIcon={<CheckboxIcon checked color={colorIcon} />} checkedIcon={<CheckboxIcon checked color={colorIcon} />}
onChange={handleChange} onChange={handleChange}
checked={checked} checked={checked}

@ -0,0 +1,17 @@
import { Skeleton } from "@mui/material";
export default function LoadingSkeleton() {
return (
<Skeleton
component="div"
variant="rectangular"
sx={{
bgcolor: "grey",
width: "100%",
height: "100%",
}}
/>
);
}

@ -1,7 +1,6 @@
import { createTheme } from "@mui/material"; import { QuizTheme } from "@model/settingsData";
import { Theme, createTheme } from "@mui/material";
import themePublic from "./genericPublication"; import themePublic from "./genericPublication";
import theme from "../generic";
const StandardTheme = createTheme({ const StandardTheme = createTheme({
@ -224,30 +223,16 @@ const BlueDarkTheme = createTheme({
} }
}) })
export const modes = { export const quizThemes: Record<QuizTheme, { theme: Theme; isLight: boolean; }> = {
StandardTheme: true, StandardTheme: { theme: StandardTheme, isLight: true },
StandardDarkTheme: false, StandardDarkTheme: { theme: StandardDarkTheme, isLight: false },
PinkTheme: true, PinkTheme: { theme: PinkTheme, isLight: true },
PinkDarkTheme: false, PinkDarkTheme: { theme: PinkDarkTheme, isLight: false },
BlackWhiteTheme: true, BlackWhiteTheme: { theme: BlackWhiteTheme, isLight: true },
OliveTheme: true, OliveTheme: { theme: OliveTheme, isLight: true },
YellowTheme: true, YellowTheme: { theme: YellowTheme, isLight: true },
GoldDarkTheme: false, GoldDarkTheme: { theme: GoldDarkTheme, isLight: false },
PurpleTheme: true, PurpleTheme: { theme: PurpleTheme, isLight: true },
BlueTheme: true, BlueTheme: { theme: BlueTheme, isLight: true },
BlueDarkTheme: false BlueDarkTheme: { theme: BlueDarkTheme, isLight: false },
} };
export const themesPublication = {
StandardTheme,
StandardDarkTheme,
PinkTheme,
PinkDarkTheme,
BlackWhiteTheme,
OliveTheme,
YellowTheme,
GoldDarkTheme,
PurpleTheme,
BlueTheme,
BlueDarkTheme,
}