обрезано

This commit is contained in:
Nastya 2025-07-11 18:19:06 +03:00
parent e5d5119628
commit c6d8cc3e68
55 changed files with 90 additions and 53384 deletions

@ -32,19 +32,14 @@ export function useQuizData(quizId: string, preview: boolean = false) {
needConfig: true, needConfig: true,
}); });
//firstData.settings.status = "ai"; //firstData.settings.status = "ai";
console.log("useQuizData: firstData received:", firstData);
console.log("useQuizData: firstData.settings:", firstData.settings);
initDataManager({ initDataManager({
status: firstData.settings.status, status: firstData.settings.status,
haveRoot: firstData.settings.cfg.haveRoot, haveRoot: firstData.settings.cfg.haveRoot,
}); });
console.log("useQuizData: calling setQuizData with firstData");
setQuizData(firstData); setQuizData(firstData);
// Определяем нужно ли загружать все данные // Определяем нужно ли загружать все данные
console.log("Определяем нужно ли загружать все данные");
console.log(firstData.settings.status);
if (!["ai"].includes(firstData.settings.status)) { if (!["ai"].includes(firstData.settings.status)) {
setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа
return firstData; return firstData;
@ -74,15 +69,10 @@ export function useQuizData(quizId: string, preview: boolean = false) {
limit: 1, limit: 1,
needConfig: false, needConfig: false,
}); });
console.log(
"AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE "
);
console.log(data);
addQuestions(data.questions); addQuestions(data.questions);
changeNextLoading(false); changeNextLoading(false);
return data; return data;
} catch (p) { } catch (p) {
console.log(p);
setPage(questions.length); setPage(questions.length);
changeNextLoading(false); changeNextLoading(false);
} }

@ -168,10 +168,7 @@ export async function getAndParceData(props: GetDataProps) {
} }
//Парсим строки в строках //Парсим строки в строках
console.log("до парса_______________________");
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse)); const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse));
console.log("после парса_______________________");
console.log(quizSettings);
//Единоразово стрингифаим ВСЁ распаршенное и удаляем лишние пробелы //Единоразово стрингифаим ВСЁ распаршенное и удаляем лишние пробелы
const res = JSON.parse( const res = JSON.parse(
JSON.stringify({ data: quizSettings }) JSON.stringify({ data: quizSettings })

@ -55,13 +55,6 @@ function QuizAnswererInner({
addquizid(quizId); addquizid(quizId);
}, []); }, []);
useEffect(() => {
console.log(settings);
console.log(questions);
console.log("r");
console.log(r);
}, [questions, settings]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
vkMetrics.quizOpened(); vkMetrics.quizOpened();
@ -72,7 +65,6 @@ function QuizAnswererInner({
useEffect(() => { useEffect(() => {
//Хук на случай если данные переданы нам сразу, а не "нам нужно их запросить" //Хук на случай если данные переданы нам сразу, а не "нам нужно их запросить"
if (quizSettings !== undefined) { if (quizSettings !== undefined) {
console.log("QuizAnswerer: calling setQuizData with quizSettings");
setQuizData(quizSettings); setQuizData(quizSettings);
initDataManager({ initDataManager({
status: quizSettings.settings.status, status: quizSettings.settings.status,
@ -98,11 +90,7 @@ function QuizAnswererInner({
}; };
}, []); }, []);
console.log("settings");
console.log(settings);
if (isLoading && !questions.length) return <LoadingSkeleton />; if (isLoading && !questions.length) return <LoadingSkeleton />;
console.log("error");
console.log(error);
if (error) return <ApologyPage error={error} />; if (error) return <ApologyPage error={error} />;
if (Object.keys(settings).length == 0) return <ApologyPage error={new Error("quiz data is null")} />; if (Object.keys(settings).length == 0) return <ApologyPage error={new Error("quiz data is null")} />;

@ -6,11 +6,7 @@ type Props = Partial<FallbackProps>;
export const ApologyPage = ({ error }: Props) => { export const ApologyPage = ({ error }: Props) => {
let message = error.message || error.response?.data || " "; let message = error.message || error.response?.data || " ";
console.log("message");
console.log(message.toLowerCase());
const { t } = useTranslation(); const { t } = useTranslation();
console.log("t");
console.log(t(message.toLowerCase()));
return ( return (
<Box <Box

@ -1,351 +0,0 @@
import { useEffect, useRef, useState } from "react";
import { Box, Button, Link, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { Inputs } from "@/components/ViewPublicationPage/ContactForm/Inputs/Inputs";
import { ContactTextBlock } from "./ContactTextBlock";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { sendFC, SendFCParams } from "@api/quizRelase";
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
import { EMAIL_REGEXP } from "@utils/emailRegexp";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { DESIGN_LIST } from "@utils/designList";
import { NameplateLogo } from "@icons/NameplateLogo";
import type { FormContactFieldData, FormContactFieldName } from "@model/settingsData";
import type { QuizQuestionResult } from "@model/questionTypes/result";
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { isProduction } from "@/utils/defineDomain";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
type Props = {
currentQuestion: AnyTypedQuizQuestion;
onShowResult: () => void;
};
//Костыль для особого квиза. Для него не нужно показывать email адрес
const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8");
export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const theme = useTheme();
const { settings, questions, quizId, show_badge, preview } = useQuizStore();
const [ready, setReady] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [text, setText] = useState("");
const [adress, setAdress] = useState("");
const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight);
const fireOnce = useRef(true);
const [fire, setFire] = useState(false);
const isMobile = useRootContainerSize() < 850;
const isTablet = useRootContainerSize() < 1000;
const { t } = useTranslation();
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
useEffect(() => {
function handleResize() {
setScreenHeight(window.innerHeight);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
const resultQuestion =
currentQuestion.type === "result"
? currentQuestion
: questions.find((question): question is QuizQuestionResult => {
if (settings?.cfg.haveRoot) {
return question.type === "result" && question.content.rule.parentId === currentQuestion.content.id;
} else {
return question.type === "result" && question.content.rule.parentId === "line";
}
});
if (!resultQuestion) throw new Error("Result question not found");
const inputHC = async () => {
const FC = settings.cfg.formContact.fields || settings.cfg.formContact;
const body: SendFCParams["body"] = {};
if (name.length > 0) body.name = name;
if (email.length > 0) body.email = email;
if (phone.length > 0) body.phone = phone;
if (adress.length > 0) body.address = adress;
if (text.length > 0) body.customs = { [FC.text.text || t("Last name")]: text };
if (Object.keys(body).length > 0) {
try {
await sendFC({
questionId: currentQuestion.id,
body: body,
qid: quizId,
preview,
});
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) {
enqueueSnackbar(t("The answer was not counted"));
}
}
};
const FCcopy: Record<FormContactFieldName, FormContactFieldData> =
settings.cfg.formContact.fields || settings.cfg.formContact;
const filteredFC: Partial<Record<FormContactFieldName, FormContactFieldData>> = {};
for (const i in FCcopy) {
const field = FCcopy[i as keyof typeof FCcopy];
if (field.used) {
filteredFC[i as FormContactFieldName] = field;
}
}
async function handleShowResultsClick() {
const FC = settings.cfg.formContact.fields;
if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) {
return enqueueSnackbar("Incorrect email entered");
}
if (fireOnce.current) {
if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0)
return enqueueSnackbar(t("Please fill in the fields"));
//почта валидна, хоть одно поле заполнено
setFire(true);
try {
await inputHC();
fireOnce.current = false;
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
sessions[quizId] = Date.now();
localStorage.setItem("sessions", JSON.stringify(sessions));
vkMetrics.contactsFormFilled();
yandexMetrics.contactsFormFilled();
//Оповещаем какие поля были заполнены
if (name.length !== 0) {
vkMetrics.contactsFormField("name");
yandexMetrics.contactsFormField("name");
}
if (email.length !== 0) {
vkMetrics.contactsFormField("email");
yandexMetrics.contactsFormField("email");
}
if (phone.length !== 0) {
vkMetrics.contactsFormField("phone");
yandexMetrics.contactsFormField("phone");
}
if (text.length !== 0) {
vkMetrics.contactsFormField("text");
yandexMetrics.contactsFormField("text");
}
if (adress.length !== 0) {
vkMetrics.contactsFormField("address");
yandexMetrics.contactsFormField("address");
}
} catch (e) {
enqueueSnackbar(t("Please try again later"));
}
if (settings.cfg.resultInfo.showResultForm === "after") {
onShowResult();
}
enqueueSnackbar(t("Data sent successfully"));
}
setFire(false);
}
useEffect(() => {
vkMetrics.contactsFormOpened();
yandexMetrics.contactsFormOpened();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.palette.background.default,
height: screenHeight > 500 ? "100%" : "auto",
overflow: "auto",
"&::-webkit-scrollbar": {
width: "0",
display: "none",
msOverflowStyle: "none",
},
scrollbarWidth: "none",
msOverflowStyle: "none",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage:
settings.cfg.design && !isMobile
? quizThemes[settings.cfg.theme].isLight
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: `linear-gradient(90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%), url(${
DESIGN_LIST[settings.cfg.theme]
})`
: null,
}}
>
<Box
sx={{
width: !isMobile ? "100%" : isMobile ? undefined : "530px",
borderRadius: "4px",
height: isMobile ? "100%" : "auto",
minHeight: "100%",
display: "flex",
flexDirection: isMobile ? "column" : "row",
background: settings.cfg.design && !isMobile ? undefined : theme.palette.background.default,
}}
>
<ContactTextBlock settings={settings} />
<Box
sx={{
flexGrow: isMobile ? 1 : 0,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexDirection: "column",
backgroundColor: theme.palette.background.default,
height: "auto",
}}
>
<Box
sx={{
display: "flex",
alignItems: isMobile ? undefined : "center",
justifyContent: "center",
flexDirection: "column",
p: isMobile ? "0 20px" : isTablet ? "105px 40px 0 60px" : "105px 60px 0 60px",
margin: isMobile ? "0" : "auto 0",
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
mt: isMobile ? "10px" : "20px",
mb: "20px",
}}
>
<Inputs
name={name}
setName={setName}
email={email}
setEmail={setEmail}
phone={phone}
setPhone={setPhone}
text={text}
setText={setText}
adress={adress}
setAdress={setAdress}
crutch={{
disableEmail: isDisableEmail,
}}
/>
</Box>
<Box
sx={{
display: "flex",
width: isMobile ? "300px" : "390px",
}}
>
<CustomCheckbox
label=""
handleChange={({ target }) => {
setReady(target.checked);
}}
checked={ready}
colorIcon={theme.palette.primary.main}
sx={{ marginRight: "0" }}
/>
<Typography
sx={{
color: theme.palette.text.primary,
lineHeight: "18.96px",
}}
fontSize={"16px"}
>
С&ensp;
<Link
href={"https://shub.pena.digital/ppdd"}
target="_blank"
>
{`${t("Regulation on the processing of personal data")} `}
</Link>
&ensp;{t("and")}&ensp;
<Link
href={"https://shub.pena.digital/docs/privacy"}
target="_blank"
>
{" "}
{`${t("Privacy Policy")} `}
</Link>
&ensp;{t("familiarized")}
</Typography>
</Box>
<Button
disabled={!(ready && !fire)}
variant="contained"
onClick={handleShowResultsClick}
sx={{
border: `1px solid ${theme.palette.primary.main}`,
margin: isMobile ? "auto" : undefined,
mt: "20px",
p: "10px 20px",
"&:disabled": {
border: "1px solid #9A9AAF",
color: "#9A9AAF",
},
}}
>
{settings.cfg.formContact?.button || t("Get results")}
</Button>
</Box>
{show_badge && (
<Box
component={Link}
target={"_blank"}
href={`https://${isProduction ? "" : "s"}quiz.pena.digital/answer/v1.0.0/logo?q=${quizId}`}
sx={{
display: "flex",
alignItems: "center",
mt: "55px",
mb: isMobile ? "30px" : isTablet ? "40px" : "50px",
gap: "10px",
textDecoration: "none",
margitTop: "auto",
}}
>
<NameplateLogo
style={{
fontSize: "20px",
color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF",
}}
/>
</Box>
)}
</Box>
</Box>
</Box>
);
};

@ -1,67 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
import { QuizSettingsConfig } from "@model/settingsData.ts";
import { FC } from "react";
import { useTranslation } from "react-i18next";
type ContactTextBlockProps = {
settings: QuizSettingsConfig;
};
export const ContactTextBlock: FC<ContactTextBlockProps> = ({ settings }) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 850;
const isTablet = useRootContainerSize() < 1000;
const { t } = useTranslation();
return (
<Box
sx={{
flexGrow: isMobile ? 0 : 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderRight: isMobile ? undefined : "1px solid #9A9AAF80",
margin: isMobile ? 0 : "40px 0",
padding: isMobile ? "0" : "0 40px",
}}
>
<Box
sx={{
maxWidth: isMobile ? "100%" : isTablet ? "410px" : "630px",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
padding: isMobile ? "40px 20px 0 20px" : "0",
mt: isMobile ? 0 : isTablet ? "-180px" : "-47px",
}}
>
<Typography
sx={{
textAlign: isTablet ? undefined : "center",
fontSize: "24px",
lineHeight: "normal",
fontWeight: 501,
color: theme.palette.text.primary,
wordBreak: "break-word",
}}
>
{settings.cfg.formContact.title || t("Fill out the form to receive your test results")}
</Typography>
{settings.cfg.formContact.desc && (
<Typography
sx={{
color: theme.palette.text.primary,
m: "20px 0",
fontSize: "18px",
wordBreak: "break-word",
}}
>
{settings.cfg.formContact.desc}
</Typography>
)}
</Box>
</Box>
);
};

@ -1,66 +0,0 @@
import { MenuItem, Select, SelectChangeEvent, useTheme } from "@mui/material";
import { Dispatch, FC, SetStateAction, useState } from "react";
import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx";
import { Value } from "react-phone-number-input";
type CountrySelectorProps = {
setMask: Dispatch<SetStateAction<string>>;
};
export const CountrySelector: FC<CountrySelectorProps> = ({ setMask }) => {
const theme = useTheme();
const [country, setCountry] = useState("RU");
const handleChange = (e: SelectChangeEvent<Value>) => {
setCountry(e.target.value);
setMask(phoneMasksByCountry[e.target.value][1]);
};
return (
<Select
//@ts-ignore
value={country}
onChange={handleChange}
renderValue={(value) => value}
// autoComplete={true}
MenuProps={{
PaperProps: {
style: {
backgroundColor: theme.palette.background.default,
borderRadius: "12px",
scrollbarWidth: "none",
},
},
}}
sx={{
minWidth: 50,
backgroundColor: theme.palette.background.default,
"& .MuiSelect-select": {
paddingLeft: "5px",
paddingRight: "5px",
color: "gray",
fontSize: "12px",
border: "none",
},
"& .MuiOutlinedInput-notchedOutline": {
border: "none",
},
"&:hover .MuiOutlinedInput-notchedOutline": {
border: "none",
},
"&:hover:before": {
border: "none",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
border: "none",
},
"&.Mui-focused:hover .MuiOutlinedInput-notchedOutline": {
border: "none",
},
}}
>
{Object.keys(phoneMasksByCountry).map((countryCode) => {
return <MenuItem value={countryCode}>{phoneMasksByCountry[countryCode][0]}</MenuItem>;
})}
</Select>
);
};

@ -1,99 +0,0 @@
import { Box, InputAdornment, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
import { useIMask, IMask } from "react-imask";
import { quizThemes } from "@utils/themes/Publication/themePublication.ts";
import { ChangeEvent, FC, HTMLInputTypeAttribute, useEffect, useState } from "react";
import { CountrySelector } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx";
import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx";
import { useQuizStore } from "@/stores/useQuizStore";
type InputProps = {
title: string;
desc: string;
Icon: FC<{ color: string; backgroundColor: string }>;
onChange: TextFieldProps["onChange"];
onChangePhone?: (phone: string) => void;
id: string;
isPhone?: boolean;
type?: HTMLInputTypeAttribute;
value?: string;
};
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
let first = true;
function phoneChange(e: ChangeEvent<HTMLInputElement>, mask: string) {
const masked = IMask.createMask({
mask: "+7 (000) 000-00-00",
// ...and other options
});
masked.value = e.target.value;
const a = IMask.pipe(e.target.value, {
mask,
});
return a || "";
}
export const CustomInput = ({ title, desc, Icon, onChange, onChangePhone, isPhone, type, value }: InputProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 600;
const { settings } = useQuizStore();
const [mask, setMask] = useState(phoneMasksByCountry["RU"][1]);
// const { ref } = useIMask({ mask });
return (
<Box m="10px 0">
<Typography
mb="7px"
color={theme.palette.text.primary}
fontSize={"16px"}
>
{title}
</Typography>
<TextField
// inputRef={isPhone ? ref : null}
//@ts-ignore
onChange={(e: ChangeEvent<HTMLInputElement>) =>
isPhone ? onChangePhone?.(phoneChange(e, mask)) : onChange?.(e)
}
type={isPhone ? "tel" : type}
value={value}
sx={{
width: isMobile ? "100%" : "390px",
backgroundColor: theme.palette.background.default,
fontSize: "16px",
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF80",
borderRadius: "12px",
},
"& .MuiInputBase-root": {
paddingLeft: 0,
},
"& .MuiOutlinedInput-input": {
paddingLeft: "10px",
},
"& .MuiOutlinedInput-root": {
"&:hover fieldset": {
borderColor: theme.palette.primary.main,
},
},
}}
placeholder={desc}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Icon
color="gray"
backgroundColor={quizThemes[settings.cfg.theme].isLight ? "#F2F3F7" : "#F2F3F71A"}
/>
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">{isPhone && <CountrySelector setMask={setMask} />}</InputAdornment>
),
}}
/>
</Box>
);
};

@ -1,126 +0,0 @@
import NameIcon from "@icons/ContactFormIcon/NameIcon.tsx";
import EmailIcon from "@icons/ContactFormIcon/EmailIcon.tsx";
import TextIcon from "@icons/ContactFormIcon/TextIcon.tsx";
import AddressIcon from "@icons/ContactFormIcon/AddressIcon.tsx";
import { Dispatch, SetStateAction } from "react";
import { CustomInput } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx";
import PhoneIcon from "@icons/ContactFormIcon/PhoneIcon.tsx";
import PhoneInput from "react-phone-number-input";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
type InputsProps = {
name: string;
setName: Dispatch<SetStateAction<string>>;
email: string;
setEmail: Dispatch<SetStateAction<string>>;
phone: string;
setPhone: Dispatch<SetStateAction<string>>;
text: string;
setText: Dispatch<SetStateAction<string>>;
adress: string;
setAdress: Dispatch<SetStateAction<string>>;
crutch: {
disableEmail: boolean;
};
};
const iscrutch = "/cc006b40-ccbd-4600-a1d3-f902f85aa0a0";
const pathOnly = window.location.pathname;
export const Inputs = ({
name,
setName,
email,
setEmail,
phone,
setPhone,
text,
setText,
adress,
setAdress,
crutch,
}: InputsProps) => {
const { settings } = useQuizStore();
const { t } = useTranslation();
const FC = settings.cfg.formContact.fields;
if (!FC) return null;
const Name = (
<CustomInput
onChange={({ target }) => setName(target.value)}
id={name}
title={
pathOnly === iscrutch
? "Введите имя и фамилию"
: FC["name"].innerText || `${t("Enter")} ${t("Name").toLowerCase()}`
}
desc={FC["name"].text || t("Name")}
Icon={NameIcon}
/>
);
const Email = (
<CustomInput
onChange={({ target }) => {
setEmail(target.value.replaceAll(/\s/g, ""));
}}
id={email}
title={FC["email"].innerText || `${t("Enter")} Email`}
desc={FC["email"].text || "Email"}
Icon={EmailIcon}
type="email"
/>
);
const Phone = (
<CustomInput
onChange={({ target }) => setText(target.value)}
onChangePhone={(phone: string) => {
setPhone(phone);
}}
value={phone}
id={phone}
title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`}
desc={FC["phone"].text || t("Phone number")}
Icon={PhoneIcon}
isPhone={true}
/>
);
const Text = (
<CustomInput
onChange={({ target }) => setText(target.value)}
id={text}
title={FC["text"].text || `${t("Enter")} ${t("Last name").toLowerCase()}`}
desc={FC["text"].innerText || t("Last name")}
Icon={TextIcon}
/>
);
const Adress = (
<CustomInput
onChange={({ target }) => setAdress(target.value)}
id={adress}
title={FC["address"].innerText || `${t("Enter")} ${t("Address").toLowerCase()}`}
desc={FC["address"].text || t("Address")}
Icon={AddressIcon}
/>
);
if (Object.values(FC).some((data) => data.used)) {
return (
<>
{FC["name"].used ? Name : <></>}
{FC["email"].used && !crutch.disableEmail ? Email : <></>}
{FC["phone"].used ? Phone : <></>}
{FC["text"].used ? Text : <></>}
{FC["address"].used ? Adress : <></>}
</>
);
} else {
return (
<>
{Name}
{Email}
{Phone}
</>
);
}
};

@ -1,144 +0,0 @@
import { IncorrectAnswer } from "@/assets/icons/IncorrectAnswer";
import { CorrectAnswer } from "@/assets/icons/CorrectAnswer";
import { Box, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@/stores/quizView";
import { AnyTypedQuizQuestion, QuizQuestionVariant } from "@/index";
import { useTranslation } from "react-i18next";
import { useQuizStore } from "@/stores/useQuizStore";
export const PointSystemResultList = () => {
const theme = useTheme();
const { questions } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers);
const { t } = useTranslation();
const questionsWothoutResult = questions.filter<QuizQuestionVariant>(
(q: AnyTypedQuizQuestion): q is QuizQuestionVariant => q.type === "variant"
);
return questionsWothoutResult.map((currentQuestion) => {
let answerIndex = 0;
let currentVariants = currentQuestion.content.variants;
const currentAnswer = answers.find((a) => a.questionId === currentQuestion.id);
const answeredVariant = currentVariants.find((v, i) => {
if (v.id === currentAnswer?.answer) {
answerIndex = i;
return true;
}
});
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Box
sx={{
display: "inline-flex",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{
display: "inline-flex",
gap: "16px",
}}
>
<Typography
sx={{
color: theme.palette.grey[500],
}}
>
{currentQuestion.page + 1}.
</Typography>
<Typography
sx={{
color: theme.palette.text.primary,
}}
>
{currentQuestion.title || t("Question without a title")}
</Typography>
</Box>
<Typography
sx={{
color: answeredVariant?.points ? theme.palette.primary.main : theme.palette.grey[500],
}}
>
{answeredVariant?.points || "0"}
</Typography>
</Box>
<Box
sx={{
display: "inline-flex",
mt: "15px",
gap: "10px",
}}
>
<Typography
sx={{
color: theme.palette.grey[500],
}}
>
{t("Your answer")}:
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Line
checkTrue={Boolean(answeredVariant?.points)}
text={answeredVariant?.answer}
/>
{/* {Boolean(answeredVariant?.points) ? <CorrectAnswer /> : <IncorrectAnswer />}
<Typography>{answeredVariant?.answer || "не выбрано"}</Typography> */}
{currentVariants.map((v) => {
if (v.id === currentAnswer?.answer) {
return <></>;
} else
return (
<Line
checkTrue={Boolean(v?.points)}
text={v.answer}
/>
);
})}
</Box>
</Box>
</Box>
);
});
};
interface LineProps {
checkTrue: boolean;
text?: string;
}
const Line = ({ checkTrue, text }: LineProps) => {
const theme = useTheme();
return (
<Box
sx={{
display: "inline-flex",
gap: "10px",
mb: "10px",
}}
>
{checkTrue ? <CorrectAnswer /> : <IncorrectAnswer />}
<Typography
sx={{
color: theme.palette.grey[500],
}}
>
{text || "не выбрано"}
</Typography>
</Box>
);
};

@ -1,23 +1,12 @@
import { Box, Link, useTheme } from "@mui/material"; import { Box, Link, useTheme } from "@mui/material";
import { Footer } from "./Footer"; import { Footer } from "./Footer";
import { Date } from "./questions/Date";
import { Emoji } from "./questions/Emoji";
import { File } from "./questions/File";
import { Images } from "./questions/Images";
import { Number } from "./questions/Number";
import { Page } from "./questions/Page";
import { Rating } from "./questions/Rating";
import { Select } from "./questions/Select";
import { Text } from "./questions/Text"; import { Text } from "./questions/Text";
import { Variant } from "./questions/Variant";
import { Varimg } from "./questions/Varimg";
import type { RealTypedQuizQuestion } from "../../model/questionTypes/shared"; import type { RealTypedQuizQuestion } 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 { notReachable } from "@utils/notReachable";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { DESIGN_LIST } from "@/utils/designList"; import { DESIGN_LIST } from "@/utils/designList";
@ -88,9 +77,8 @@ export const Question = ({
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<QuestionByType <Text
key={currentQuestion.id} currentQuestion={currentQuestion}
question={currentQuestion}
stepNumber={currentQuestionStepNumber} stepNumber={currentQuestionStepNumber}
/> />
{show_badge && ( {show_badge && (
@ -133,37 +121,3 @@ export const Question = ({
</Box> </Box>
); );
}; };
function QuestionByType({ question, stepNumber }: { question: RealTypedQuizQuestion; stepNumber: number | null }) {
switch (question.type) {
case "variant":
return <Variant currentQuestion={question} />;
case "images":
return <Images currentQuestion={question} />;
case "varimg":
return <Varimg currentQuestion={question} />;
case "emoji":
return <Emoji currentQuestion={question} />;
case "text":
return (
<Text
currentQuestion={question}
stepNumber={stepNumber}
/>
);
case "select":
return <Select currentQuestion={question} />;
case "date":
return <Date currentQuestion={question} />;
case "number":
return <Number currentQuestion={question} />;
case "file":
return <File currentQuestion={question} />;
case "page":
return <Page currentQuestion={question} />;
case "rating":
return <Rating currentQuestion={question} />;
default:
notReachable(question);
}
}

@ -13,8 +13,6 @@ import { NameplateLogo } from "@icons/NameplateLogo";
import type { QuizQuestionResult } from "@/model/questionTypes/result"; import type { QuizQuestionResult } from "@/model/questionTypes/result";
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe"; import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
import { TextAccordion } from "./tools/TextAccordion";
import { PointSystemResultList } from "./PointSystemResultList";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { sendFC, sendResult } from "@/api/quizRelase"; import { sendFC, sendResult } from "@/api/quizRelase";
import { isProduction } from "@/utils/defineDomain"; import { isProduction } from "@/utils/defineDomain";
@ -240,55 +238,6 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
{resultQuestion.content.text} {resultQuestion.content.text}
</Typography> </Typography>
)} )}
{settings.cfg?.score && (
<>
<Typography
sx={{
color: theme.palette.primary.main,
fontSize: "30px",
m: "30px 0",
fontWeight: 600,
}}
>
{t("Your points")}
</Typography>
<Typography
sx={{
color: theme.palette.primary.main,
fontSize: "30px",
fontWeight: 600,
}}
>
{pointsSum} {t("of")} {questions.filter((e) => e.type != "result").length}
</Typography>
<TextAccordion
headerText={
<Typography
sx={{
color: theme.palette.primary.main,
"&:hover": {
color: theme.palette.primary.dark,
},
}}
>
{t("View answers")}
</Typography>
}
sx={{
mt: "60px",
width: "100%",
}}
>
<Box
sx={{
mt: "25px",
}}
>
<PointSystemResultList />
</Box>
</TextAccordion>
</>
)}
</Box> </Box>
</Box> </Box>
{show_badge && ( {show_badge && (

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

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

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

@ -1,480 +0,0 @@
import { Box, Button, ButtonBase, Link, Paper, Typography, useTheme } from "@mui/material";
import { QuizPreviewLayoutByType } from "./QuizPreviewLayoutByType";
import { useQuizStore } from "@/stores/useQuizStore";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useUADevice } from "@utils/hooks/useUADevice";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { NameplateLogo } from "@icons/NameplateLogo";
import { useQuizViewStore } from "@/stores/quizView";
import { DESIGN_LIST } from "@/utils/designList";
import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals";
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
import { isProduction } from "@/utils/defineDomain";
export const StartPageViewPublication = () => {
const theme = useTheme();
const { settings, show_badge, quizId, questions } = useQuizStore();
const { isMobileDevice } = useUADevice();
const setCurrentQuizStep = useQuizViewStore((state) => state.setCurrentQuizStep);
const size = useRootContainerSize();
const isMobile = size < 700;
const isTablet = size >= 700 && size < 1100;
const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber);
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
const handleCopyNumber = () => {
navigator.clipboard.writeText(settings.cfg.info.phonenumber);
vkMetrics.phoneNumberOpened();
yandexMetrics.phoneNumberOpened();
};
const background =
settings.cfg.startpage.background.type === "image" ? (
<img
src={settings.cfg.startpage.background.desktop || DESIGN_LIST[settings.cfg.theme] || ""}
alt=""
style={{
display: "block",
width: isMobile || settings.cfg.startpageType === "expanded" ? "100%" : undefined,
height: "100%",
minWidth: "100%",
maxHeight: "100%",
objectFit: "cover",
overflow: "hidden",
}}
/>
) : settings.cfg.startpage.background.type === "video" ? (
settings.cfg.startpage.background.video ? (
<QuizVideo
videoUrl={settings.cfg.startpage.background.video}
containerSX={{
width: settings.cfg.startpageType === "centered" ? "550px" : "100%",
height: settings.cfg.startpageType === "centered" ? "275px" : "100%",
borderRadius: settings.cfg.startpageType === "centered" ? "10px" : "0",
overflow: "hidden",
"& iframe": {
width: "100%",
height: "100%",
transform:
settings.cfg.startpageType === "centered"
? ""
: settings.cfg.startpageType === "expanded"
? "scale(1.5)"
: "scale(2.4)",
},
}}
/>
) : null
) : null;
const quizHeaderBlock = (
<Box
sx={{
margin: settings.cfg.startpageType === "centered" ? "0 auto" : null,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "nowrap"
: "wrap",
gap: isMobile ? "20px" : "30px",
mb:
settings.cfg.startpageType === "centered"
? isMobile
? "20px"
: "25px"
: settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center" && !isMobile
? 0
: "7px",
justifyContent:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center" && isMobile
? "center"
: undefined,
}}
onClick={(event) => event.preventDefault()}
>
{settings.cfg.startpage.logo && (
<img
src={settings.cfg.startpage.logo}
style={{
maxHeight: isMobile ? "30px" : "40px",
maxWidth: isMobile ? "100px" : "110px",
objectFit: "cover",
}}
alt=""
/>
)}
<Typography
sx={{
fontSize: "12px",
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
wordBreak:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "normal"
: "break-word",
}}
>
{settings.cfg.info.orgname}
</Typography>
</Box>
</Box>
);
const PenaBadge = (
<Box
component={Link}
target={"_blank"}
href={`https://${isProduction ? "" : "s"}quiz.pena.digital/answer/v1.0.0/logo?q=${quizId}`}
sx={{
display: "flex",
alignItems: "center",
gap: "7px",
textDecoration: "none",
marginLeft:
settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isTablet &&
!isMobile
? "61px"
: undefined,
}}
>
<NameplateLogo
style={{
fontSize: "23px",
color:
settings.cfg.startpageType === "expanded"
? "#FFFFFF"
: quizThemes[settings.cfg.theme].isLight
? "#151515"
: "#FFFFFF",
}}
/>
</Box>
);
const realQuestionsCount = questions.filter(
(question) => question.type !== null && question.type !== "result"
).length;
const onQuizStart = () => {
setCurrentQuizStep("question");
vkMetrics.firstPageOpened();
yandexMetrics.firstPageOpened();
};
const onSiteClick = () => {
vkMetrics.emailOpened();
yandexMetrics.emailOpened();
setTimeout(() => {
location.href = (
settings.cfg.info.site.includes("https") ? settings.cfg.info.site : `https://${settings.cfg.info.site}`
).replace(/\s+/g, "");
}, 1000);
};
return (
<Paper
className="settings-preview-draghandle"
sx={{
borderRadius: 0,
height: "100%",
width: "100%",
background:
settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "left" || (isMobile && settings.cfg.startpage.position === "right")
? "linear-gradient(90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
: settings.cfg.startpage.position === "center"
? "linear-gradient(0deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
: "linear-gradient(-90deg, rgba(39, 38, 38, 0.95) 7.66%, rgba(42, 42, 46, 0.85) 42.12%, rgba(51, 54, 71, 0.4) 100%)"
: theme.palette.background.default,
color: settings.cfg.startpageType === "expanded" ? "white" : "black",
}}
onClick={(event) => event.preventDefault()}
>
<QuizPreviewLayoutByType
quizHeaderBlock={quizHeaderBlock}
quizMainBlock={
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: settings.cfg.startpageType === "standard" && isMobile ? "start" : "center",
flexGrow: settings.cfg.startpageType === "centered" ? 0 : 1,
wordBreak: "break-word",
alignItems:
settings.cfg.startpageType === "centered"
? "center"
: settings.cfg.startpageType === "expanded"
? settings.cfg.startpage.position === "center"
? "center"
: "start"
: "start",
marginTop: settings.cfg.startpageType === "centered" ? "30px" : isMobile ? "0px" : "5px",
maxWidth: isMobile
? "100%"
: settings.cfg.startpageType === "centered"
? "700px"
: isTablet &&
settings.cfg.startpageType !== "expanded" &&
settings.cfg.startpage.position !== "center"
? "380px"
: "531px",
}}
>
<Typography
sx={{
fontWeight: "700",
fontSize: isMobile ? "24px" : "27px",
fontStyle: "normal",
fontStretch: "normal",
lineHeight: isMobile ? "26.4px" : "normal",
overflowWrap: "break-word",
width: "100%",
textAlign:
settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center"
? "center"
: "-moz-initial",
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
}}
>
{settings.name}
</Typography>
<Typography
sx={{
fontSize: isMobile ? "16px" : "17px",
fontWeight: "400",
lineHeight: isMobile ? "19.2px" : "normal",
margin: "12px 0 30px",
overflowWrap: "break-word",
width: "100%",
textAlign:
settings.cfg.startpageType === "centered" || settings.cfg.startpage.position === "center"
? "center"
: "-moz-initial",
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
}}
>
{settings.cfg.startpage.description}
</Typography>
<Box width={settings.cfg.startpageType === "standard" ? "100%" : "auto"}>
<Button
variant="contained"
disabled={realQuestionsCount === 0}
sx={{
fontSize: "18px",
padding: "10px 20px",
width: "auto",
background: theme.palette.primary.main,
borderRadius: "12px",
}}
onClick={onQuizStart}
>
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"}
</Button>
</Box>
</Box>
<Box
sx={{
display: "flex",
flexGrow: settings.cfg.startpageType === "centered" ? (isMobile ? 0 : 1) : 0,
gap: isMobile ? "30px" : "40px",
alignItems: "flex-end",
justifyContent:
(settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
isMobile) ||
(settings.cfg.startpageType === "centered" && isMobile)
? "center"
: "space-between",
width: "100%",
flexWrap:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? isMobile
? "wrap-reverse"
: "nowrap"
: "wrap",
}}
>
{settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isMobile &&
quizHeaderBlock}
<Box
sx={{
maxWidth: "300px",
display:
(settings.cfg.startpageType === "centered" && isMobile) ||
(settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
isMobile)
? "flex"
: "block",
flexDirection: "column",
alignItems: "center",
order:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "2"
: "0",
}}
>
{settings.cfg.info.site && (
<ButtonBase
onClick={onSiteClick}
sx={{
display: "block",
width: "100%",
marginTop: "10px",
marginLeft:
settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isMobile
? "auto"
: undefined,
}}
>
<Typography
sx={{
lineHeight: "19px",
fontSize: "16px",
textAlign:
settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isMobile
? "end"
: (settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
isMobile) ||
(settings.cfg.startpageType === "centered" && isMobile)
? "center"
: "start",
color: theme.palette.primary.main,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{settings.cfg.info.site}
</Typography>
</ButtonBase>
)}
{settings.cfg.info.clickable ? (
isMobileDevice ? (
<Link href={`tel:${settings.cfg.info.phonenumber}`}>
<Typography
sx={{
lineHeight: "19px",
textAlign:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "end"
: "none",
fontSize: "16px",
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
</Link>
) : (
<ButtonBase
onClick={handleCopyNumber}
sx={{
display: "block",
marginTop: "10px",
marginLeft:
settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isMobile
? "auto"
: undefined,
}}
>
<Typography
sx={{
textAlign:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "end"
: "none",
fontSize: "16px",
lineHeight: "19px",
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
</ButtonBase>
)
) : (
<Typography
sx={{
lineHeight: "19px",
textAlign:
settings.cfg.startpageType === "expanded" && settings.cfg.startpage.position === "center"
? "end"
: "none",
fontSize: "16px",
marginTop: "10px",
color: settings.cfg.startpageType === "expanded" ? "#FFFFFF" : theme.palette.text.primary,
}}
>
{settings.cfg.info.phonenumber}
</Typography>
)}
<Typography
sx={{
lineHeight: "14px",
width: "100%",
overflowWrap: "break-word",
fontSize: "12px",
textAlign:
settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
!isMobile
? "end"
: (settings.cfg.startpageType === "expanded" &&
settings.cfg.startpage.position === "center" &&
isMobile) ||
(settings.cfg.startpageType === "centered" && isMobile)
? "center"
: "none",
maxHeight: "120px",
overflow: "auto",
marginTop: "10px",
"&::-webkit-scrollbar": { width: 0 },
color: settings.cfg.startpageType === "expanded" ? "white" : theme.palette.text.primary,
}}
>
{settings.cfg.info.law}
</Typography>
</Box>
{show_badge && PenaBadge}
</Box>
</>
}
backgroundBlock={background}
startpageType={settings.cfg.startpageType}
alignType={settings.cfg.startpage.position}
/>
</Paper>
);
};

@ -1,4 +1,3 @@
import { ContactForm } from "@/components/ViewPublicationPage/ContactForm/ContactForm.tsx";
import { extractImageLinksFromQuestion } from "@/utils/extractImageLinks"; import { extractImageLinksFromQuestion } from "@/utils/extractImageLinks";
import { useVKMetrics } from "@/utils/hooks/metrics/useVKMetrics"; import { useVKMetrics } from "@/utils/hooks/metrics/useVKMetrics";
import { useYandexMetrics } from "@/utils/hooks/metrics/useYandexMetrics"; import { useYandexMetrics } from "@/utils/hooks/metrics/useYandexMetrics";
@ -14,7 +13,6 @@ import { Helmet } from "react-helmet-async";
import { Question } from "./Question"; import { Question } from "./Question";
import QuestionSelect from "./QuestionSelect"; import QuestionSelect from "./QuestionSelect";
import { ResultForm } from "./ResultForm"; import { ResultForm } from "./ResultForm";
import { StartPageViewPublication } from "./StartPageViewPublication";
import NextButton from "./tools/NextButton"; import NextButton from "./tools/NextButton";
import PrevButton from "./tools/PrevButton"; import PrevButton from "./tools/PrevButton";
import unscreen from "@/ui_kit/unscreen"; import unscreen from "@/ui_kit/unscreen";
@ -85,17 +83,9 @@ export default function ViewPublicationPage() {
const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id); const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
let quizStepElement: ReactElement; let quizStepElement: ReactElement;
switch (currentQuizStep) {
case "startpage": {
quizStepElement = <StartPageViewPublication />;
break;
}
case "question": {
if (currentQuestion.type === "result") { if (currentQuestion.type === "result") {
quizStepElement = <ResultForm resultQuestion={currentQuestion} />; quizStepElement = <ResultForm resultQuestion={currentQuestion} />;
break; } else {
}
quizStepElement = ( quizStepElement = (
<Question <Question
key={currentQuestion.id} key={currentQuestion.id}
@ -130,19 +120,6 @@ export default function ViewPublicationPage() {
} }
/> />
); );
break;
}
case "contactform": {
quizStepElement = (
<ContactForm
currentQuestion={currentQuestion}
onShowResult={showResultAfterContactForm}
/>
);
break;
}
default:
notReachable(currentQuizStep);
} }
const preloadLinks = new Set([ const preloadLinks = new Set([

@ -1,77 +0,0 @@
import { useQuizViewStore } from "@/stores/quizView";
import { useQuizStore } from "@/stores/useQuizStore";
import CalendarIcon from "@icons/CalendarIcon";
import type { QuizQuestionDate } from "@model/questionTypes/date";
import { Box, Typography, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { Moment } from "moment";
import moment from "moment";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export default ({ currentQuestion }: DateProps) => {
const { settings } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const currentAnswer = moment(answer) || moment();
const onDateChange = async (date: Moment | null) => {
if (!date) return;
updateAnswer(currentQuestion.id, date, 0);
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<DatePicker
format="DD/MM/YYYY"
slots={{
openPickerIcon: () => (
<CalendarIcon
sx={{
"& path": { stroke: theme.palette.primary.main },
"& rect": { stroke: theme.palette.primary.main },
}}
/>
),
}}
value={currentAnswer}
onChange={onDateChange}
slotProps={{
openPickerButton: { sx: { p: 0 }, "data-cy": "open-datepicker" },
layout: {
sx: { backgroundColor: theme.palette.background.default },
},
}}
sx={{
"& .MuiInputBase-root": {
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderRadius: "10px",
maxWidth: "250px",
pr: "30px",
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
"& fieldset": { borderColor: "#9A9AAF" },
},
}}
/>
</Box>
);
};

@ -1,104 +0,0 @@
import { useQuizViewStore } from "@/stores/quizView";
import type { QuizQuestionDate } from "@model/questionTypes/date";
import { DateCalendar } from "@mui/x-date-pickers";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { Moment } from "moment";
import moment from "moment";
import { Box, Paper, TextField, useTheme } from "@mui/material";
import { useRootContainerSize } from "@/contexts/RootContainerWidthContext";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export default ({ currentQuestion }: DateProps) => {
const theme = useTheme();
const today = moment();
const isMobile = useRootContainerSize() < 690;
const { settings } = useQuizStore();
const { updateAnswer } = useQuizViewStore((state) => state);
const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers);
const answer = (answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string) || ["0", "0"];
const currentFrom = Number(answer[0]) ? moment(Number(answer[0])) : moment().utc();
const currentTo = Number(answer[1]) ? moment(Number(answer[1])) : moment().utc();
const onDateChange = async (date: Moment | null, index: number) => {
if (!date) return;
let newAnswer = [...answer];
newAnswer[index] = (moment(date).unix() * 1000).toString();
updateAnswer(currentQuestion.id, newAnswer, 0);
};
return (
<Paper
sx={{
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
width: isMobile ? "min-content" : "auto",
display: "inline-flex",
flexWrap: "wrap",
marginTop: "20px",
p: "20px",
}}
>
<Box>
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("From")}</span>
<DateCalendar
sx={{
"& .MuiInputBase-root": {
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderRadius: "10px",
maxWidth: "250px",
pr: "30px",
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
"& fieldset": { borderColor: "#9A9AAF" },
},
}}
value={currentFrom}
onChange={(data) => onDateChange(data, 0)}
/>
</Box>
<Box>
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>{t("До")}</span>
<DateCalendar
minDate={today}
sx={{
"& .MuiInputBase-root": {
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
borderRadius: "10px",
maxWidth: "250px",
pr: "30px",
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
"& fieldset": { borderColor: "#9A9AAF" },
},
}}
value={currentTo}
onChange={(data) => onDateChange(data, 1)}
/>
</Box>
</Paper>
);
};

@ -1,29 +0,0 @@
import type { QuizQuestionDate } from "@model/questionTypes/date";
import DateRange from "./DateRange";
import DatePicker from "./DatePicker";
import { Box, Typography, useTheme } from "@mui/material";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme();
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{currentQuestion.content.isRange ? (
<DateRange currentQuestion={currentQuestion} />
) : (
<DatePicker currentQuestion={currentQuestion} />
)}
</Box>
);
};

@ -1,49 +0,0 @@
import EmojiPickerOriginal from "@emoji-mart/react";
import { Box } from "@mui/material";
type Emoji = {
emoticons: string[];
id: string;
keywords: string[];
name: string;
native: string;
shortcodes: string;
unified: string;
};
type EmojiPickerProps = {
onEmojiSelect: (emoji: Emoji) => void;
};
export const EmojiPicker = ({ onEmojiSelect }: EmojiPickerProps) => (
<Box sx={{ minWidth: "352px" }}>
<EmojiPickerOriginal
onEmojiSelect={onEmojiSelect}
theme="light"
locale="ru"
exceptEmojis={ignoreEmojis}
/>
</Box>
);
const ignoreEmojis = [
"two_men_holding_hands",
"two_women_holding_hands",
"man-kiss-man",
"woman-kiss-woman",
"man-heart-man",
"woman-heart-woman",
"man-man-boy",
"man-man-girl",
"man-man-girl-boy",
"man-man-girl-girl",
"man-man-boy-boy",
"woman-woman-boy",
"woman-woman-girl",
"woman-woman-girl-boy",
"woman-woman-girl-girl",
"woman-woman-boy-boy",
"rainbow-flag",
"transgender_flag",
"transgender_symbol",
];

@ -1,271 +0,0 @@
import type { QuestionVariant } from "@/model/questionTypes/shared";
import { useQuizStore } from "@/stores/useQuizStore";
import { useQuizViewStore, type OwnVariant } from "@stores/quizView";
import {
Box,
Checkbox,
FormControl,
FormControlLabel,
Input,
Radio,
TextareaAutosize,
Typography,
useTheme,
} from "@mui/material";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import type { MouseEvent } from "react";
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import { OwnEmojiPicker } from "./OwnEmojiPicker";
polyfillCountryFlagEmojis();
type EmojiVariantProps = {
questionId: string;
variant: QuestionVariant;
index: number;
isMulti: boolean;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
answer: string | string[] | undefined;
};
interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v: OwnVariant) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<Box sx={{ overflow: "auto" }}>
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
overflow: "auto",
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
</Box>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const EmojiVariant = ({
answer,
variant,
index,
questionId,
isMulti,
own,
questionLargeCheck,
ownPlaceholder,
}: EmojiVariantProps) => {
const { settings } = useQuizStore();
const { updateAnswer, deleteAnswer, updateOwnVariant, ownVariants } = useQuizViewStore((state) => state);
const theme = useTheme();
const { t } = useTranslation();
const customEmoji = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.extendedText || "";
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
const variantId = variant.id;
if (isMulti) {
const currentAnswer = Array.isArray(answer) ? answer : [];
const newAnswer = currentAnswer.includes(variantId)
? currentAnswer.filter((item) => item !== variantId)
: [...currentAnswer, variantId];
updateAnswer(questionId, newAnswer, variant.points || 0);
} else {
if (answer === variant.id) {
deleteAnswer(questionId);
} else {
updateAnswer(questionId, variant.id, variant.points || 0);
}
}
};
const handleEmojiSelect = (emoji: string) => {
// We store custom emoji in ownVariants store, with a specific field to differentiate
const currentOwnAnswer = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.answer || "";
updateOwnVariant(variant.id, currentOwnAnswer, emoji);
};
const handleEmojiRemove = () => {
// Сохраняем текущий answer, очищаем только extendedText (эмодзи)
const currentOwnAnswer = ownVariants.find((v: OwnVariant) => v.id === variant.id)?.variant.answer || "";
updateOwnVariant(variant.id, currentOwnAnswer, "");
};
const isSelected = isMulti ? Array.isArray(answer) && answer.includes(variant.id) : answer === variant.id;
return (
<FormControl
key={index}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor: isSelected ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)"
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
"&:hover": { borderColor: theme.palette.primary.main },
}}
onClick={onVariantClick}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
cursor: "pointer",
}}
>
{own ? (
<OwnEmojiPicker
emoji={customEmoji || variant.extendedText}
onEmojiSelect={handleEmojiSelect}
onEmojiRemove={customEmoji ? handleEmojiRemove : undefined}
/>
) : (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && <Typography fontSize="100px">{variant.extendedText}</Typography>}
</Box>
)}
</Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
{t("Enter your answer")}
</Typography>
)}
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
margin: 0,
padding: "15px",
display: "flex",
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
justifyContent: "center",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? "100%" : "60px",
overflow: "auto",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
width: "100%",
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
value={index}
control={
isMulti ? (
<Checkbox
checked={isSelected}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
) : (
<Radio
checked={isSelected}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
)
}
label={
own ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography>
</Box>
)
}
/>
</FormControl>
);
};

@ -1,103 +0,0 @@
import { Box, ButtonBase, Typography, useTheme, Modal, IconButton } from "@mui/material";
import { useState } from "react";
import { EmojiPicker } from "./EmojiPicker";
import { useTranslation } from "react-i18next";
import CloseIcon from "@mui/icons-material/Close";
interface Props {
emoji: string;
onEmojiSelect?: (emoji: string) => void;
onEmojiRemove?: () => void;
}
export const OwnEmojiPicker = ({ emoji = "", onEmojiSelect, onEmojiRemove }: Props) => {
const theme = useTheme();
const { t } = useTranslation();
const [isPickerOpen, setIsPickerOpen] = useState(false);
const handleEmojiSelect = (emojiData: any) => {
onEmojiSelect?.(emojiData.native);
setIsPickerOpen(false);
};
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
setIsPickerOpen(true);
};
const handleClose = (e: React.MouseEvent) => {
e.stopPropagation();
setIsPickerOpen(false);
};
const handleRemoveEmoji = (e: React.MouseEvent) => {
e.stopPropagation();
onEmojiRemove?.();
};
return (
<>
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
<ButtonBase
onClick={handleClick}
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
"&:hover": {
bgcolor: theme.palette.grey[100],
},
}}
>
<Typography fontSize={emoji ? "100px" : "18px"}>{emoji || t("select emoji")}</Typography>
</ButtonBase>
{onEmojiRemove && (
<IconButton
onClick={handleRemoveEmoji}
sx={{
position: "absolute",
top: 8,
left: 8,
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
color: "white",
height: "25px",
width: "25px",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<CloseIcon />
</IconButton>
)}
</Box>
<Modal
open={isPickerOpen}
onClose={handleClose}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
keepMounted
>
<Box
onClick={(e) => e.stopPropagation()}
sx={{
bgcolor: "background.paper",
borderRadius: 2,
p: 2,
boxShadow: 24,
}}
>
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
</Box>
</Modal>
</>
);
};

@ -1,66 +0,0 @@
import type { QuizQuestionEmoji } from "@model/questionTypes/emoji";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { EmojiVariant } from "./EmojiVariant";
import moment from "moment";
polyfillCountryFlagEmojis();
type EmojiProps = {
currentQuestion: QuizQuestionEmoji;
};
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const selectedVariantId = Array.isArray(answer) ? answer[0] : answer;
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={selectedVariantId}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<EmojiVariant
key={variant.id}
questionId={currentQuestion.id}
variant={variant}
index={index}
isMulti={Boolean(currentQuestion.content.multi)}
own={Boolean(variant.isOwn)}
questionLargeCheck={true}
answer={answer}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box>
</RadioGroup>
</Box>
);
};

@ -1,150 +0,0 @@
import { useState } from "react";
import { Box, ButtonBase, Skeleton, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { sendAnswer, sendFile } from "@api/quizRelase";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizViewStore } from "@stores/quizView";
import {
ACCEPT_SEND_FILE_TYPES_MAP,
MAX_FILE_SIZE,
UPLOAD_FILE_DESCRIPTIONS_MAP,
} from "@/components/ViewPublicationPage/tools/fileUpload";
import Info from "@icons/Info";
import UploadIcon from "@icons/UploadIcon";
import type { QuizQuestionFile } from "@model/questionTypes/file";
import type { ModalWarningType } from "./index";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
type UploadFileProps = {
currentQuestion: QuizQuestionFile;
setModalWarningType: (modalType: ModalWarningType) => void;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
};
export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, setIsSending }: UploadFileProps) => {
const { quizId, preview } = useQuizStore();
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
const theme = useTheme();
const { t } = useTranslation();
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 500;
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const uploadFile = async (file: File | undefined) => {
if (isSending) return;
if (!file) return;
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some((fileType) =>
file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) return setModalWarningType("errorType");
setIsSending(true);
try {
const data = await sendFile({
questionId: currentQuestion.id,
body: {
file: file,
name: file.name,
preview,
},
qid: quizId,
});
await sendAnswer({
questionId: currentQuestion.id,
body: `${data!.data.fileIDMap[currentQuestion.id]}`,
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
} catch (error) {
console.error(error);
enqueueSnackbar(t("The answer was not counted"));
}
setIsSending(false);
};
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDropzoneHighlighted(false);
const file = event.dataTransfer.files[0];
uploadFile(file);
};
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
{isSending ? (
<Skeleton
variant="rounded"
sx={{ width: "100%", height: "120px", maxWidth: "560px" }}
/>
) : (
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={({ target }) => uploadFile(target.files?.[0])}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box
onDragEnter={() => !answer?.split("|")[0] && setIsDropzoneHighlighted(true)}
onDragLeave={() => setIsDropzoneHighlighted(false)}
onDragOver={(event) => event.preventDefault()}
onDrop={onDrop}
sx={{
width: "100%",
height: isMobile ? undefined : "120px",
display: "flex",
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: "#F2F3F7",
border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography sx={{ color: "#9A9AAF", fontWeight: 500 }}>
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title)}
</Typography>
<Typography
sx={{
color: "#9A9AAF",
fontSize: "16px",
lineHeight: "19px",
}}
>
{t(UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].description)}
</Typography>
</Box>
</Box>
</ButtonBase>
)}
<Info
sx={{ width: "40px", height: "40px" }}
color={theme.palette.primary.main}
onClick={() => setModalWarningType(currentQuestion.content.type)}
/>
</Box>
);
};

@ -1,75 +0,0 @@
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import CloseBold from "@icons/CloseBold";
import type { QuizQuestionFile } from "@model/questionTypes/file";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
type UploadedFileProps = {
currentQuestion: QuizQuestionFile;
setIsSending: (isSending: boolean) => void;
};
export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProps) => {
const { quizId, preview } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const { t } = useTranslation();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const deleteFile = async () => {
if (answer.length > 0) {
setIsSending(true);
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
}
updateAnswer(currentQuestion.id, "", 0);
setIsSending(false);
};
return (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography color={theme.palette.text.primary}>{t("You have uploaded")}:</Typography>
<Box
sx={{
padding: "5px 5px 5px 16px",
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
alignItems: "center",
overflow: "hidden",
gap: "15px",
}}
>
<Typography
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{answer?.split("|")[0]}
</Typography>
<IconButton
sx={{ p: 0 }}
onClick={deleteFile}
>
<CloseBold />
</IconButton>
</Box>
</Box>
);
};

@ -1,122 +0,0 @@
import { useState } from "react";
import { Box, Modal, Typography, useTheme } from "@mui/material";
import { UploadFile } from "./UploadFile";
import { UploadedFile } from "./UploadedFile";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizViewStore } from "@stores/quizView";
import { ACCEPT_SEND_FILE_TYPES_MAP } from "@/components/ViewPublicationPage/tools/fileUpload";
import type { QuizQuestionFile } from "@model/questionTypes/file";
import { useTranslation } from "react-i18next";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type FileProps = {
currentQuestion: QuizQuestionFile;
};
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const answers = useQuizViewStore((state) => state.answers);
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
const [isSending, setIsSending] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500;
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "600px",
}}
>
{answer?.split("|")[0] ? (
<UploadedFile
currentQuestion={currentQuestion}
setIsSending={setIsSending}
/>
) : (
<UploadFile
currentQuestion={currentQuestion}
setModalWarningType={setModalWarningType}
isSending={isSending}
setIsSending={setIsSending}
/>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
style={{ marginTop: "15px", maxWidth: "300px", maxHeight: "300px" }}
alt=""
/>
)}
{answer && currentQuestion.content.type === "video" && (
<video
src={answer.split("|")[1]}
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
objectFit: "cover",
}}
/>
)}
</Box>
<Modal
open={modalWarningType !== null}
onClose={() => setModalWarningType(null)}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: isMobile ? 300 : 400,
bgcolor: "background.paper",
borderRadius: 3,
boxShadow: 24,
p: 4,
}}
>
<CurrentModal status={modalWarningType} />
</Box>
</Modal>
</Box>
);
};
const CurrentModal = ({ status }: { status: ModalWarningType }) => {
const { t } = useTranslation();
switch (status) {
case null:
return null;
case "errorType":
return <Typography>{t("Incorrect file type selected")}</Typography>;
case "errorSize":
return <Typography>{t("File is too big. Maximum size is 50 MB")}</Typography>;
default:
return (
<>
<Typography>{t("Acceptable file extensions")}:</Typography>
<Typography>{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</>
);
}
};

@ -1,282 +0,0 @@
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
import { Box, Checkbox, FormControlLabel, Input, Radio, TextareaAutosize, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useMemo, type MouseEvent, useRef, useEffect } from "react";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next";
import { OwnImage } from "./OwnImage";
import { useSnackbar } from "notistack";
type ImagesProps = {
questionId: string;
variant: QuestionVariantWithEditedImages;
index: number;
answer: string | string[] | undefined;
isMulti: boolean;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
};
interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<Box sx={{ overflow: "auto" }}>
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
</Box>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const ImageVariant = ({
questionId,
answer,
isMulti,
variant,
index,
own,
questionLargeCheck,
ownPlaceholder,
}: ImagesProps) => {
const { settings } = useQuizStore();
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const { t } = useTranslation();
const isMobile = useRootContainerSize() < 450;
const isTablet = useRootContainerSize() < 850;
const { enqueueSnackbar } = useSnackbar();
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
const variantId = variant.id;
if (isMulti) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
return updateAnswer(
questionId,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
variant.points || 0
);
}
updateAnswer(questionId, variantId, variant.points || 0);
if (answer === variantId) {
deleteAnswer(questionId);
}
};
const choiceImgUrl = useMemo(() => {
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return variant.extendedText;
}
}, []);
useEffect(() => {
if (canvasRef.current !== null) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (ctx !== null) {
const img = new Image();
img.src = choiceImgUrl;
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
};
}
}
}, []);
return (
<Box
sx={{
position: "relative",
cursor: "pointer",
borderRadius: "12px",
border: `1px solid`,
borderColor: !!answer?.includes(variant.id) ? theme.palette.primary.main : "#9A9AAF",
"&:hover": { borderColor: theme.palette.primary.main },
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)"
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
}}
onClick={onVariantClick}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{own ? (
<OwnImage
imageUrl={choiceImgUrl}
questionId={questionId}
variantId={variant.id}
onValidationError={(errorType) => {
enqueueSnackbar(errorType === "size" ? t("file is too big") : t("file type is not supported"), {
variant: "warning",
});
}}
/>
) : (
variant.extendedText && (
<canvas
ref={canvasRef}
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "12px 12px 0 0",
}}
/>
)
)}
</Box>
</Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
{t("Enter your answer")}
</Typography>
)}
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
display: "flex",
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
justifyContent: "center",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
lineHeight: "normal",
overflow: "auto",
maxHeight: "100%",
width: "100%",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
}}
value={index}
control={
isMulti ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
)
}
label={
own ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
variant.answer
)
}
/>
</Box>
);
};

@ -1,187 +0,0 @@
import { Box, ButtonBase, IconButton, Typography, useTheme } from "@mui/material";
import { useState, useRef } from "react";
import CloseIcon from "@mui/icons-material/Close";
import { useTranslation } from "react-i18next";
import { useQuizStore } from "@/stores/useQuizStore";
import { useQuizViewStore } from "@/stores/quizView";
import { useSnackbar } from "notistack";
import { Skeleton } from "@mui/material";
import UploadIcon from "@/assets/icons/UploadIcon";
import { sendFile } from "@/api/quizRelase";
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE } from "../../tools/fileUpload";
// Пропсы компонента
export type OwnImageProps = {
imageUrl?: string;
questionId: string;
variantId: string;
onValidationError: (error: "size" | "type") => void;
};
export const OwnImage = ({ imageUrl, questionId, variantId, onValidationError }: OwnImageProps) => {
const theme = useTheme();
const { t } = useTranslation();
const { quizId, preview } = useQuizStore();
const { ownVariants, updateOwnVariant } = useQuizViewStore((state) => state);
const { enqueueSnackbar } = useSnackbar();
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Получаем ownVariant для этого варианта
const ownVariantData = ownVariants.find((v) => v.id === variantId);
// Загрузка файла
const uploadImage = async (file: File) => {
if (isUploading) return;
if (!file) return;
if (file.size > MAX_FILE_SIZE) {
onValidationError("size");
return;
}
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.picture.some((fileType) =>
file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) {
onValidationError("type");
return;
}
setIsUploading(true);
try {
const data = await sendFile({
questionId,
body: { file, name: file.name, preview },
qid: quizId,
});
const fileId = data?.data.fileIDMap[questionId];
const localImageUrl = URL.createObjectURL(file);
updateOwnVariant(variantId, "", "", fileId, localImageUrl);
} catch (error) {
console.error("Error uploading image:", error);
enqueueSnackbar(t("The answer was not counted"));
} finally {
setIsUploading(false);
}
};
// Обработчик выбора файла
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
uploadImage(file);
}
};
// Открытие диалога выбора файла
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (fileInputRef.current) fileInputRef.current.value = "";
fileInputRef.current?.click();
};
// Удаление изображения
const handleRemoveImage = (e: React.MouseEvent) => {
e.stopPropagation();
updateOwnVariant(variantId, ownVariantData?.variant.answer || "", "", "", "");
/*
1 - answer - письменный ответ
2 - extendedText - строка используется в эмодзи-вопросах для хранения выбранного эмодзи
3 - originalImageUrl - полный URL изображения, загруженного на сервер
4 - localImageUrl - временный URL для отображения изображения в браузере
*/
};
// Определяем, что показывать
let imageToDisplay: string | null = null;
if (ownVariantData?.variant.localImageUrl) {
imageToDisplay = ownVariantData.variant.localImageUrl;
} else if (imageUrl) {
imageToDisplay = imageUrl;
}
if (isUploading) {
return (
<Skeleton
variant="rounded"
sx={{ width: "100%", height: "100%", borderRadius: "12px" }}
/>
);
}
return (
<ButtonBase
component="div"
onClick={handleClick}
disabled={isUploading}
sx={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "12px",
transition: "border-color 0.3s, background-color 0.3s",
overflow: "hidden",
position: "relative",
opacity: isUploading ? 0.7 : 1,
}}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
hidden
/>
{imageToDisplay ? (
<>
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
<img
src={imageToDisplay}
alt="Preview"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</Box>
<IconButton
onClick={handleRemoveImage}
sx={{
position: "absolute",
top: 8,
left: 8,
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
color: "white",
height: "25px",
width: "25px",
display: ownVariantData?.variant.localImageUrl ? "inherit" : "none",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<CloseIcon />
</IconButton>
</>
) : (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
opacity: 0.5,
}}
>
<UploadIcon />
<Typography
variant="body2"
color="text.secondary"
sx={{ p: 2, textAlign: "center" }}
>
{t("Add your image")}
</Typography>
</Box>
)}
</ButtonBase>
);
};

@ -1,71 +0,0 @@
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { QuizQuestionImages } from "@model/questionTypes/images";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { createQuizViewStore, useQuizViewStore } from "@stores/quizView";
import { ImageVariant } from "./ImageVariant";
import moment from "moment";
type ImagesProps = {
currentQuestion: QuizQuestionImages;
};
export const Images = ({ currentQuestion }: ImagesProps) => {
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet ? (isMobile ? "repeat(1, 1fr)" : "repeat(2, 1fr)") : "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<ImageVariant
key={variant.id}
questionId={currentQuestion.id}
variant={variant}
index={index}
answer={answer}
isMulti={Boolean(currentQuestion.content.multi)}
own={Boolean(variant.isOwn)}
questionLargeCheck={true}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box>
</RadioGroup>
</Box>
);
};

@ -1,391 +0,0 @@
import { useQuizStore } from "@/stores/useQuizStore";
import type { QuizQuestionNumber } from "@model/questionTypes/number";
import { Box, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import { CustomSlider } from "@ui_kit/CustomSlider";
import CustomTextField from "@ui_kit/CustomTextField";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent, SyntheticEvent } from "react";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
type NumberProps = {
currentQuestion: QuizQuestionNumber;
};
export const Number = ({ currentQuestion }: NumberProps) => {
const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000");
const [reversedInputValue, setReversedInputValue] = useState<string>("0");
const [reversedMinRange, setReversedMinRange] = useState<string>("0");
const [reversedMaxRange, setReversedMaxRange] = useState<string>("100000000000");
const { settings } = useQuizStore();
const { updateAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const [minBorder, maxBorder] = currentQuestion.content.range.split("—").map(window.Number);
const min = minBorder < maxBorder ? minBorder : maxBorder;
const max = minBorder < maxBorder ? maxBorder : minBorder;
const reversed = minBorder > maxBorder;
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer as string;
const sliderValue =
answer ||
(reversed ? max + min - currentQuestion.content.start + "—" + max : currentQuestion.content.start + "—" + max);
useEffect(() => {
console.log("reversed:", reversed);
}, [reversed]);
const sendAnswerToBackend = async (value: string, noUpdate = false) => {
if (!noUpdate) {
updateAnswer(currentQuestion.id, value, 0);
}
};
const updateValueDebounced = useDebouncedCallback(async (value: string) => {
if (reversed) {
const newValue =
window.Number(value) < window.Number(min)
? String(min)
: window.Number(value) > window.Number(max)
? String(max)
: value;
setReversedInputValue(newValue);
updateAnswer(currentQuestion.id, String(max + min - window.Number(newValue)), 0);
await sendAnswerToBackend(String(window.Number(newValue)), true);
return;
}
const newValue =
window.Number(value) < window.Number(minRange)
? minRange
: window.Number(value) > window.Number(maxRange)
? maxRange
: value;
setInputValue(newValue);
await sendAnswerToBackend(newValue);
}, 1000);
const updateMinRangeDebounced = useDebouncedCallback(async (value: string, crowded = false) => {
if (reversed) {
const newMinRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[0]) < min
? min
: max + min - window.Number(value.split("—")[0]);
const newMinValue = window.Number(value.split("—")[0]) > max ? String(max) : value.split("—")[0];
setReversedMinRange(crowded ? String(max + min - window.Number(newMinValue)) : newMinValue);
updateAnswer(currentQuestion.id, `${newMinRange}${value.split("—")[1]}`, 0);
await sendAnswerToBackend(`${newMinValue}${value.split("—")[1]}`, true);
return;
}
const newMinValue = crowded
? maxRange
: window.Number(value.split("—")[0]) < min
? String(min)
: value.split("—")[0];
setMinRange(newMinValue);
await sendAnswerToBackend(`${newMinValue}${value.split("—")[1]}`);
}, 1000);
const updateMaxRangeDebounced = useDebouncedCallback(async (value: string, crowded = false) => {
if (reversed) {
const newMaxRange = crowded
? window.Number(value.split("—")[1])
: max + min - window.Number(value.split("—")[1]) > max
? max
: max + min - window.Number(value.split("—")[1]);
const newMaxValue = window.Number(value.split("—")[1]) < min ? String(min) : value.split("—")[1];
setReversedMaxRange(crowded ? String(max + min - window.Number(newMaxValue)) : newMaxValue);
updateAnswer(currentQuestion.id, `${value.split("—")[0]}${newMaxRange}`, 0);
await sendAnswerToBackend(`${value.split("—")[0]}${newMaxValue}`, true);
return;
}
const newMaxValue = crowded
? minRange
: window.Number(value.split("—")[1]) > max
? String(max)
: value.split("—")[1];
setMaxRange(newMaxValue);
await sendAnswerToBackend(`${value.split("—")[0]}${newMaxValue}`);
}, 1000);
useEffect(() => {
if (answer) {
if (answer.includes("—")) {
if (reversed) {
setReversedMinRange(String(max + min - window.Number(answer.split("—")[0])));
setReversedMaxRange(String(max + min - window.Number(answer.split("—")[1])));
} else {
setMinRange(answer.split("—")[0]);
setMaxRange(answer.split("—")[1]);
}
} else {
if (reversed) {
setReversedInputValue(String(max + min - window.Number(answer)));
} else {
setInputValue(answer);
}
}
}
if (!answer) {
setMinRange(String(currentQuestion.content.start));
setMaxRange(String(max));
if (currentQuestion.content.chooseRange) {
setReversedMinRange(String(currentQuestion.content.start));
setReversedMaxRange(String(min));
}
setReversedInputValue(String(currentQuestion.content.start));
setInputValue(String(currentQuestion.content.start));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSliderChange = (_: Event, value: number | number[]) => {
const range = Array.isArray(value) ? `${value[0]}${value[1]}` : String(value);
updateAnswer(currentQuestion.id, range, 0);
};
const onChangeCommitted = async (_: Event | SyntheticEvent<Element, Event>, value: number | number[]) => {
if (currentQuestion.content.chooseRange && Array.isArray(value)) {
if (reversed) {
const newMinReversedValue = String(max + min - value[0]);
const newMaxReversedValue = String(max + min - value[1]);
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
setReversedMinRange(newMinReversedValue);
setReversedMaxRange(newMaxReversedValue);
await sendAnswerToBackend(`${newMinReversedValue}${newMaxReversedValue}`, true);
return;
}
setMinRange(String(value[0]));
setMaxRange(String(value[1]));
await sendAnswerToBackend(`${value[0]}${value[1]}`);
return;
}
if (reversed) {
setReversedInputValue(String(max + min - window.Number(value)));
} else {
setInputValue(String(value));
}
await sendAnswerToBackend(String(value));
};
const changeValueLabelFormat = (value: number) => {
if (!reversed) {
return value;
}
const [minSliderBorder, maxSliderBorder] = sliderValue.split("—").map(window.Number);
if (value === minSliderBorder) {
return max + min - minSliderBorder;
}
return max + min - maxSliderBorder;
};
const onInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const value = target.value.replace(/\D/g, "");
if (reversed) {
setReversedInputValue(value);
} else {
setInputValue(value);
}
updateValueDebounced(value);
};
const onMinInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
if (reversed) {
setReversedMinRange(newValue);
if (window.Number(newValue) <= window.Number(reversedMaxRange)) {
const value = max + min - window.Number(reversedMaxRange);
updateMinRangeDebounced(`${value}${value}`, true);
return;
}
updateMinRangeDebounced(`${newValue}${max + min - window.Number(reversedMaxRange)}`);
return;
}
setMinRange(newValue);
if (window.Number(newValue) >= window.Number(maxRange)) {
updateMinRangeDebounced(`${maxRange}${maxRange}`, true);
return;
}
updateMinRangeDebounced(`${newValue}${maxRange}`);
};
const onMaxInputChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const newValue = target.value.replace(/\D/g, "");
if (reversed) {
setReversedMaxRange(newValue);
if (window.Number(newValue) >= window.Number(reversedMinRange)) {
const value = max + min - window.Number(reversedMinRange);
updateMaxRangeDebounced(`${value}${value}`, true);
return;
}
updateMaxRangeDebounced(`${max + min - window.Number(reversedMinRange)}${newValue}`);
return;
}
setMaxRange(newValue);
if (window.Number(newValue) <= window.Number(minRange)) {
updateMaxRangeDebounced(`${minRange}${minRange}`, true);
return;
}
updateMaxRangeDebounced(`${minRange}${newValue}`);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
gap: "30px",
padding: "0 30px",
}}
>
<CustomSlider
value={
currentQuestion.content.chooseRange
? sliderValue.split("—").length || 0 > 1
? sliderValue.split("—").map((item) => window.Number(item))
: [min, min + 1]
: window.Number(sliderValue.split("—")[0])
}
min={min}
max={max}
step={currentQuestion.content.step || 1}
onChange={onSliderChange}
onChangeCommitted={onChangeCommitted}
valueLabelFormat={changeValueLabelFormat}
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,
borderRadius: "8px",
minWidth: "60px",
height: "36px",
},
}}
/>
{!currentQuestion.content.chooseRange && (
<CustomTextField
placeholder="0"
value={reversed ? reversedInputValue : inputValue}
onChange={onInputChange}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
borderColor: "#9A9AAF",
},
}}
/>
)}
{currentQuestion.content.chooseRange && (
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
"& .MuiFormControl-root": { width: "auto" },
}}
>
<CustomTextField
placeholder="0"
value={reversed ? String(reversedMinRange) : minRange}
onChange={onMinInputChange}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
borderColor: "#9A9AAF",
},
}}
/>
<Typography color={theme.palette.text.primary}>до</Typography>
<CustomTextField
placeholder="0"
value={reversed ? String(reversedMaxRange) : maxRange}
onChange={onMaxInputChange}
sx={{
maxWidth: "80px",
"& .MuiOutlinedInput-root": { background: "transparent" },
"& .MuiInputBase-input": { textAlign: "center", zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
backgroundColor: quizThemes[settings.cfg.theme].isLight ? "white" : theme.palette.background.default,
borderColor: "#9A9AAF",
},
}}
/>
</Box>
)}
</Box>
</Box>
);
};

@ -1,76 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import type { QuizQuestionPage } from "@model/questionTypes/page";
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
type PageProps = {
currentQuestion: QuizQuestionPage;
};
export const Page = ({ currentQuestion }: PageProps) => {
const theme = useTheme();
return (
<Box>
<Typography
variant="h5"
sx={{
paddingBottom: "25px",
color: theme.palette.text.primary,
wordBreak: "break-word",
}}
>
{currentQuestion.title}
</Typography>
<Typography
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.content.text}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
{currentQuestion.content.useImage
? currentQuestion.content.back && (
<Box
sx={{
borderRadius: "12px",
border: "1px solid #9A9AAF",
overflow: "hidden",
}}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</Box>
)
: currentQuestion.content.video && (
<QuizVideo
containerSX={{
width: "100%",
height: "calc(100% - 270px)",
maxHeight: "80%",
objectFit: "contain",
aspectRatio: "16 / 9",
}}
videoUrl={currentQuestion.content.video}
/>
)}
</Box>
</Box>
);
};

@ -1,144 +0,0 @@
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "@model/questionTypes/rating";
import { Box, Rating as RatingComponent, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
const RATING_FORM_BUTTONS = [
{
name: "star",
icon: (color: string, width: number) => (
<StarIconMini
width={width}
color={color}
/>
),
},
{
name: "trophie",
icon: (color: string, width: number) => (
<TropfyIcon
width={width}
color={color}
/>
),
},
{
name: "flag",
icon: (color: string, width: number) => (
<FlagIcon
width={width}
color={color}
/>
),
},
{
name: "heart",
icon: (color: string, width: number) => (
<HeartIcon
width={width}
color={color}
/>
),
},
{
name: "like",
icon: (color: string, width: number) => (
<LikeIcon
width={width}
color={color}
/>
),
},
{
name: "bubble",
icon: (color: string, width: number) => (
<LightbulbIcon
width={width}
color={color}
/>
),
},
{
name: "hashtag",
icon: (color: string, width: number) => (
<HashtagIcon
width={width}
color={color}
/>
),
},
];
type RatingProps = {
currentQuestion: QuizQuestionRating;
};
export const Rating = ({ currentQuestion }: RatingProps) => {
const { updateAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 750;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const form = RATING_FORM_BUTTONS.find(({ name }) => name === currentQuestion.content.form);
const sendRating = async (value: number | null) => {
updateAnswer(currentQuestion.id, String(value), 0);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
flexDirection: "column",
}}
>
<Box sx={{ display: "inline-block", width: "100%" }}>
<RatingComponent
value={Number(answer || 0)}
onChange={(_, value) => sendRating(value)}
sx={{
height: "50px",
opacity: "1!important",
"& .MuiRating-root.Mui-disabled": { opacity: "1!important" },
"& .MuiRating-icon": { mr: isMobile ? undefined : "15px" },
}}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.primary.main, isMobile ? 30 : isTablet ? 40 : 50)}
emptyIcon={form?.icon("#9A9AAF", isMobile ? 30 : isTablet ? 40 : 50)}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
width: "100%",
}}
>
<Typography sx={{ color: "#9A9AAF" }}>{currentQuestion.content.ratingNegativeDescription}</Typography>
<Typography sx={{ color: "#9A9AAF" }}>{currentQuestion.content.ratingPositiveDescription}</Typography>
</Box>
</Box>
</Box>
);
};

@ -1,66 +0,0 @@
import { Select as SelectComponent } from "@/components/ViewPublicationPage/tools/Select";
import { useQuizStore } from "@/stores/useQuizStore";
import type { QuizQuestionSelect } from "@model/questionTypes/select";
import { Box, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type SelectProps = {
currentQuestion: QuizQuestionSelect;
};
export const Select = ({ currentQuestion }: SelectProps) => {
const { settings } = useQuizStore();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const sendSelectedAnswer = async (value: number) => {
if (value < 0) {
deleteAnswer(currentQuestion.id);
return;
}
updateAnswer(currentQuestion.id, String(value), 0);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<SelectComponent
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
sx={{
"& .MuiSelect-select.MuiSelect-outlined": { zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(255,255,255, 0.3)"
: "transparent",
},
}}
onChange={(_, value) => sendSelectedAnswer(value)}
/>
</Box>
</Box>
);
};

@ -37,7 +37,6 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
return currentQuestion.content.back; return currentQuestion.content.back;
} }
}, [currentQuestion]); }, [currentQuestion]);
let isCrutch23022025 = window.location.pathname === "/bf8cae3a-e150-479d-befa-7f264087b223";
return ( return (
<Box> <Box>
<Typography <Typography
@ -52,7 +51,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
display: "flex", display: "flex",
width: "100%", width: "100%",
marginTop: "20px", marginTop: "20px",
flexDirection: isCrutch23022025 ? "column" : isMobile ? "column-reverse" : undefined, flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: "center", alignItems: "center",
}} }}
> >
@ -75,9 +74,9 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && ( {choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box <Box
sx={{ sx={{
maxWidth: isCrutch23022025 ? undefined : "400px", maxWidth: "400px",
width: isCrutch23022025 ? "auto" : "100%", width: "100%",
height: isCrutch23022025 ? "auto" : "300px", height: "300px",
margin: "15px", margin: "15px",
}} }}
onClick={(event) => event.preventDefault()} onClick={(event) => event.preventDefault()}

@ -1,139 +0,0 @@
import { Box, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
import { Answer, useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent, FC } from "react";
import type { QuizQuestionText } from "@model/questionTypes/text";
import { useQuizStore } from "@/stores/useQuizStore";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
const ORIENTATION = [
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
];
interface TextSpecialProps {
currentQuestion: QuizQuestionText;
answer?: Answer;
stepNumber?: number | null;
}
export const TextSpecial = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => {
const { settings } = useQuizStore();
const { updateAnswer } = useQuizViewStore((state) => state);
const isHorizontal = ORIENTATION[Number(stepNumber) - 1].horizontal;
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
};
return (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
gap: "20px",
}}
>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
<Box
sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
{
<TextField
autoFocus={true}
multiline
maxRows={4}
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={onInputChange}
inputProps={{
maxLength: 400,
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
sx={{
width: "100%",
"& .MuiOutlinedInput-root": {
backgroundColor: settings.cfg.design ? "rgba(154,154,175, 0.2)" : "#FFFFFF",
},
"&:focus-visible": {
borderColor: theme.palette.primary.main,
},
}}
/>
}
</Box>
{!isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
<Box
sx={{ margin: "15px", width: "40vw" }}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
alt=""
/>
</Box>
)}
</Box>
);
};

@ -1,112 +0,0 @@
import { Box, TextField as MuiTextField, TextFieldProps, Typography, useTheme } from "@mui/material";
import { Answer, useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent, FC } from "react";
import type { QuizQuestionText } from "@model/questionTypes/text";
import { useQuizStore } from "@/stores/useQuizStore";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
interface TextSpecialProps {
currentQuestion: QuizQuestionText;
answer?: Answer;
stepNumber?: number | null;
}
export const TextSpecialHorisontal = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => {
const { settings } = useQuizStore();
const { updateAnswer } = useQuizViewStore((state) => state);
const isHorizontal = true;
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
};
return (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
gap: "20px",
}}
>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
<Box
sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
alt=""
/>
</Box>
)}
{
<TextField
autoFocus={true}
multiline
maxRows={4}
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={onInputChange}
inputProps={{
maxLength: 400,
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
sx={{
width: "100%",
"& .MuiOutlinedInput-root": {
backgroundColor: settings.cfg.design ? "rgba(154,154,175, 0.2)" : "#FFFFFF",
},
"&:focus-visible": {
borderColor: theme.palette.primary.main,
},
}}
/>
}
</Box>
{!isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (
<Box
sx={{ margin: "15px", width: "40vw" }}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
alt=""
/>
</Box>
)}
</Box>
);
};

@ -1,55 +1,21 @@
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
import { TextNormal } from "./TextNormal"; import { TextNormal } from "./TextNormal";
import { TextSpecial } from "./TextSpecial";
import { TextSpecialHorisontal } from "./TextSpecialHorisontal";
import type { QuizQuestionText } from "@model/questionTypes/text"; import type { QuizQuestionText } from "@model/questionTypes/text";
import { useQuizStore } from "@/stores/useQuizStore";
type TextProps = { type TextProps = {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
stepNumber: number | null; stepNumber: number | null;
}; };
const pathOnly = window.location.pathname;
export const Text = ({ currentQuestion, stepNumber }: TextProps) => { export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const { settings } = useQuizStore();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
if (pathOnly === "/92ed5e3e-8e6a-491e-87d0-d3197682d0e3" || pathOnly === "/cc006b40-ccbd-4600-a1d3-f902f85aa0a0")
return (
<TextSpecialHorisontal
currentQuestion={currentQuestion}
answer={answer}
stepNumber={stepNumber}
/>
);
switch (settings.cfg.spec) {
case true:
return (
<TextSpecial
currentQuestion={currentQuestion}
answer={answer}
stepNumber={stepNumber}
/>
);
case undefined:
return ( return (
<TextNormal <TextNormal
currentQuestion={currentQuestion} currentQuestion={currentQuestion}
answer={answer} answer={answer}
/> />
); );
default:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
/>
);
}
}; };

@ -1,216 +0,0 @@
import { useQuizStore } from "@/stores/useQuizStore";
import type { QuestionVariant } from "@model/questionTypes/shared";
import {
Checkbox,
FormControlLabel,
Input,
TextField as MuiTextField,
Radio,
TextFieldProps,
TextareaAutosize,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { FC, MouseEvent } from "react";
import { useTranslation } from "react-i18next";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const VariantItem = ({
questionId,
isMulti,
variant,
answer,
index,
own = false,
questionLargeCheck,
ownPlaceholder,
}: {
isMulti: boolean;
questionId: string;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
}) => {
const { settings } = useQuizStore();
const theme = useTheme();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const { t } = useTranslation();
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault();
const variantId = variant.id;
if (isMulti) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
return updateAnswer(
questionId,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
variant.points || 0
);
}
updateAnswer(questionId, variantId, answer === variantId ? 0 : variant.points || 0);
if (answer === variantId) {
deleteAnswer(questionId);
}
};
return (
<FormControlLabel
key={variant.id}
sx={{
position: "relative",
margin: "0",
mt: own ? "10px" : "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&:hover": { borderColor: theme.palette.primary.main },
"&.MuiFormControl-root": { width: "100%" },
"& .MuiFormControlLabel-label": {
width: "100%",
maxHeight: "100%",
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: theme.palette.primary.main },
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
value={index}
labelPlacement="start"
control={
isMulti ? (
<Radio
checked={!!answer?.includes(variant.id)}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
)
}
label={
own ? (
<>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
position: "absolute",
top: "-23px",
}}
>
{t("Enter your answer")}
</Typography>
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
</>
) : (
variant.answer
)
}
onClick={sendVariant}
/>
);
};

@ -1,159 +0,0 @@
import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material";
import { useEffect, useMemo } from "react";
import { VariantItem } from "./VariantItem";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizViewStore } from "@stores/quizView";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
import moment from "moment";
type VariantProps = {
currentQuestion: QuizQuestionVariant;
};
// 23.02.2025
const crutchlist = {
115048: { x: 629, y: 491 },
115101: { x: 979, y: 980 },
115109: { x: 746, y: 745 },
115122: { x: 959, y: 960 },
115132: { x: 541, y: 541 },
115142: { x: 834, y: 544 },
115178: { x: 1127, y: 1127 },
115191: { x: 1106, y: 1106 },
115207: { x: 905, y: 906 },
115254: { x: 637, y: 637 },
115270: { x: 702, y: 703 },
115287: { x: 714, y: 715 },
115329: { x: 915, y: 916 },
115348: { x: 700, y: 701 },
115368: { x: 400, y: 300 },
115389: { x: 839, y: 840 },
115411: { x: 612, y: 610 },
115434: { x: 474, y: 473 },
115462: { x: 385, y: 385 },
115487: { x: 676, y: 677 },
115515: { x: 341, y: 341 },
115547: { x: 402, y: 403 },
115575: { x: 502, y: 503 },
115612: { x: 400, y: 300 },
115642: { x: 603, y: 603 },
};
export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
//let isCrutch23022025Question = isCrutch23022025 && crutchlist.hasOwnProperty(currentQuestion.id)
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [currentQuestion]);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
id="batya"
sx={{
display: "flex",
gap: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Group
name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
width: "100%",
gap: "20px",
}}
>
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<VariantItem
key={variant.id}
questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={variant}
answer={answer}
index={index}
own={Boolean(variant.isOwn)}
questionLargeCheck={currentQuestion.content.largeCheck}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box>
</Group>
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box
sx={{
maxWidth: "400px",
width: "100%",
height: "300px",
}}
onClick={(event) => event.preventDefault()}
>
<img
key={currentQuestion.id}
src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
</Box>
);
};

@ -1,83 +0,0 @@
import React, { forwardRef, useState } from "react";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizStore } from "@/stores/useQuizStore";
import { useSnackbar } from "notistack";
import { useTranslation } from "react-i18next";
import { sendFile } from "@/api/quizRelase";
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE } from "../../tools/fileUpload";
interface OwnVarimgImageProps {
questionId: string;
variantId: string;
}
export const OwnVarimgImage = forwardRef<HTMLInputElement, OwnVarimgImageProps>(({ questionId, variantId }, ref) => {
const { updateAnswer, updateOwnVariant } = useQuizViewStore((state) => state);
const { quizId, preview } = useQuizStore();
const { enqueueSnackbar } = useSnackbar();
const { t } = useTranslation();
const [isUploading, setIsUploading] = useState(false);
const uploadImage = async (file: File) => {
if (isUploading) return;
if (!file) return;
// Валидация размера файла
if (file.size > MAX_FILE_SIZE) {
enqueueSnackbar(t("file is too big"), { variant: "warning" });
return;
}
// Валидация типа файла
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP.picture.some((fileType) =>
file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) {
enqueueSnackbar(t("file type is not supported"), { variant: "warning" });
return;
}
setIsUploading(true);
try {
const data = await sendFile({
questionId,
body: { file, name: file.name, preview },
qid: quizId,
});
const fileId = data?.data.fileIDMap[questionId];
const localImageUrl = URL.createObjectURL(file);
updateOwnVariant(variantId, "", "", fileId, localImageUrl);
// Убираем автоматический выбор own варианта - загрузка возможна только при выбранном own варианте
// updateAnswer(questionId, variantId, 0);
} catch (error) {
console.error("Error uploading image:", error);
enqueueSnackbar(t("The answer was not counted"));
} finally {
setIsUploading(false);
}
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
uploadImage(file);
event.target.value = "";
}
};
return (
<input
type="file"
ref={ref}
style={{ display: "none" }}
accept={ACCEPT_SEND_FILE_TYPES_MAP.picture.join(",")}
onChange={handleFileChange}
disabled={isUploading}
/>
);
});
OwnVarimgImage.displayName = "OwnVarimgImage";

@ -1,237 +0,0 @@
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
import { useQuizStore } from "@/stores/useQuizStore";
import { FormControlLabel, TextareaAutosize, Radio, useTheme, Box, Input, Typography } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { type MouseEvent } from "react";
import { useTranslation } from "react-i18next";
type VarimgVariantProps = {
questionId: string;
variant: QuestionVariantWithEditedImages;
index: number;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
questionLargeCheck: boolean;
isMulti: boolean;
answer: string | string[] | undefined;
ownPlaceholder: string;
};
interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
maxHeight: "44px",
overflow: "auto",
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: ownAnswer.length === 0 ? "ownPlaceholder" : theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const VarimgVariant = ({
questionId,
variant,
index,
isSending,
setIsSending,
questionLargeCheck,
ownPlaceholder,
answer,
}: VarimgVariantProps) => {
const theme = useTheme();
const { settings } = useQuizStore();
const { t } = useTranslation();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault();
updateAnswer(questionId, variant.id, variant.points || 0);
if (answer === variant.id) {
deleteAnswer(questionId);
}
};
if (variant?.isOwn) {
return (
<Box>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
{t("Enter your answer")}
</Typography>
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: 0,
justifyContent: "space-between",
"&:hover": { borderColor: theme.palette.primary.main },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
width: "100%",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
labelPlacement="start"
value={index}
onClick={sendVariant}
label={
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
}
/>
</Box>
);
} else {
return (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: 0,
justifyContent: "space-between",
"&:hover": { borderColor: theme.palette.primary.main },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
width: "100%",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
labelPlacement="start"
value={index}
onClick={sendVariant}
label={variant.answer}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
}
/>
);
}
};

@ -1,267 +0,0 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Box, ButtonBase, RadioGroup, Typography, useTheme, IconButton } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import { VarimgVariant } from "./VarimgVariant";
import { OwnVarimgImage } from "./OwnVarimgImage";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import BlankImage from "@icons/BlankImage";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import moment from "moment";
import { useTranslation } from "react-i18next";
type VarimgProps = {
currentQuestion: QuizQuestionVarImg;
};
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
const { t } = useTranslation();
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
const ownVariantInQuestion = useMemo(
() => currentQuestion.content.variants.find((v) => v.isOwn),
[currentQuestion.content.variants]
);
const ownVariantData = ownVariants.find((v) => v.id === answer);
const ownImageUrl = useMemo(() => {
return ownVariantData?.variant.localImageUrl;
}, [ownVariantData]);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const choiceImgUrlAnswer = useMemo(() => {
if (variant !== undefined) {
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return variant.extendedText;
}
}
}, [variant]);
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [variant]);
const handlePreviewAreaClick = () => {
// Загрузка возможна только если own вариант выбран
if (ownVariantInQuestion && answer === ownVariantInQuestion.id) {
inputRef.current?.click();
}
};
const handleRemoveImage = (e: React.MouseEvent) => {
e.stopPropagation();
if (ownVariantData) {
// Сохраняем текущий answer, очищаем только изображения
const currentAnswer = ownVariantData.variant.answer || "";
updateOwnVariant(ownVariantData.id, currentAnswer, "", "", "");
}
};
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: "30px",
alignItems: isMobile ? "center" : undefined,
}}
>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: "20px",
"&:focus": { color: theme.palette.text.primary },
"&:active": { color: theme.palette.text.primary },
}}
>
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<VarimgVariant
key={variant.id}
questionId={currentQuestion.id}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
questionLargeCheck={currentQuestion.content.largeCheck}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
isMulti={Boolean(currentQuestion.content?.multi)}
answer={answer}
/>
))}
{ownVariantInQuestion && (
<OwnVarimgImage
ref={inputRef}
questionId={currentQuestion.id}
variantId={ownVariantInQuestion.id}
/>
)}
</Box>
</RadioGroup>
<ButtonBase
onClick={handlePreviewAreaClick}
disabled={!ownVariantInQuestion || answer !== ownVariantInQuestion.id}
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF30",
color: theme.palette.text.primary,
textAlign: "center",
position: "relative",
"&:hover": {
backgroundColor:
ownVariantInQuestion && answer === ownVariantInQuestion.id ? "rgba(0,0,0,0.04)" : "transparent",
},
}}
>
{(() => {
if (answer) {
const imageUrl = variant?.isOwn && ownImageUrl ? ownImageUrl : choiceImgUrlAnswer;
if (imageUrl) {
return (
<>
<img
key={imageUrl}
src={imageUrl}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
{variant?.isOwn && ownImageUrl && (
<IconButton
onClick={handleRemoveImage}
sx={{
position: "absolute",
top: 8,
left: 8,
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
color: "white",
height: "25px",
width: "25px",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<CloseIcon />
</IconButton>
)}
</>
);
}
return (
<Box
sx={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<BlankImage />
{variant?.isOwn && (
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1,
}}
>
{t("Add your image")}
</Box>
)}
</Box>
);
}
if (choiceImgUrlQuestion && choiceImgUrlQuestion.trim().length > 0) {
return (
<img
src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
);
}
if (currentQuestion.content.replText && currentQuestion.content.replText.trim().length > 0) {
return currentQuestion.content.replText;
}
return isMobile ? t("Select an answer option below") : t("Select an answer option on the left");
})()}
</ButtonBase>
</Box>
</Box>
);
};

@ -1,136 +0,0 @@
import { useState, useEffect } from "react";
import { Select as MuiSelect, MenuItem, FormControl, Typography, useTheme } from "@mui/material";
import ArrowDown from "@icons/ArrowDownIcon";
import type { SelectChangeEvent, SxProps } from "@mui/material";
type SelectProps = {
items: string[];
activeItemIndex?: number;
empty?: boolean;
onChange?: (item: string, num: number) => void;
sx?: SxProps;
colorMain?: string;
colorPlaceholder?: string;
placeholder?: string;
};
export const Select = ({
items,
activeItemIndex = 0,
empty,
onChange,
sx,
placeholder = "",
colorMain = "#7E2AEA",
colorPlaceholder = "#9A9AAF",
}: SelectProps) => {
const [activeItem, setActiveItem] = useState<number>(empty ? -1 : activeItemIndex);
const theme = useTheme();
useEffect(() => {
setActiveItem(activeItemIndex);
}, [activeItemIndex]);
const handleChange = (event: SelectChangeEvent) => {
const newItemIndex = Number(event.target.value);
if (newItemIndex === activeItem) {
setActiveItem(-1);
onChange?.("", -1);
return;
}
setActiveItem(newItemIndex);
onChange?.(items[newItemIndex], newItemIndex);
};
return (
<FormControl
fullWidth
size="small"
sx={{ width: "100%", height: "48px", ...sx }}
>
<MuiSelect
displayEmpty
renderValue={(value) =>
value ? items[Number(value)] : <Typography sx={{ color: colorPlaceholder }}>{placeholder}</Typography>
}
id="display-select"
variant="outlined"
value={activeItem === -1 ? "" : String(activeItem)}
onChange={handleChange}
sx={{
width: "100%",
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${colorMain} !important`,
borderRadius: "10px",
},
"& .MuiSelect-icon": {
color: theme.palette.primary.main,
},
}}
MenuProps={{
PaperProps: {
sx: {
mt: "8px",
p: "4px",
borderRadius: "8px",
border: "1px solid #EEE4FC",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
},
},
MenuListProps: {
sx: {
py: 0,
display: "flex",
flexDirection: "column",
gap: "8px",
maxWidth: "1380px",
"& .Mui-selected": {
backgroundColor: "#F2F3F7",
color: colorMain,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.text.primary,
display: "block",
px: "9px",
gap: "20px",
"& .MuiTypography-root": {
overflow: "hidden",
textOverflow: "ellipsis",
},
},
}}
IconComponent={(props) => <ArrowDown {...props} />}
>
{items.map((item, index) => (
<MenuItem
key={item + index}
value={index}
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
padding: "10px",
borderRadius: "5px",
color: colorPlaceholder,
whiteSpace: "normal",
wordBreak: "break-word",
}}
>
{item}
</MenuItem>
))}
</MuiSelect>
</FormControl>
);
};

@ -27,7 +27,6 @@ export interface GetQuizDataResponse {
} }
export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> { export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> {
console.log(quizDataResponse);
const readyData = { const readyData = {
cnt: quizDataResponse.cnt, cnt: quizDataResponse.cnt,
show_badge: quizDataResponse.show_badge, show_badge: quizDataResponse.show_badge,
@ -66,7 +65,6 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
readyData.questions = items; readyData.questions = items;
if (quizDataResponse?.settings !== undefined) { if (quizDataResponse?.settings !== undefined) {
console.log("попытка парсануть сеттингс", quizDataResponse.settings);
readyData.settings = { readyData.settings = {
fp: quizDataResponse.settings.fp, fp: quizDataResponse.settings.fp,
rep: quizDataResponse.settings.rep, rep: quizDataResponse.settings.rep,

@ -98,21 +98,9 @@ export interface QuizQuestionBase {
}; };
} }
export type AnyTypedQuizQuestion = export type AnyTypedQuizQuestion = QuizQuestionText | QuizQuestionResult;
| QuizQuestionVariant
| QuizQuestionImages
| QuizQuestionVarImg
| QuizQuestionEmoji
| QuizQuestionText
| QuizQuestionSelect
| QuizQuestionDate
| QuizQuestionNumber
| QuizQuestionFile
| QuizQuestionPage
| QuizQuestionRating
| QuizQuestionResult;
export type RealTypedQuizQuestion = Exclude<AnyTypedQuizQuestion, QuizQuestionResult>; export type RealTypedQuizQuestion = Exclude<QuizQuestionText, QuizQuestionResult>;
type FilterQuestionsWithVariants<T> = T extends { type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[] }; content: { variants: QuestionVariant[] };

@ -25,22 +25,14 @@ export const useQuizStore = create<QuizStore>(() => ({
})); }));
export const setQuizData = (data: QuizSettings) => { export const setQuizData = (data: QuizSettings) => {
console.log("setQuizData called with:");
console.log("data:", data);
console.log("data.settings:", data.settings);
console.log("data.questions:", data.questions);
const currentState = useQuizStore.getState(); const currentState = useQuizStore.getState();
console.log("Current state before update:", currentState);
useQuizStore.setState((state: QuizStore) => { useQuizStore.setState((state: QuizStore) => {
const newState = { ...state, ...data }; const newState = { ...state, ...data };
console.log("New state after update:", newState);
return newState; return newState;
}); });
const updatedState = useQuizStore.getState(); const updatedState = useQuizStore.getState();
console.log("State after setState:", updatedState);
}; };
export const addQuestions = (newQuestions: AnyTypedQuizQuestion[]) => export const addQuestions = (newQuestions: AnyTypedQuizQuestion[]) =>

@ -13,6 +13,5 @@ const isProduction = !(
//туризм больше не в исключениях //туризм больше не в исключениях
if (!isProduction) domain = "https://s.hbpn.link"; if (!isProduction) domain = "https://s.hbpn.link";
domain = "https://hbpn.link";
export { domain, isProduction }; export { domain, isProduction };

@ -14,10 +14,7 @@ export function useAIQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt, quizStep } = useQuizStore(); const { settings, questions, quizId, cnt, quizStep } = useQuizStore();
useEffect(() => { useEffect(() => {}, [questions]);
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах //Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
@ -29,9 +26,6 @@ export function useAIQuiz() {
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber); const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
const currentQuestion = useMemo(() => { const currentQuestion = useMemo(() => {
console.log("выбор currentQuestion");
console.log("quizStep ", quizStep);
console.log("questions[quizStep] ", questions[quizStep]);
const calcQuestion = questions[quizStep]; const calcQuestion = questions[quizStep];
if (calcQuestion) { if (calcQuestion) {
vkMetrics.questionPassed(calcQuestion.id); vkMetrics.questionPassed(calcQuestion.id);
@ -44,8 +38,6 @@ export function useAIQuiz() {
useEffect(() => { useEffect(() => {
if (currentQuestion.type === "result") showResult(); if (currentQuestion.type === "result") showResult();
if (currentQuestion) changeNextLoading(false); if (currentQuestion) changeNextLoading(false);
console.log("questions");
console.log(questions);
}, [currentQuestion, questions]); }, [currentQuestion, questions]);
//Показать визуалом юзеру результат //Показать визуалом юзеру результат

@ -14,12 +14,6 @@ export function useBranchingQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt } = useQuizStore(); const { settings, questions, quizId, cnt } = useQuizStore();
useEffect(() => {
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
console.log(questions);
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
const sortedQuestions = useMemo(() => { const sortedQuestions = useMemo(() => {
@ -236,10 +230,6 @@ export function useBranchingQuiz() {
if ("required" in currentQuestion.content && currentQuestion.content.required) { if ("required" in currentQuestion.content && currentQuestion.content.required) {
return hasAnswer; return hasAnswer;
} }
console.log(linearQuestionIndex);
console.log(questions.length);
console.log(cnt);
if (linearQuestionIndex !== null && questions.length < cnt) return true; if (linearQuestionIndex !== null && questions.length < cnt) return true;
return Boolean(nextQuestion); return Boolean(nextQuestion);
}, [answers, currentQuestion, nextQuestion]); }, [answers, currentQuestion, nextQuestion]);

@ -14,12 +14,6 @@ export function useLinearQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt } = useQuizStore(); const { settings, questions, quizId, cnt } = useQuizStore();
useEffect(() => {
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
console.log(questions);
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
const sortedQuestions = useMemo(() => { const sortedQuestions = useMemo(() => {
@ -236,10 +230,6 @@ export function useLinearQuiz() {
if ("required" in currentQuestion.content && currentQuestion.content.required) { if ("required" in currentQuestion.content && currentQuestion.content.required) {
return hasAnswer; return hasAnswer;
} }
console.log(linearQuestionIndex);
console.log(questions.length);
console.log(cnt);
if (linearQuestionIndex !== null && questions.length < cnt) return true; if (linearQuestionIndex !== null && questions.length < cnt) return true;
return Boolean(nextQuestion); return Boolean(nextQuestion);
}, [answers, currentQuestion, nextQuestion]); }, [answers, currentQuestion, nextQuestion]);

@ -2,7 +2,6 @@ import { sendAnswer } from "@/api/quizRelase";
import { RealTypedQuizQuestion } from "@/model/questionTypes/shared"; import { RealTypedQuizQuestion } from "@/model/questionTypes/shared";
import { OwnVariant, QuestionAnswer, createQuizViewStore } from "@/stores/quizView"; import { OwnVariant, QuestionAnswer, createQuizViewStore } from "@/stores/quizView";
import moment from "moment"; import moment from "moment";
import { notReachable } from "./notReachable";
export async function sendQuestionAnswer( export async function sendQuestionAnswer(
quizId: string, quizId: string,
@ -17,202 +16,8 @@ export async function sendQuestionAnswer(
qid: quizId, qid: quizId,
}); });
} }
switch (question.type) {
case "date": {
let answer = "";
if (question.content.isRange) { if (question.type === "text") {
if (!Array.isArray(questionAnswer.answer)) throw new Error("Cannot send answer in range question");
let from = Number(questionAnswer.answer[0]);
let to = Number(questionAnswer.answer[1]);
if (
from !== 0 &&
to !== 0 &&
from !== Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]))
) {
from = Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
to = Math.max(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
}
answer = `${!from ? "_" : moment(from).format("YYYY.MM.DD")} - ${!to ? "_" : moment(to).format("YYYY.MM.DD")}`;
} else {
if (!moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send answer in date question");
answer = moment(questionAnswer.answer).format("YYYY.MM.DD");
}
return sendAnswer({
questionId: question.id,
body: answer,
qid: quizId,
});
}
case "emoji": {
if (question.content.multi) {
const answer = questionAnswer.answer as string[];
let answerString = ``;
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
selectedVariants.forEach((variant) => {
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
const customEmoji = ownVariantData?.extendedText || "";
const emojiToSend = customEmoji || variant.extendedText;
const textToSend = variant.isOwn ? ownVariantData?.answer || "" : variant.answer;
answerString += `\`${emojiToSend} ${textToSend}\`,`;
});
answerString = answerString.slice(0, -1);
return sendAnswer({
questionId: question.id,
body: answerString,
qid: quizId,
});
}
// Fallback for old string format for single choice
const answer = questionAnswer.answer as string;
const variant = question.content.variants.find((v) => v.id === answer);
if (!variant) {
// This can happen if the answer is not set, so we don't throw an error, just send empty
return sendAnswer({
questionId: question.id,
body: "",
qid: quizId,
});
}
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
const customEmoji = ownVariantData?.extendedText || "";
const emojiToSend = customEmoji || variant.extendedText;
const textToSend = variant.isOwn ? ownVariantData?.answer || "" : variant.answer;
const body = `${emojiToSend} ${textToSend}`.trim();
return sendAnswer({
questionId: question.id,
body: body,
qid: quizId,
});
}
case "file": {
return;
}
case "images": {
if (question.content.multi) {
const answer = questionAnswer.answer;
const ownAnswer = Array.isArray(answer)
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
?.answer || ""
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
"";
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
//Оставляем только выбранные варианты
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
let answerString = ``;
selectedVariants.forEach((e) => {
if (!e.isOwn || (e.isOwn && question.content.own)) {
let imageValue = e.extendedText;
if (e.isOwn) {
// Берем fileId из ownVariants для own вариантов
const ownVariantData = ownVariants.find((v) => v.id === e.id)?.variant;
if (ownVariantData?.originalImageUrl) {
// Конструируем полный URL для own вариантов
const baseUrl =
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
// Убираем расширение файла из fileId
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(
/\.(jpg|jpeg|png|gif|webp)$/i,
""
);
imageValue = baseUrl + fileIdWithoutExtension;
}
}
const body = {
Image: imageValue,
Description: e.isOwn ? ownAnswer : e.answer,
};
answerString += `\`${JSON.stringify(body)}\`,`;
}
});
answerString = answerString.slice(0, -1);
return sendAnswer({
questionId: question.id,
body: answerString,
qid: quizId,
});
}
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
let imageValue = variant.extendedText;
if (variant.isOwn) {
// Берем fileId из ownVariants для own вариантов
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
if (ownVariantData?.originalImageUrl) {
// Конструируем полный URL для own вариантов
const baseUrl =
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
// Убираем расширение файла из fileId
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
imageValue = baseUrl + fileIdWithoutExtension;
}
}
const body = {
Image: imageValue,
Description: variant.answer,
};
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
return sendAnswer({
questionId: question.id,
body: `\`${JSON.stringify(body)}\``,
qid: quizId,
});
}
case "number": {
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
return sendAnswer({
questionId: question.id,
body: questionAnswer.answer,
qid: quizId,
});
}
case "page": {
return;
}
case "rating": {
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
return sendAnswer({
questionId: question.id,
body: String(questionAnswer.answer) + " из " + question.content.steps,
qid: quizId,
});
}
case "select": {
if (typeof questionAnswer.answer !== "string") throw new Error("Cannot send answer in select question");
const variant = question.content.variants[Number(questionAnswer.answer)];
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
return sendAnswer({
questionId: question.id,
body: variant.answer,
qid: quizId,
});
}
case "text": {
if (moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send Moment in text question"); if (moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send Moment in text question");
return sendAnswer({ return sendAnswer({
@ -220,81 +25,5 @@ export async function sendQuestionAnswer(
body: questionAnswer.answer, body: questionAnswer.answer,
qid: quizId, qid: quizId,
}); });
} } else throw new Error("Inappropriate question type");
case "variant": {
if (question.content.multi) {
const answer = questionAnswer.answer;
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
const ownAnswer = Array.isArray(answer)
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
?.answer || ""
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
"";
//Оставляем только выбранные варианты
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
let answerString = ``;
selectedVariants.forEach((e) => {
if (!e.isOwn) answerString += `\`${e.answer}\`,`;
});
if (question.content.own && selectedVariants.some((v) => v.isOwn)) {
answerString += `\`${ownAnswer}\`,`;
}
answerString = answerString.slice(0, -1);
return sendAnswer({
questionId: question.id,
body: answerString,
qid: quizId,
});
}
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
return sendAnswer({
questionId: question.id,
body: variant.answer,
qid: quizId,
});
}
case "varimg": {
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
const ownAnswer =
ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer || "";
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
let imageValue = variant.extendedText;
if (variant.isOwn) {
// Берем fileId из ownVariants для own вариантов
const ownVariantData = ownVariants.find((v) => v.id === variant.id)?.variant;
if (ownVariantData?.originalImageUrl) {
// Конструируем полный URL для own вариантов
const baseUrl =
"https://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/55c25eb9-4533-4d51-9da5-54e63e8aeace/";
// Убираем расширение файла из fileId
const fileIdWithoutExtension = ownVariantData.originalImageUrl.replace(/\.(jpg|jpeg|png|gif|webp)$/i, "");
imageValue = baseUrl + fileIdWithoutExtension;
}
}
const body = {
Image: imageValue,
Description: variant.isOwn ? ownAnswer : variant.answer,
};
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
return sendAnswer({
questionId: question.id,
body: `\`${JSON.stringify(body)}\``,
qid: quizId,
});
}
default:
notReachable(question);
}
} }

@ -64,9 +64,7 @@ i18n
}); });
// 3. Логирование всех событий // 3. Логирование всех событий
i18n.on("languageChanged", (lng) => { i18n.on("languageChanged", (lng) => {});
console.log("Язык изменён на:", lng);
});
i18n.on("failedLoading", (lng, ns, msg) => { i18n.on("failedLoading", (lng, ns, msg) => {
console.error(`Ошибка загрузки ${lng}.json:`, msg); console.error(`Ошибка загрузки ${lng}.json:`, msg);

@ -197,9 +197,7 @@ const r = {
}; };
// 3. Конфигурация i18n без Backend // 3. Конфигурация i18n без Backend
i18n i18n.use(initReactI18next).init({
.use(initReactI18next)
.init({
resources: r, // Используем встроенные переводы resources: r, // Используем встроенные переводы
lng: getLanguageFromURL(), lng: getLanguageFromURL(),
fallbackLng: "ru", fallbackLng: "ru",
@ -228,18 +226,6 @@ i18n
stack: new Error().stack, stack: new Error().stack,
}); });
}, },
})
.then(() => {
console.log("i18n initialized. Current language:", i18n.language);
console.log("Available languages:", i18n.languages);
console.log("Available keys for ru:", Object.keys(r.ru));
console.log("Available keys for en:", Object.keys(r.en));
console.log("Available keys for uz:", Object.keys(r.uz));
});
// 4. Логирование событий
i18n.on("languageChanged", (lng) => {
console.log("Language changed to:", lng);
}); });
export default i18n; export default i18n;

47250
widget_en.js

File diff suppressed because one or more lines are too long