diff --git a/lib/api/quizRelase.ts b/lib/api/quizRelase.ts index f895078..99fb4a9 100644 --- a/lib/api/quizRelase.ts +++ b/lib/api/quizRelase.ts @@ -78,23 +78,26 @@ export async function getData(quizId: string): Promise<{ error?: AxiosError; }> { try { - const { data, headers } = await axios(domain + `/answer/v1.0.0/settings${window.location.search}`, { - method: "POST", - headers: { - "X-Sessionkey": SESSIONS, - "Content-Type": "application/json", - DeviceType: DeviceType, - Device: Device, - OS: OSDevice, - Browser: userAgent, - }, - data: { - quiz_id: quizId, - limit: 100, - page: 0, - need_config: true, - }, - }); + const { data, headers } = await axios( + domain + `/answer/v1.0.0/settings${window.location.search}`, + { + method: "POST", + headers: { + "X-Sessionkey": SESSIONS, + "Content-Type": "application/json", + DeviceType: DeviceType, + Device: Device, + OS: OSDevice, + Browser: userAgent, + }, + data: { + quiz_id: quizId, + limit: 100, + page: 0, + need_config: true, + }, + } + ); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); //Тут ещё проверка на антифрод без парса конфига. Нам не интересно время если не нужно запрещать проходить чаще чем в сутки @@ -143,10 +146,10 @@ type SendAnswerProps = { questionId: string; body: string | 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; const formData = new FormData(); diff --git a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx index f55100c..bcfc4b5 100644 --- a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx +++ b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx @@ -167,6 +167,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => { useEffect(() => { vkMetrics.contactsFormOpened(); yandexMetrics.contactsFormOpened(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( @@ -274,11 +275,17 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => { fontSize={"16px"} > С  - + Положением об обработке персональных данных{" "}  и  - + {" "} Политикой конфиденциальности{" "} diff --git a/lib/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx b/lib/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx index cc27f3b..4be59fa 100644 --- a/lib/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx +++ b/lib/components/ViewPublicationPage/ContactForm/CustomInput/CustomInput.tsx @@ -3,7 +3,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext.ts"; import { useQuizSettings } from "@contexts/QuizDataContext.ts"; import { useIMask } from "react-imask"; 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 { phoneMasksByCountry } from "@utils/phoneMasksByCountry.tsx"; @@ -14,11 +14,12 @@ type InputProps = { onChange: TextFieldProps["onChange"]; id: string; isPhone?: boolean; + type?: HTMLInputTypeAttribute; }; const TextField = MuiTextField as unknown as FC; -export const CustomInput = ({ title, desc, Icon, onChange, isPhone }: InputProps) => { +export const CustomInput = ({ title, desc, Icon, onChange, isPhone, type }: InputProps) => { const theme = useTheme(); const isMobile = useRootContainerSize() < 600; const { settings } = useQuizSettings(); @@ -26,13 +27,18 @@ export const CustomInput = ({ title, desc, Icon, onChange, isPhone }: InputProps const { ref } = useIMask({ mask }); return ( - + {title} - + ), endAdornment: ( diff --git a/lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx b/lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx index 4a6fcbb..72383c4 100644 --- a/lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx +++ b/lib/components/ViewPublicationPage/ContactForm/Inputs/Inputs.tsx @@ -52,6 +52,7 @@ export const Inputs = ({ title={FC["email"].innerText || "Введите Email"} desc={FC["email"].text || "Email"} Icon={EmailIcon} + type="email" /> ); const Phone = ( diff --git a/lib/components/ViewPublicationPage/ViewPublicationPage.tsx b/lib/components/ViewPublicationPage/ViewPublicationPage.tsx index 6f8a209..4c90917 100644 --- a/lib/components/ViewPublicationPage/ViewPublicationPage.tsx +++ b/lib/components/ViewPublicationPage/ViewPublicationPage.tsx @@ -2,7 +2,7 @@ import { ContactForm } from "@/components/ViewPublicationPage/ContactForm/Contac import { extractImageLinksFromQuestion } from "@/utils/extractImageLinks"; import { useVKMetrics } from "@/utils/hooks/metrics/useVKMetrics"; import { useYandexMetrics } from "@/utils/hooks/metrics/useYandexMetrics"; -import { sendAnswer } from "@api/quizRelase"; +import { sendQuestionAnswer } from "@/utils/sendQuestionAnswer"; import { useQuizSettings } from "@contexts/QuizDataContext"; import { ThemeProvider, Typography } from "@mui/material"; import { useQuizViewStore } from "@stores/quizView"; @@ -37,8 +37,6 @@ export default function ViewPublicationPage() { useYandexMetrics(settings?.cfg?.yandexMetricsNumber); useVKMetrics(settings?.cfg?.vkMetricsNumber); - const isAnswer = answers.some((ans) => ans.questionId === currentQuestion?.id); - useEffect( function setFaviconAndTitle() { if (!changeFaviconAndTitle) return; @@ -68,6 +66,8 @@ export default function ViewPublicationPage() { ); + const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id); + let quizStepElement: ReactElement; switch (currentQuizStep) { case "startpage": { @@ -94,20 +94,15 @@ export default function ViewPublicationPage() { nextButton={ { - 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); + }); }} /> } diff --git a/lib/components/ViewPublicationPage/questions/Date/index.tsx b/lib/components/ViewPublicationPage/questions/Date/index.tsx index 31cf3ff..1e21a04 100644 --- a/lib/components/ViewPublicationPage/questions/Date/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Date/index.tsx @@ -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 { useQuizSettings } from "@contexts/QuizDataContext"; - -import { quizThemes } from "@utils/themes/Publication/themePublication"; - import CalendarIcon from "@icons/CalendarIcon"; - -import type { Moment } from "moment"; 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 const Date = ({ currentQuestion }: DateProps) => { - const [isSending, setIsSending] = useState(false); - const { settings, quizId, preview } = useQuizSettings(); + const { settings } = useQuizSettings(); const answers = useQuizViewStore((state) => state.answers); const { updateAnswer } = useQuizViewStore((state) => state); const theme = useTheme(); @@ -29,28 +21,18 @@ export const Date = ({ currentQuestion }: DateProps) => { const currentAnswer = moment(answer) || moment(); const onDateChange = async (date: Moment | null) => { - if (isSending || !date) return; + if (!date) return; - setIsSending(true); - 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); + updateAnswer(currentQuestion.id, date, 0); }; return ( - + {currentQuestion.title} void; }; -export const EmojiVariant = ({ currentQuestion, variant, index, isSending, setIsSending }: EmojiVariantProps) => { - const { quizId, settings, preview } = useQuizSettings(); +export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps) => { + const { settings } = useQuizSettings(); const answers = useQuizViewStore((state) => state.answers); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; + const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {}; const onVariantClick = async (event: MouseEvent) => { event.preventDefault(); - if (isSending) return; - setIsSending(true); - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: - currentQuestion.content.variants[index].extendedText + " " + currentQuestion.content.variants[index].answer, - qid: quizId, - preview, - }); + updateAnswer(questionId, variant.id, variant.points || 0); - updateAnswer( - currentQuestion.id, - currentQuestion.content.variants[index].id, - currentQuestion.content.variants[index].points || 0 - ); - } catch (error) { - enqueueSnackbar("ответ не был засчитан"); + if (answer === variant.id) { + deleteAnswer(questionId); } - - 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 ( { - const [isSending, setIsSending] = useState(false); const answers = useQuizViewStore((state) => state.answers); const { updateAnswer } = useQuizViewStore((state) => state); const theme = useTheme(); @@ -22,7 +18,11 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => { return ( - + {currentQuestion.title} { {currentQuestion.content.variants.map((variant, index) => ( ))} diff --git a/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx b/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx index eb70069..1077def 100644 --- a/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx +++ b/lib/components/ViewPublicationPage/questions/Images/ImageVariant.tsx @@ -1,69 +1,33 @@ -import { Box, FormControlLabel, Radio, useTheme } from "@mui/material"; -import { enqueueSnackbar } from "notistack"; - -import { sendAnswer } from "@api/quizRelase"; -import { useQuizViewStore } from "@stores/quizView"; +import type { QuestionVariant } from "@/model/questionTypes/shared"; 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 RadioIcon from "@ui_kit/RadioIcon"; - +import { quizThemes } from "@utils/themes/Publication/themePublication"; import type { MouseEvent } from "react"; -import type { QuestionVariant } from "@/model/questionTypes/shared"; -import type { QuizQuestionImages } from "@model/questionTypes/images"; type ImagesProps = { - currentQuestion: QuizQuestionImages; + questionId: string; variant: QuestionVariant; - isSending: boolean; - setIsSending: (isSending: boolean) => void; index: number; }; -export const ImageVariant = ({ currentQuestion, variant, isSending, setIsSending, index }: ImagesProps) => { - const { settings, quizId, preview } = useQuizSettings(); +export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => { + const { settings } = useQuizSettings(); const answers = useQuizViewStore((state) => state.answers); const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state); 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) => { event.preventDefault(); - if (isSending) return; - setIsSending(true); - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: `${currentQuestion.content.variants[index].answer} `, - qid: quizId, - preview, - }); - updateAnswer( - currentQuestion.id, - currentQuestion.content.variants[index].id, - currentQuestion.content.variants[index].points || 0 - ); - } catch (error) { - enqueueSnackbar("ответ не был засчитан"); + updateAnswer(questionId, variant.id, variant.points || 0); + + if (answer === variant.id) { + deleteAnswer(questionId); } - - 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 ( diff --git a/lib/components/ViewPublicationPage/questions/Images/index.tsx b/lib/components/ViewPublicationPage/questions/Images/index.tsx index 9b3f236..5387b01 100644 --- a/lib/components/ViewPublicationPage/questions/Images/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Images/index.tsx @@ -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 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 = { currentQuestion: QuizQuestionImages; }; export const Images = ({ currentQuestion }: ImagesProps) => { - const [isSending, setIsSending] = useState(false); const answers = useQuizViewStore((state) => state.answers); const theme = useTheme(); const answer = answers.find(({ questionId }) => questionId === currentQuestion.id)?.answer; @@ -22,7 +17,11 @@ export const Images = ({ currentQuestion }: ImagesProps) => { return ( - + {currentQuestion.title} { {currentQuestion.content.variants.map((variant, index) => ( ))} diff --git a/lib/components/ViewPublicationPage/questions/Number/index.tsx b/lib/components/ViewPublicationPage/questions/Number/index.tsx index 621683f..fc403c5 100644 --- a/lib/components/ViewPublicationPage/questions/Number/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Number/index.tsx @@ -1,34 +1,26 @@ +import { useQuizSettings } from "@contexts/QuizDataContext"; +import type { QuizQuestionNumber } from "@model/questionTypes/number"; import { Box, Typography, useTheme } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useDebouncedCallback } from "use-debounce"; - +import { useQuizViewStore } from "@stores/quizView"; import { CustomSlider } from "@ui_kit/CustomSlider"; 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 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 [isSending, setIsSending] = useState(false); const [inputValue, setInputValue] = useState("0"); const [minRange, setMinRange] = useState("0"); const [maxRange, setMaxRange] = useState("100000000000"); const [reversedInputValue, setReversedInputValue] = useState("0"); const [reversedMinRange, setReversedMinRange] = useState("0"); const [reversedMaxRange, setReversedMaxRange] = useState("100000000000"); - const { settings, quizId, preview } = useQuizSettings(); + const { settings } = useQuizSettings(); const { updateAnswer } = useQuizViewStore((state) => state); const answers = useQuizViewStore((state) => state.answers); const theme = useTheme(); @@ -49,23 +41,9 @@ export const Number = ({ currentQuestion }: NumberProps) => { }, [reversed]); const sendAnswerToBackend = async (value: string, noUpdate = false) => { - setIsSending(true); - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: value, - qid: quizId, - preview, - }); - - if (!noUpdate) { - updateAnswer(currentQuestion.id, value, 0); - } - } catch (error) { - enqueueSnackbar("ответ не был засчитан"); + if (!noUpdate) { + updateAnswer(currentQuestion.id, value, 0); } - - setIsSending(false); }; const updateValueDebounced = useDebouncedCallback(async (value: string) => { @@ -180,6 +158,7 @@ export const Number = ({ currentQuestion }: NumberProps) => { setReversedInputValue(String(currentQuestion.content.start)); setInputValue(String(currentQuestion.content.start)); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onSliderChange = (_: Event, value: number | number[]) => { @@ -305,7 +284,11 @@ export const Number = ({ currentQuestion }: NumberProps) => { return ( - + {currentQuestion.title} , + icon: (color: string, width: number) => ( + + ), }, { name: "trophie", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, { name: "flag", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, { name: "heart", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, { name: "like", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, { name: "bubble", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, { name: "hashtag", - icon: (color: string, width: number) => , + icon: (color: string, width: number) => ( + + ), }, ]; @@ -53,38 +81,26 @@ type RatingProps = { }; export const Rating = ({ currentQuestion }: RatingProps) => { - const [isSending, setIsSending] = useState(false); - const { quizId, preview } = useQuizSettings(); 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) => { - setIsSending(true); - - 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); + updateAnswer(currentQuestion.id, String(value), 0); }; return ( - + {currentQuestion.title} { > sendRating(value)} sx={{ diff --git a/lib/components/ViewPublicationPage/questions/Select/index.tsx b/lib/components/ViewPublicationPage/questions/Select/index.tsx index 6e80b1e..e624e7a 100644 --- a/lib/components/ViewPublicationPage/questions/Select/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Select/index.tsx @@ -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 { sendAnswer } from "@api/quizRelase"; -import { useQuizViewStore } from "@stores/quizView"; import { useQuizSettings } from "@contexts/QuizDataContext"; - -import { quizThemes } from "@utils/themes/Publication/themePublication"; - 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 [isSending, setIsSending] = useState(false); - const { quizId, settings, preview } = useQuizSettings(); + const { settings } = useQuizSettings(); 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) => { - setIsSending(true); - if (value < 0) { deleteAnswer(currentQuestion.id); - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: "", - qid: quizId, - preview, - }); - } catch (error) { - enqueueSnackbar("ответ не был засчитан"); - } - - return setIsSending(false); + return; } - try { - 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); + updateAnswer(currentQuestion.id, String(value), 0); }; return ( - + {currentQuestion.title} { }} > answer)} diff --git a/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx b/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx index 1745fd3..3090c9a 100644 --- a/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx +++ b/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx @@ -14,11 +14,10 @@ import type { QuizQuestionText } from "@model/questionTypes/text"; interface TextNormalProps { currentQuestion: QuizQuestionText; answer?: Answer; - inputHC: (text: string) => void; stepNumber?: number | null; } -export const TextNormal = ({ currentQuestion, answer, inputHC }: TextNormalProps) => { +export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => { const { settings } = useQuizSettings(); const { updateAnswer } = useQuizViewStore((state) => state); const isMobile = useRootContainerSize() < 650; @@ -26,12 +25,15 @@ export const TextNormal = ({ currentQuestion, answer, inputHC }: TextNormalProps const onInputChange = async ({ target }: ChangeEvent) => { updateAnswer(currentQuestion.id, target.value, 0); - inputHC(target.value); }; return ( - + {currentQuestion.title} void; stepNumber?: number | null; } -export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: TextSpecialProps) => { +export const TextSpecial = ({ currentQuestion, answer, stepNumber }: TextSpecialProps) => { const { settings } = useQuizSettings(); const { updateAnswer } = useQuizViewStore((state) => state); const isHorizontal = ORIENTATION[Number(stepNumber) - 1].horizontal; @@ -54,7 +53,6 @@ export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: Te const onInputChange = async ({ target }: ChangeEvent) => { updateAnswer(currentQuestion.id, target.value, 0); - inputHC(target.value); }; return ( @@ -75,7 +73,11 @@ export const TextSpecial = ({ currentQuestion, answer, inputHC, stepNumber }: Te gap: "20px", }} > - + {currentQuestion.title} {isHorizontal && currentQuestion.content.back && currentQuestion.content.back !== " " && ( diff --git a/lib/components/ViewPublicationPage/questions/Text/index.tsx b/lib/components/ViewPublicationPage/questions/Text/index.tsx index 06ae862..208b8be 100644 --- a/lib/components/ViewPublicationPage/questions/Text/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Text/index.tsx @@ -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 { useQuizViewStore } from "@stores/quizView"; +import { TextNormal } from "./TextNormal"; +import { TextSpecial } from "./TextSpecial"; import type { QuizQuestionText } from "@model/questionTypes/text"; @@ -17,42 +11,34 @@ type TextProps = { }; export const Text = ({ currentQuestion, stepNumber }: TextProps) => { - const [isSending, setIsSending] = useState(false); - const { settings, preview, quizId } = useQuizSettings(); + const { settings } = useQuizSettings(); const answers = useQuizViewStore((state) => state.answers); 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) { case true: return ( - + ); case undefined: - return ; + return ( + + ); default: - return ; + return ( + + ); } }; diff --git a/lib/components/ViewPublicationPage/questions/Variant/VariantItem.tsx b/lib/components/ViewPublicationPage/questions/Variant/VariantItem.tsx index 60266bd..3835ed8 100644 --- a/lib/components/ViewPublicationPage/questions/Variant/VariantItem.tsx +++ b/lib/components/ViewPublicationPage/questions/Variant/VariantItem.tsx @@ -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 { quizThemes } from "@utils/themes/Publication/themePublication"; - 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 RadioIcon from "@ui_kit/RadioIcon"; - +import { quizThemes } from "@utils/themes/Publication/themePublication"; 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; export const VariantItem = ({ - currentQuestion, + questionId, + isMulti, variant, answer, index, own = false, - isSending, - setIsSending, }: { - currentQuestion: QuizQuestionVariant; + isMulti: boolean; + questionId: string; variant: QuestionVariant; answer: string | string[] | undefined; index: number; own?: boolean; - isSending: boolean; - setIsSending: (a: boolean) => void; }) => { - const { settings, quizId, preview } = useQuizSettings(); + const { settings } = useQuizSettings(); const theme = useTheme(); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const sendVariant = async (event: MouseEvent) => { event.preventDefault(); - if (isSending) { - return; - } + const variantId = variant.id; - setIsSending(true); - - const variantId = currentQuestion.content.variants[index].id; - - if (currentQuestion.content.multi) { + if (isMulti) { const currentAnswer = typeof answer !== "string" ? answer || [] : []; - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: currentAnswer.includes(variantId) - ? currentAnswer?.filter((item) => item !== variantId) - : [...currentAnswer, variantId], - qid: quizId, - preview, - }); - - 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 + return updateAnswer( + questionId, + currentAnswer.includes(variantId) + ? currentAnswer?.filter((item) => item !== variantId) + : [...currentAnswer, variantId], + variant.points || 0 ); - } catch (error) { - console.error(error); - enqueueSnackbar("ответ не был засчитан"); } + updateAnswer(questionId, variantId, answer === variantId ? 0 : variant.points || 0); + if (answer === variantId) { - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: "", - qid: quizId, - preview, - }); - } catch (error) { - console.error(error); - enqueueSnackbar("ответ не был засчитан"); - } - deleteAnswer(currentQuestion.id); + deleteAnswer(questionId); } - - setIsSending(false); }; return ( } + checkedIcon={ + + } icon={} /> ) : ( - } icon={} /> + } + icon={} + /> ) } label={own ? : variant.answer} diff --git a/lib/components/ViewPublicationPage/questions/Variant/index.tsx b/lib/components/ViewPublicationPage/questions/Variant/index.tsx index 2796601..68cd397 100644 --- a/lib/components/ViewPublicationPage/questions/Variant/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Variant/index.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState } from "react"; import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material"; +import { useEffect } from "react"; import { VariantItem } from "./VariantItem"; -import { useQuizViewStore } from "@stores/quizView"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; +import { useQuizViewStore } from "@stores/quizView"; import type { QuizQuestionVariant } from "@model/questionTypes/variant"; import moment from "moment"; @@ -14,13 +14,13 @@ type 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 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 Group = currentQuestion.content.multi ? FormGroup : RadioGroup; @@ -29,13 +29,18 @@ export const Variant = ({ currentQuestion }: VariantProps) => { if (!ownVariant) { updateOwnVariant(currentQuestion.id, ""); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question"); return ( - + {currentQuestion.title} { {currentQuestion.content.variants.map((variant, index) => ( ))} {currentQuestion.content.own && ownVariant && ( )} diff --git a/lib/components/ViewPublicationPage/questions/Varimg/VarimgVariant.tsx b/lib/components/ViewPublicationPage/questions/Varimg/VarimgVariant.tsx index 06d4f6a..c50e1cf 100644 --- a/lib/components/ViewPublicationPage/questions/Varimg/VarimgVariant.tsx +++ b/lib/components/ViewPublicationPage/questions/Varimg/VarimgVariant.tsx @@ -1,75 +1,37 @@ -import { FormControlLabel, Radio, useTheme } from "@mui/material"; -import { enqueueSnackbar } from "notistack"; - -import { useQuizViewStore } from "@stores/quizView"; - -import { sendAnswer } from "@api/quizRelase"; +import type { QuestionVariant } from "@/model/questionTypes/shared"; import { useQuizSettings } from "@contexts/QuizDataContext"; - -import { quizThemes } from "@utils/themes/Publication/themePublication"; - +import { FormControlLabel, Radio, 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 { MouseEvent } from "react"; -import type { QuestionVariant } from "@/model/questionTypes/shared"; -import type { QuizQuestionVarImg } from "@model/questionTypes/varimg"; type VarimgVariantProps = { - currentQuestion: QuizQuestionVarImg; + questionId: string; variant: QuestionVariant; index: number; isSending: boolean; setIsSending: (isSending: boolean) => void; }; -export const VarimgVariant = ({ currentQuestion, variant, index, isSending, setIsSending }: VarimgVariantProps) => { - const { settings, quizId, preview } = useQuizSettings(); +export const VarimgVariant = ({ questionId, variant, index, isSending, setIsSending }: VarimgVariantProps) => { + const { settings } = useQuizSettings(); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const answers = useQuizViewStore((state) => state.answers); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; + const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {}; const sendVariant = async (event: MouseEvent) => { event.preventDefault(); - setIsSending(true); + updateAnswer(questionId, variant.id, variant.points || 0); - try { - await sendAnswer({ - questionId: currentQuestion.id, - body: `${currentQuestion.content.variants[index].answer} `, - qid: quizId, - preview, - }); - - updateAnswer( - currentQuestion.id, - currentQuestion.content.variants[index].id, - currentQuestion.content.variants[index].points || 0 - ); - } catch (error) { - enqueueSnackbar("ответ не был засчитан"); + if (answer === variant.id) { + deleteAnswer(questionId); } - - 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 ( @@ -110,7 +72,12 @@ export const VarimgVariant = ({ currentQuestion, variant, index, isSending, setI value={index} onClick={sendVariant} label={variant.answer} - control={} icon={} />} + control={ + } + icon={} + /> + } /> ); }; diff --git a/lib/components/ViewPublicationPage/questions/Varimg/index.tsx b/lib/components/ViewPublicationPage/questions/Varimg/index.tsx index 5e58738..d0f9c3b 100644 --- a/lib/components/ViewPublicationPage/questions/Varimg/index.tsx +++ b/lib/components/ViewPublicationPage/questions/Varimg/index.tsx @@ -26,7 +26,11 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => { return ( - + {currentQuestion.title} { {currentQuestion.content.variants.map((variant, index) => ( { const [activeItem, setActiveItem] = useState(empty ? -1 : activeItemIndex); const theme = useTheme(); @@ -50,7 +48,11 @@ export const Select = ({ }; return ( - + diff --git a/lib/stores/quizView.ts b/lib/stores/quizView.ts index d614b1f..2720189 100644 --- a/lib/stores/quizView.ts +++ b/lib/stores/quizView.ts @@ -5,10 +5,11 @@ import { nanoid } from "nanoid"; import { createContext, useContext } from "react"; import { createStore, useStore } from "zustand"; import { immer } from "zustand/middleware/immer"; +import { devtools } from "zustand/middleware"; export type Answer = string | string[] | Moment; -type QuestionAnswer = { +export type QuestionAnswer = { questionId: string; answer: Answer; }; @@ -45,59 +46,102 @@ export function useQuizViewStore(selector: (state: QuizViewStore & QuizViewAc export const createQuizViewStore = () => createStore()( - immer((set, get) => ({ - answers: [], - ownVariants: [], - points: {}, - pointsSum: 0, - currentQuizStep: "startpage", - updateAnswer(questionId, answer, points) { - set((state) => { - const index = state.answers.findIndex((answer) => questionId === answer.questionId); + immer( + devtools( + (set, get) => ({ + answers: [], + ownVariants: [], + points: {}, + pointsSum: 0, + currentQuizStep: "startpage", + updateAnswer(questionId, answer, points) { + set( + (state) => { + const index = state.answers.findIndex((answer) => questionId === answer.questionId); - if (index < 0) { - state.answers.push({ questionId, answer }); - } else { - state.answers[index] = { questionId, answer }; - } + if (index < 0) { + state.answers.push({ questionId, answer }); + } else { + 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); - }); - }, - 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: "", + state.pointsSum = Object.values(state.points).reduce((sum, value) => sum + value); }, + 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; - } - }); - }, - deleteOwnVariant(id) { - set((state) => { - state.ownVariants = state.ownVariants.filter((variant) => variant.id !== id); - }); - }, - setCurrentQuizStep(step) { - set({ currentQuizStep: step }); - }, - })) + }, + }), + { + name: "QuizViewStore-" + nanoid(4), + enabled: import.meta.env.DEV, + trace: import.meta.env.DEV, + } + ) + ) ); diff --git a/lib/utils/sendQuestionAnswer.ts b/lib/utils/sendQuestionAnswer.ts new file mode 100644 index 0000000..c3d6fa1 --- /dev/null +++ b/lib/utils/sendQuestionAnswer.ts @@ -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} `, + 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} `, + qid: quizId, + }); + } + default: + notReachable(question); + } +}