send answers only on next question click

This commit is contained in:
nflnkr 2024-06-29 12:32:16 +03:00
parent d7d20ed5a0
commit 821213a14c
23 changed files with 509 additions and 550 deletions

@ -78,23 +78,26 @@ export async function getData(quizId: string): Promise<{
error?: AxiosError; error?: AxiosError;
}> { }> {
try { try {
const { data, headers } = await axios<GetQuizDataResponse>(domain + `/answer/v1.0.0/settings${window.location.search}`, { const { data, headers } = await axios<GetQuizDataResponse>(
method: "POST", domain + `/answer/v1.0.0/settings${window.location.search}`,
headers: { {
"X-Sessionkey": SESSIONS, method: "POST",
"Content-Type": "application/json", headers: {
DeviceType: DeviceType, "X-Sessionkey": SESSIONS,
Device: Device, "Content-Type": "application/json",
OS: OSDevice, DeviceType: DeviceType,
Browser: userAgent, Device: Device,
}, OS: OSDevice,
data: { Browser: userAgent,
quiz_id: quizId, },
limit: 100, data: {
page: 0, quiz_id: quizId,
need_config: true, limit: 100,
}, page: 0,
}); need_config: true,
},
}
);
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
//Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки //Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки
@ -143,10 +146,10 @@ type SendAnswerProps = {
questionId: string; questionId: string;
body: string | string[]; body: string | string[];
qid: string; qid: string;
preview: boolean; preview?: boolean;
}; };
export function sendAnswer({ questionId, body, qid, preview }: SendAnswerProps) { export function sendAnswer({ questionId, body, qid, preview = false }: SendAnswerProps) {
if (preview) return; if (preview) return;
const formData = new FormData(); const formData = new FormData();

@ -167,6 +167,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
useEffect(() => { useEffect(() => {
vkMetrics.contactsFormOpened(); vkMetrics.contactsFormOpened();
yandexMetrics.contactsFormOpened(); yandexMetrics.contactsFormOpened();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
@ -274,11 +275,17 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
fontSize={"16px"} fontSize={"16px"}
> >
С&ensp; С&ensp;
<Link href={"https://shub.pena.digital/ppdd"} target="_blank"> <Link
href={"https://shub.pena.digital/ppdd"}
target="_blank"
>
Положением об обработке персональных данных{" "} Положением об обработке персональных данных{" "}
</Link> </Link>
&ensp;и&ensp; &ensp;и&ensp;
<Link href={"https://shub.pena.digital/docs/privacy"} target="_blank"> <Link
href={"https://shub.pena.digital/docs/privacy"}
target="_blank"
>
{" "} {" "}
Политикой конфиденциальности{" "} Политикой конфиденциальности{" "}
</Link> </Link>

@ -3,7 +3,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts";
import { useQuizSettings } from "@contexts/QuizDataContext.ts"; import { useQuizSettings } from "@contexts/QuizDataContext.ts";
import { useIMask } from "react-imask"; import { useIMask } from "react-imask";
import { quizThemes } from "@utils/themes/Publication/themePublication.ts"; import { quizThemes } from "@utils/themes/Publication/themePublication.ts";
import { FC, useState } from "react"; import { FC, HTMLInputTypeAttribute, useState } from "react";
import { CountrySelector } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx"; import { CountrySelector } from "@/components/ViewPublicationPage/ContactForm/CustomInput/CountrySelector/CountrySelector.tsx";
import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx"; import { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx";
@ -14,11 +14,12 @@ type InputProps = {
onChange: TextFieldProps["onChange"]; onChange: TextFieldProps["onChange"];
id: string; id: string;
isPhone?: boolean; isPhone?: boolean;
type?: HTMLInputTypeAttribute;
}; };
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
export const CustomInput = ({ title, desc, Icon, onChange, isPhone }: InputProps) => { export const CustomInput = ({ title, desc, Icon, onChange, isPhone, type }: InputProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 600; const isMobile = useRootContainerSize() < 600;
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
@ -26,13 +27,18 @@ export const CustomInput = ({ title, desc, Icon, onChange, isPhone }: InputProps
const { ref } = useIMask({ mask }); const { ref } = useIMask({ mask });
return ( return (
<Box m="10px 0"> <Box m="10px 0">
<Typography mb="7px" color={theme.palette.text.primary} fontSize={"16px"}> <Typography
mb="7px"
color={theme.palette.text.primary}
fontSize={"16px"}
>
{title} {title}
</Typography> </Typography>
<TextField <TextField
inputRef={isPhone ? ref : null} inputRef={isPhone ? ref : null}
onChange={onChange} onChange={onChange}
type={type}
sx={{ sx={{
width: isMobile ? "100%" : "390px", width: isMobile ? "100%" : "390px",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
@ -57,7 +63,10 @@ export const CustomInput = ({ title, desc, Icon, onChange, isPhone }: InputProps
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<Icon color="gray" backgroundColor={quizThemes[settings.cfg.theme].isLight ? "#F2F3F7" : "#F2F3F71A"} /> <Icon
color="gray"
backgroundColor={quizThemes[settings.cfg.theme].isLight ? "#F2F3F7" : "#F2F3F71A"}
/>
</InputAdornment> </InputAdornment>
), ),
endAdornment: ( endAdornment: (

@ -52,6 +52,7 @@ export const Inputs = ({
title={FC["email"].innerText || "Введите Email"} title={FC["email"].innerText || "Введите Email"}
desc={FC["email"].text || "Email"} desc={FC["email"].text || "Email"}
Icon={EmailIcon} Icon={EmailIcon}
type="email"
/> />
); );
const Phone = ( const Phone = (

@ -2,7 +2,7 @@ import { ContactForm } from "@/components/ViewPublicationPage/ContactForm/Contac
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";
import { sendAnswer } from "@api/quizRelase"; import { sendQuestionAnswer } from "@/utils/sendQuestionAnswer";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { ThemeProvider, Typography } from "@mui/material"; import { ThemeProvider, Typography } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
@ -37,8 +37,6 @@ export default function ViewPublicationPage() {
useYandexMetrics(settings?.cfg?.yandexMetricsNumber); useYandexMetrics(settings?.cfg?.yandexMetricsNumber);
useVKMetrics(settings?.cfg?.vkMetricsNumber); useVKMetrics(settings?.cfg?.vkMetricsNumber);
const isAnswer = answers.some((ans) => ans.questionId === currentQuestion?.id);
useEffect( useEffect(
function setFaviconAndTitle() { function setFaviconAndTitle() {
if (!changeFaviconAndTitle) return; if (!changeFaviconAndTitle) return;
@ -68,6 +66,8 @@ export default function ViewPublicationPage() {
</ThemeProvider> </ThemeProvider>
); );
const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
let quizStepElement: ReactElement; let quizStepElement: ReactElement;
switch (currentQuizStep) { switch (currentQuizStep) {
case "startpage": { case "startpage": {
@ -94,20 +94,15 @@ export default function ViewPublicationPage() {
nextButton={ nextButton={
<NextButton <NextButton
isNextButtonEnabled={isNextButtonEnabled} isNextButtonEnabled={isNextButtonEnabled}
moveToNextQuestion={async () => { moveToNextQuestion={() => {
if (!isAnswer) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
moveToNextQuestion(); moveToNextQuestion();
if (!currentAnswer || preview) return;
sendQuestionAnswer(quizId, currentQuestion, currentAnswer)?.catch((e) => {
enqueueSnackbar("Ошибка при отправке ответа");
console.error("Error sending answer", e);
});
}} }}
/> />
} }

@ -1,27 +1,19 @@
import { useState } from "react";
import moment from "moment";
import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@/stores/quizView"; import { useQuizViewStore } from "@/stores/quizView";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import CalendarIcon from "@icons/CalendarIcon"; import CalendarIcon from "@icons/CalendarIcon";
import type { Moment } from "moment";
import type { QuizQuestionDate } from "@model/questionTypes/date"; 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 = { type DateProps = {
currentQuestion: QuizQuestionDate; currentQuestion: QuizQuestionDate;
}; };
export const Date = ({ currentQuestion }: DateProps) => { export const Date = ({ currentQuestion }: DateProps) => {
const [isSending, setIsSending] = useState<boolean>(false); const { settings } = useQuizSettings();
const { settings, quizId, preview } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
@ -29,28 +21,18 @@ export const Date = ({ currentQuestion }: DateProps) => {
const currentAnswer = moment(answer) || moment(); const currentAnswer = moment(answer) || moment();
const onDateChange = async (date: Moment | null) => { const onDateChange = async (date: Moment | null) => {
if (isSending || !date) return; if (!date) return;
setIsSending(true); updateAnswer(currentQuestion.id, date, 0);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: moment(date).format("YYYY.MM.DD"),
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, date, 0);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}; };
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box

@ -1,80 +1,41 @@
import { Box, FormControl, FormControlLabel, Radio, Typography, useTheme } from "@mui/material"; import type { QuestionVariant } from "@/model/questionTypes/shared";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { enqueueSnackbar } from "notistack";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { Box, FormControl, FormControlLabel, Radio, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionEmoji } from "@model/questionTypes/emoji";
polyfillCountryFlagEmojis(); polyfillCountryFlagEmojis();
type EmojiVariantProps = { type EmojiVariantProps = {
currentQuestion: QuizQuestionEmoji; questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
index: number; index: number;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
}; };
export const EmojiVariant = ({ currentQuestion, variant, index, isSending, setIsSending }: EmojiVariantProps) => { export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps) => {
const { quizId, settings, preview } = useQuizSettings(); const { settings } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {};
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => { const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
if (isSending) return;
setIsSending(true); updateAnswer(questionId, variant.id, variant.points || 0);
try {
await sendAnswer({
questionId: currentQuestion.id,
body:
currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer,
qid: quizId,
preview,
});
updateAnswer( if (answer === variant.id) {
currentQuestion.id, deleteAnswer(questionId);
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
} }
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}; };
return ( return (
<FormControl <FormControl
key={index} key={index}
disabled={isSending}
sx={{ sx={{
borderRadius: "12px", borderRadius: "12px",
border: `1px solid`, border: `1px solid`,

@ -1,10 +1,7 @@
import { useState } from "react";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { useQuizViewStore } from "@stores/quizView";
import type { QuizQuestionEmoji } from "@model/questionTypes/emoji"; 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 { EmojiVariant } from "./EmojiVariant";
polyfillCountryFlagEmojis(); polyfillCountryFlagEmojis();
@ -14,7 +11,6 @@ type EmojiProps = {
}; };
export const Emoji = ({ currentQuestion }: EmojiProps) => { export const Emoji = ({ currentQuestion }: EmojiProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
@ -22,7 +18,11 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<RadioGroup <RadioGroup
@ -47,10 +47,8 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants.map((variant, index) => (
<EmojiVariant <EmojiVariant
key={variant.id} key={variant.id}
currentQuestion={currentQuestion} questionId={currentQuestion.id}
variant={variant} variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index} index={index}
/> />
))} ))}

@ -1,69 +1,33 @@
import { Box, FormControlLabel, Radio, useTheme } from "@mui/material"; import type { QuestionVariant } from "@/model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { Box, FormControlLabel, Radio, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionImages } from "@model/questionTypes/images";
type ImagesProps = { type ImagesProps = {
currentQuestion: QuizQuestionImages; questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
index: number; index: number;
}; };
export const ImageVariant = ({ currentQuestion, variant, isSending, setIsSending, index }: ImagesProps) => { export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
const { settings, quizId, preview } = useQuizSettings(); const { settings } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state); const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer; const answer = answers.find((answer) => answer.questionId === questionId)?.answer;
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => { const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
if (isSending) return;
setIsSending(true); updateAnswer(questionId, variant.id, variant.points || 0);
try {
await sendAnswer({ if (answer === variant.id) {
questionId: currentQuestion.id, deleteAnswer(questionId);
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
} }
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}; };
return ( return (

@ -1,19 +1,14 @@
import { useState } from "react";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { ImageVariant } from "./ImageVariant";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { QuizQuestionImages } from "@model/questionTypes/images"; import type { QuizQuestionImages } from "@model/questionTypes/images";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import { ImageVariant } from "./ImageVariant";
type ImagesProps = { type ImagesProps = {
currentQuestion: QuizQuestionImages; currentQuestion: QuizQuestionImages;
}; };
export const Images = ({ currentQuestion }: ImagesProps) => { export const Images = ({ currentQuestion }: ImagesProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme(); const theme = useTheme();
const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
@ -22,7 +17,11 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<RadioGroup <RadioGroup
@ -47,10 +46,8 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants.map((variant, index) => (
<ImageVariant <ImageVariant
key={variant.id} key={variant.id}
currentQuestion={currentQuestion} questionId={currentQuestion.id}
variant={variant} variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index} index={index}
/> />
))} ))}

@ -1,34 +1,26 @@
import { useQuizSettings } from "@contexts/QuizDataContext";
import type { QuizQuestionNumber } from "@model/questionTypes/number";
import { Box, Typography, useTheme } from "@mui/material"; import { Box, Typography, useTheme } from "@mui/material";
import { useEffect, useState } from "react"; import { useQuizViewStore } from "@stores/quizView";
import { useDebouncedCallback } from "use-debounce";
import { CustomSlider } from "@ui_kit/CustomSlider"; import { CustomSlider } from "@ui_kit/CustomSlider";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import type { QuizQuestionNumber } from "@model/questionTypes/number";
import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent, SyntheticEvent } from "react"; import type { ChangeEvent, SyntheticEvent } from "react";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
type NumberProps = { type NumberProps = {
currentQuestion: QuizQuestionNumber; currentQuestion: QuizQuestionNumber;
}; };
export const Number = ({ currentQuestion }: NumberProps) => { export const Number = ({ currentQuestion }: NumberProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>("0"); const [inputValue, setInputValue] = useState<string>("0");
const [minRange, setMinRange] = useState<string>("0"); const [minRange, setMinRange] = useState<string>("0");
const [maxRange, setMaxRange] = useState<string>("100000000000"); const [maxRange, setMaxRange] = useState<string>("100000000000");
const [reversedInputValue, setReversedInputValue] = useState<string>("0"); const [reversedInputValue, setReversedInputValue] = useState<string>("0");
const [reversedMinRange, setReversedMinRange] = useState<string>("0"); const [reversedMinRange, setReversedMinRange] = useState<string>("0");
const [reversedMaxRange, setReversedMaxRange] = useState<string>("100000000000"); const [reversedMaxRange, setReversedMaxRange] = useState<string>("100000000000");
const { settings, quizId, preview } = useQuizSettings(); const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme(); const theme = useTheme();
@ -49,23 +41,9 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}, [reversed]); }, [reversed]);
const sendAnswerToBackend = async (value: string, noUpdate = false) => { const sendAnswerToBackend = async (value: string, noUpdate = false) => {
setIsSending(true); if (!noUpdate) {
try { updateAnswer(currentQuestion.id, value, 0);
await sendAnswer({
questionId: currentQuestion.id,
body: value,
qid: quizId,
preview,
});
if (!noUpdate) {
updateAnswer(currentQuestion.id, value, 0);
}
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
} }
setIsSending(false);
}; };
const updateValueDebounced = useDebouncedCallback(async (value: string) => { const updateValueDebounced = useDebouncedCallback(async (value: string) => {
@ -180,6 +158,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
setReversedInputValue(String(currentQuestion.content.start)); setReversedInputValue(String(currentQuestion.content.start));
setInputValue(String(currentQuestion.content.start)); setInputValue(String(currentQuestion.content.start));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const onSliderChange = (_: Event, value: number | number[]) => { const onSliderChange = (_: Event, value: number | number[]) => {
@ -305,7 +284,11 @@ export const Number = ({ currentQuestion }: NumberProps) => {
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box

@ -1,12 +1,4 @@
import { useState } from "react";
import { Box, Rating as RatingComponent, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizSettings } from "@contexts/QuizDataContext";
import FlagIcon from "@icons/questionsPage/FlagIcon"; import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini"; import StarIconMini from "@icons/questionsPage/StarIconMini";
import HashtagIcon from "@icons/questionsPage/hashtagIcon"; import HashtagIcon from "@icons/questionsPage/hashtagIcon";
@ -14,37 +6,73 @@ import HeartIcon from "@icons/questionsPage/heartIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon"; import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import LikeIcon from "@icons/questionsPage/likeIcon"; import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon"; import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "@model/questionTypes/rating"; 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 = [ const RATING_FORM_BUTTONS = [
{ {
name: "star", name: "star",
icon: (color: string, width: number) => <StarIconMini width={width} color={color} />, icon: (color: string, width: number) => (
<StarIconMini
width={width}
color={color}
/>
),
}, },
{ {
name: "trophie", name: "trophie",
icon: (color: string, width: number) => <TropfyIcon width={width} color={color} />, icon: (color: string, width: number) => (
<TropfyIcon
width={width}
color={color}
/>
),
}, },
{ {
name: "flag", name: "flag",
icon: (color: string, width: number) => <FlagIcon width={width} color={color} />, icon: (color: string, width: number) => (
<FlagIcon
width={width}
color={color}
/>
),
}, },
{ {
name: "heart", name: "heart",
icon: (color: string, width: number) => <HeartIcon width={width} color={color} />, icon: (color: string, width: number) => (
<HeartIcon
width={width}
color={color}
/>
),
}, },
{ {
name: "like", name: "like",
icon: (color: string, width: number) => <LikeIcon width={width} color={color} />, icon: (color: string, width: number) => (
<LikeIcon
width={width}
color={color}
/>
),
}, },
{ {
name: "bubble", name: "bubble",
icon: (color: string, width: number) => <LightbulbIcon width={width} color={color} />, icon: (color: string, width: number) => (
<LightbulbIcon
width={width}
color={color}
/>
),
}, },
{ {
name: "hashtag", name: "hashtag",
icon: (color: string, width: number) => <HashtagIcon width={width} color={color} />, icon: (color: string, width: number) => (
<HashtagIcon
width={width}
color={color}
/>
),
}, },
]; ];
@ -53,38 +81,26 @@ type RatingProps = {
}; };
export const Rating = ({ currentQuestion }: RatingProps) => { export const Rating = ({ currentQuestion }: RatingProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const { quizId, preview } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 750; const isTablet = useRootContainerSize() < 750;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const form = RATING_FORM_BUTTONS.find(({ name }) => name === currentQuestion.content.form); const form = RATING_FORM_BUTTONS.find(({ name }) => name === currentQuestion.content.form);
const sendRating = async (value: number | null) => { const sendRating = async (value: number | null) => {
setIsSending(true); updateAnswer(currentQuestion.id, String(value), 0);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: String(value) + " из " + currentQuestion.content.steps,
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}; };
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box
@ -98,7 +114,6 @@ export const Rating = ({ currentQuestion }: RatingProps) => {
> >
<Box sx={{ display: "inline-block", width: "100%" }}> <Box sx={{ display: "inline-block", width: "100%" }}>
<RatingComponent <RatingComponent
disabled={isSending}
value={Number(answer || 0)} value={Number(answer || 0)}
onChange={(_, value) => sendRating(value)} onChange={(_, value) => sendRating(value)}
sx={{ sx={{

@ -1,68 +1,38 @@
import { useState } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { Select as SelectComponent } from "@/components/ViewPublicationPage/tools/Select"; import { Select as SelectComponent } from "@/components/ViewPublicationPage/tools/Select";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { QuizQuestionSelect } from "@model/questionTypes/select"; 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 = { type SelectProps = {
currentQuestion: QuizQuestionSelect; currentQuestion: QuizQuestionSelect;
}; };
export const Select = ({ currentQuestion }: SelectProps) => { export const Select = ({ currentQuestion }: SelectProps) => {
const [isSending, setIsSending] = useState<boolean>(false); const { settings } = useQuizSettings();
const { quizId, settings, preview } = useQuizSettings();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const sendSelectedAnswer = async (value: number) => { const sendSelectedAnswer = async (value: number) => {
setIsSending(true);
if (value < 0) { if (value < 0) {
deleteAnswer(currentQuestion.id); deleteAnswer(currentQuestion.id);
try { return;
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
return setIsSending(false);
} }
try { updateAnswer(currentQuestion.id, String(value), 0);
await sendAnswer({
questionId: currentQuestion.id,
body: String(currentQuestion.content.variants[Number(value)].answer),
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}; };
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box
@ -74,7 +44,6 @@ export const Select = ({ currentQuestion }: SelectProps) => {
}} }}
> >
<SelectComponent <SelectComponent
disabled={isSending}
placeholder={currentQuestion.content.default} placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1} activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)} items={currentQuestion.content.variants.map(({ answer }) => answer)}

@ -14,11 +14,10 @@ import type { QuizQuestionText } from "@model/questionTypes/text";
interface TextNormalProps { interface TextNormalProps {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
answer?: Answer; answer?: Answer;
inputHC: (text: string) => void;
stepNumber?: number | null; stepNumber?: number | null;
} }
export const TextNormal = ({ currentQuestion, answer, inputHC }: TextNormalProps) => { export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
@ -26,12 +25,15 @@ export const TextNormal = ({ currentQuestion, answer, inputHC }: TextNormalProps
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => { const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0); updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}; };
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box

@ -41,11 +41,10 @@ const ORIENTATION = [
interface TextSpecialProps { interface TextSpecialProps {
currentQuestion: QuizQuestionText; currentQuestion: QuizQuestionText;
answer?: Answer; answer?: Answer;
inputHC: (text: string) => void;
stepNumber?: number | null; stepNumber?: number | null;
} }
export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: TextSpecialProps) => { export const TextSpecial = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const isHorizontal = ORIENTATION[Number(stepNumber) - 1].horizontal; const isHorizontal = ORIENTATION[Number(stepNumber) - 1].horizontal;
@ -54,7 +53,6 @@ export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: Te
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => { const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0); updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}; };
return ( return (
@ -75,7 +73,11 @@ export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: Te
gap: "20px", gap: "20px",
}} }}
> >
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
{isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && ( {isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && (

@ -1,13 +1,7 @@
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { enqueueSnackbar } from "notistack";
import { TextSpecial } from "./TextSpecial";
import { TextNormal } from "./TextNormal";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { useQuizViewStore } from "@stores/quizView";
import { TextNormal } from "./TextNormal";
import { TextSpecial } from "./TextSpecial";
import type { QuizQuestionText } from "@model/questionTypes/text"; import type { QuizQuestionText } from "@model/questionTypes/text";
@ -17,42 +11,34 @@ type TextProps = {
}; };
export const Text = ({ currentQuestion, stepNumber }: TextProps) => { export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const [isSending, setIsSending] = useState<boolean>(false); const { settings } = useQuizSettings();
const { settings, preview, quizId } = useQuizSettings();
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) ?? {};
const inputHC = useDebouncedCallback(async (text) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: text,
qid: quizId,
preview,
});
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}, 400);
useEffect(() => {
inputHC.flush();
}, [inputHC]);
switch (settings.cfg.spec) { switch (settings.cfg.spec) {
case true: case true:
return ( return (
<TextSpecial currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} stepNumber={stepNumber} /> <TextSpecial
currentQuestion={currentQuestion}
answer={answer}
stepNumber={stepNumber}
/>
); );
case undefined: case undefined:
return <TextNormal currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} />; return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
/>
);
default: default:
return <TextNormal currentQuestion={currentQuestion} answer={answer} inputHC={inputHC} />; return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
/>
);
} }
}; };

@ -1,123 +1,61 @@
import { Checkbox, FormControlLabel, TextField as MuiTextField, Radio, TextFieldProps, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { CheckboxIcon } from "@icons/Checkbox"; import { CheckboxIcon } from "@icons/Checkbox";
import type { QuestionVariant } from "@model/questionTypes/shared";
import { Checkbox, FormControlLabel, TextField as MuiTextField, Radio, TextFieldProps, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { FC, MouseEvent } from "react"; import type { FC, MouseEvent } from "react";
import type { QuestionVariant } from "@model/questionTypes/shared";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
export const VariantItem = ({ export const VariantItem = ({
currentQuestion, questionId,
isMulti,
variant, variant,
answer, answer,
index, index,
own = false, own = false,
isSending,
setIsSending,
}: { }: {
currentQuestion: QuizQuestionVariant; isMulti: boolean;
questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
answer: string | string[] | undefined; answer: string | string[] | undefined;
index: number; index: number;
own?: boolean; own?: boolean;
isSending: boolean;
setIsSending: (a: boolean) => void;
}) => { }) => {
const { settings, quizId, preview } = useQuizSettings(); const { settings } = useQuizSettings();
const theme = useTheme(); const theme = useTheme();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => { const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault(); event.preventDefault();
if (isSending) { const variantId = variant.id;
return;
}
setIsSending(true); if (isMulti) {
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : []; const currentAnswer = typeof answer !== "string" ? answer || [] : [];
try { return updateAnswer(
await sendAnswer({ questionId,
questionId: currentQuestion.id, currentAnswer.includes(variantId)
body: currentAnswer.includes(variantId) ? currentAnswer?.filter((item) => item !== variantId)
? currentAnswer?.filter((item) => item !== variantId) : [...currentAnswer, variantId],
: [...currentAnswer, variantId], variant.points || 0
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
currentQuestion.content.variants[index].points || 0
);
} catch (error) {
console.error(error);
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
return;
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentQuestion.content.variants[index].answer,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
variantId,
answer === variantId ? 0 : currentQuestion.content.variants[index].points || 0
); );
} catch (error) {
console.error(error);
enqueueSnackbar("ответ не был засчитан");
} }
updateAnswer(questionId, variantId, answer === variantId ? 0 : variant.points || 0);
if (answer === variantId) { if (answer === variantId) {
try { deleteAnswer(questionId);
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
console.error(error);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
} }
setIsSending(false);
}; };
return ( return (
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
disabled={isSending}
sx={{ sx={{
margin: "0", margin: "0",
borderRadius: "12px", borderRadius: "12px",
@ -154,14 +92,22 @@ export const VariantItem = ({
value={index} value={index}
labelPlacement="start" labelPlacement="start"
control={ control={
currentQuestion.content.multi ? ( isMulti ? (
<Checkbox <Checkbox
checked={!!answer?.includes(variant.id)} checked={!!answer?.includes(variant.id)}
checkedIcon={<CheckboxIcon checked color={theme.palette.primary.main} />} checkedIcon={
<CheckboxIcon
checked
color={theme.palette.primary.main}
/>
}
icon={<CheckboxIcon />} icon={<CheckboxIcon />}
/> />
) : ( ) : (
<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} /> <Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
) )
} }
label={own ? <TextField label="Другое..." /> : variant.answer} label={own ? <TextField label="Другое..." /> : variant.answer}

@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material"; import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material";
import { useEffect } from "react";
import { VariantItem } from "./VariantItem"; import { VariantItem } from "./VariantItem";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizViewStore } from "@stores/quizView";
import type { QuizQuestionVariant } from "@model/questionTypes/variant"; import type { QuizQuestionVariant } from "@model/questionTypes/variant";
import moment from "moment"; import moment from "moment";
@ -14,13 +14,13 @@ type VariantProps = {
}; };
export const Variant = ({ currentQuestion }: VariantProps) => { export const Variant = ({ currentQuestion }: VariantProps) => {
const [isSending, setIsSending] = useState(false);
const answers = useQuizViewStore((state) => state.answers);
const { ownVariants, updateOwnVariant } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
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) ?? {}; const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer;
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id); const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup; const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
@ -29,13 +29,18 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
if (!ownVariant) { if (!ownVariant) {
updateOwnVariant(currentQuestion.id, ""); updateOwnVariant(currentQuestion.id, "");
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question"); if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box
@ -71,23 +76,21 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants.map((variant, index) => (
<VariantItem <VariantItem
key={variant.id} key={variant.id}
currentQuestion={currentQuestion} questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={variant} variant={variant}
answer={answer} answer={answer}
index={index} index={index}
isSending={isSending}
setIsSending={setIsSending}
/> />
))} ))}
{currentQuestion.content.own && ownVariant && ( {currentQuestion.content.own && ownVariant && (
<VariantItem <VariantItem
own own
currentQuestion={currentQuestion} questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={ownVariant.variant} variant={ownVariant.variant}
answer={answer} answer={answer}
index={currentQuestion.content.variants.length + 2} index={currentQuestion.content.variants.length + 2}
isSending={isSending}
setIsSending={setIsSending}
/> />
)} )}
</Box> </Box>

@ -1,75 +1,37 @@
import { FormControlLabel, Radio, useTheme } from "@mui/material"; import type { QuestionVariant } from "@/model/questionTypes/shared";
import { enqueueSnackbar } from "notistack";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { FormControlLabel, Radio, useTheme } from "@mui/material";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { MouseEvent } from "react"; import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
type VarimgVariantProps = { type VarimgVariantProps = {
currentQuestion: QuizQuestionVarImg; questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
index: number; index: number;
isSending: boolean; isSending: boolean;
setIsSending: (isSending: boolean) => void; setIsSending: (isSending: boolean) => void;
}; };
export const VarimgVariant = ({ currentQuestion, variant, index, isSending, setIsSending }: VarimgVariantProps) => { export const VarimgVariant = ({ questionId, variant, index, isSending, setIsSending }: VarimgVariantProps) => {
const { settings, quizId, preview } = useQuizSettings(); const { settings } = useQuizSettings();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {};
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => { const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault(); event.preventDefault();
setIsSending(true); updateAnswer(questionId, variant.id, variant.points || 0);
try { if (answer === variant.id) {
await sendAnswer({ deleteAnswer(questionId);
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
} }
if (answer === currentQuestion.content.variants[index].id) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
}; };
return ( return (
@ -110,7 +72,12 @@ export const VarimgVariant = ({ currentQuestion, variant, index, isSending, setI
value={index} value={index}
onClick={sendVariant} onClick={sendVariant}
label={variant.answer} label={variant.answer}
control={<Radio checkedIcon={<RadioCheck color={theme.palette.primary.main} />} icon={<RadioIcon />} />} control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
}
/> />
); );
}; };

@ -26,7 +26,11 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
return ( return (
<Box> <Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}> <Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<Box <Box
@ -63,7 +67,7 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants.map((variant, index) => (
<VarimgVariant <VarimgVariant
key={variant.id} key={variant.id}
currentQuestion={currentQuestion} questionId={currentQuestion.id}
variant={variant} variant={variant}
isSending={isSending} isSending={isSending}
setIsSending={setIsSending} setIsSending={setIsSending}

@ -14,7 +14,6 @@ type SelectProps = {
colorMain?: string; colorMain?: string;
colorPlaceholder?: string; colorPlaceholder?: string;
placeholder?: string; placeholder?: string;
disabled?: boolean;
}; };
export const Select = ({ export const Select = ({
@ -26,7 +25,6 @@ export const Select = ({
placeholder = "", placeholder = "",
colorMain = "#7E2AEA", colorMain = "#7E2AEA",
colorPlaceholder = "#9A9AAF", colorPlaceholder = "#9A9AAF",
disabled = false,
}: SelectProps) => { }: SelectProps) => {
const [activeItem, setActiveItem] = useState<number>(empty ? -1 : activeItemIndex); const [activeItem, setActiveItem] = useState<number>(empty ? -1 : activeItemIndex);
const theme = useTheme(); const theme = useTheme();
@ -50,7 +48,11 @@ export const Select = ({
}; };
return ( return (
<FormControl disabled={disabled} fullWidth size="small" sx={{ width: "100%", height: "48px", ...sx }}> <FormControl
fullWidth
size="small"
sx={{ width: "100%", height: "48px", ...sx }}
>
<MuiSelect <MuiSelect
displayEmpty displayEmpty
renderValue={(value) => renderValue={(value) =>

@ -5,10 +5,11 @@ import { nanoid } from "nanoid";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { createStore, useStore } from "zustand"; import { createStore, useStore } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
import { devtools } from "zustand/middleware";
export type Answer = string | string[] | Moment; export type Answer = string | string[] | Moment;
type QuestionAnswer = { export type QuestionAnswer = {
questionId: string; questionId: string;
answer: Answer; answer: Answer;
}; };
@ -45,59 +46,102 @@ export function useQuizViewStore<U>(selector: (state: QuizViewStore & QuizViewAc
export const createQuizViewStore = () => export const createQuizViewStore = () =>
createStore<QuizViewStore & QuizViewActions>()( createStore<QuizViewStore & QuizViewActions>()(
immer((set, get) => ({ immer(
answers: [], devtools(
ownVariants: [], (set, get) => ({
points: {}, answers: [],
pointsSum: 0, ownVariants: [],
currentQuizStep: "startpage", points: {},
updateAnswer(questionId, answer, points) { pointsSum: 0,
set((state) => { currentQuizStep: "startpage",
const index = state.answers.findIndex((answer) => questionId === answer.questionId); updateAnswer(questionId, answer, points) {
set(
(state) => {
const index = state.answers.findIndex((answer) => questionId === answer.questionId);
if (index < 0) { if (index < 0) {
state.answers.push({ questionId, answer }); state.answers.push({ questionId, answer });
} else { } else {
state.answers[index] = { questionId, answer }; state.answers[index] = { questionId, answer };
} }
state.points = { ...state.points, ...{ [questionId]: points } }; state.points = { ...state.points, ...{ [questionId]: points } };
state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value); state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value);
});
},
deleteAnswer(questionId) {
set((state) => {
state.answers = state.answers.filter((answer) => questionId !== answer.questionId);
});
},
updateOwnVariant(id, answer) {
set((state) => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: nanoid(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
}, },
false,
{
type: "updateAnswer",
questionId,
answer,
points,
}
);
},
deleteAnswer(questionId) {
set(
(state) => {
state.answers = state.answers.filter((answer) => questionId !== answer.questionId);
},
false,
{
type: "deleteAnswer",
questionId,
}
);
},
updateOwnVariant(id, answer) {
set(
(state) => {
const index = state.ownVariants.findIndex((variant) => variant.id === id);
if (index < 0) {
state.ownVariants.push({
id,
variant: {
id: nanoid(),
answer,
extendedText: "",
hints: "",
originalImageUrl: "",
},
});
} else {
state.ownVariants[index].variant.answer = answer;
}
},
false,
{
type: "updateOwnVariant",
id,
answer,
}
);
},
deleteOwnVariant(id) {
set(
(state) => {
state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id);
},
false,
{
type: "deleteOwnVariant",
id,
}
);
},
setCurrentQuizStep(step) {
set({ currentQuizStep: step }, false, {
type: "setCurrentQuizStep",
step,
}); });
} else { },
state.ownVariants[index].variant.answer = answer; }),
} {
}); name: "QuizViewStore-" + nanoid(4),
}, enabled: import.meta.env.DEV,
deleteOwnVariant(id) { trace: import.meta.env.DEV,
set((state) => { }
state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id); )
}); )
},
setCurrentQuizStep(step) {
set({ currentQuizStep: step });
},
}))
); );

@ -0,0 +1,119 @@
import { sendAnswer } from "@/api/quizRelase";
import { RealTypedQuizQuestion } from "@/model/questionTypes/shared";
import { QuestionAnswer } from "@/stores/quizView";
import moment from "moment";
import { notReachable } from "./notReachable";
export function sendQuestionAnswer(quizId: string, question: RealTypedQuizQuestion, questionAnswer: QuestionAnswer) {
switch (question.type) {
case "date": {
if (!moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send answer in date question");
return sendAnswer({
questionId: question.id,
body: moment(questionAnswer.answer).format("YYYY.MM.DD"),
qid: quizId,
});
}
case "emoji": {
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.extendedText + " " + variant.answer,
qid: quizId,
});
}
case "file": {
return;
}
case "images": {
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} <img style="width:100%; max-width:250px; max-height:250px" src="${variant.extendedText}"/>`,
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");
return sendAnswer({
questionId: question.id,
body: questionAnswer.answer,
qid: quizId,
});
}
case "variant": {
if (question.content.multi) {
const answer = questionAnswer.answer;
if (!Array.isArray(answer)) throw new Error("Cannot send answer in select question");
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
return sendAnswer({
questionId: question.id,
body: selectedVariants.map((v) => v.answer).join(", "),
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);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
return sendAnswer({
questionId: question.id,
body: `${variant.answer} <img style="width:100%; max-width:250px; max-height:250px" src="${variant.extendedText}"/>`,
qid: quizId,
});
}
default:
notReachable(question);
}
}