refactor: questions

This commit is contained in:
IlyaDoronin 2024-04-23 17:45:49 +03:00
parent e833fa2aa6
commit 40037b38ce
30 changed files with 2733 additions and 2474 deletions

@ -25,147 +25,147 @@ import { DESIGN_LIST } from "@/utils/designList";
import type { ReactNode } from "react";
type Props = {
currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null;
nextButton: ReactNode;
prevButton: ReactNode;
questionSelect: ReactNode;
currentQuestion: RealTypedQuizQuestion;
currentQuestionStepNumber: number | null;
nextButton: ReactNode;
prevButton: ReactNode;
questionSelect: ReactNode;
};
export const Question = ({
currentQuestion,
currentQuestionStepNumber,
nextButton,
prevButton,
questionSelect,
currentQuestion,
currentQuestionStepNumber,
nextButton,
prevButton,
questionSelect,
}: Props) => {
const theme = useTheme();
const { settings, show_badge } = useQuizData();
const theme = useTheme();
const { settings, show_badge } = useQuizData();
return (
return (
<Box
sx={{
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
>
<Box
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "transparent"
: "linear-gradient(90deg,#272626, transparent)"
: theme.palette.background.default,
overflow: "hidden",
}}
>
<Box
sx={{
height: "100%",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundImage: settings.cfg.design
? `url(${DESIGN_LIST[settings.cfg.theme]})`
: null,
}}
sx={{
overflow: "auto",
width: "100%",
flexGrow: 1,
scrollbarWidth: "none",
"&::-webkit-scrollbar": {
width: 0,
},
}}
>
<Box
<Box
sx={{
width: "100%",
minHeight: "100%",
maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<QuestionByType
key={currentQuestion.id}
question={currentQuestion}
stepNumber={currentQuestionStepNumber}
/>
{show_badge && (
<Link
target="_blank"
href="https://quiz.pena.digital"
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "transparent"
: "linear-gradient(90deg,#272626, transparent)"
: theme.palette.background.default,
overflow: "hidden"
mt: "20px",
alignSelf: "end",
}}
>
<Box
sx={{
overflow: "auto",
width: "100%",
flexGrow: 1,
scrollbarWidth: "none",
"&::-webkit-scrollbar": {
width: 0,
},
>
{quizThemes[settings.cfg.theme].isLight ? (
<NameplateLogoFQ
style={{
fontSize: "34px",
width: "200px",
height: "auto",
}}
>
<Box
sx={{
width: "100%",
minHeight: "100%",
maxWidth: "1440px",
padding: "40px 25px 20px",
margin: "0 auto",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<QuestionByType
key={currentQuestion.id}
question={currentQuestion}
stepNumber={currentQuestionStepNumber}
/>
{show_badge && (
<Link
target="_blank"
href="https://quiz.pena.digital"
sx={{
mt: "20px",
alignSelf: "end",
}}
>
{quizThemes[settings.cfg.theme].isLight ? (
<NameplateLogoFQ
style={{
fontSize: "34px",
width: "200px",
height: "auto",
}}
/>
) : (
<NameplateLogoFQDark
style={{
fontSize: "34px",
width: "200px",
height: "auto",
}}
/>
)}
</Link>
)}
</Box>
</Box>
{questionSelect}
<Footer
stepNumber={currentQuestionStepNumber}
prevButton={prevButton}
nextButton={nextButton}
/>
</Box>
/>
) : (
<NameplateLogoFQDark
style={{
fontSize: "34px",
width: "200px",
height: "auto",
}}
/>
)}
</Link>
)}
</Box>
</Box>
);
{questionSelect}
<Footer
stepNumber={currentQuestionStepNumber}
prevButton={prevButton}
nextButton={nextButton}
/>
</Box>
</Box>
);
};
function QuestionByType({
question,
stepNumber,
question,
stepNumber,
}: {
question: RealTypedQuizQuestion;
stepNumber: number | null;
question: RealTypedQuizQuestion;
stepNumber: number | null;
}) {
switch (question.type) {
case "variant":
return <Variant currentQuestion={question} />;
case "images":
return <Images currentQuestion={question} />;
case "varimg":
return <Varimg currentQuestion={question} />;
case "emoji":
return <Emoji currentQuestion={question} />;
case "text":
return <Text currentQuestion={question} stepNumber={stepNumber} />;
case "select":
return <Select currentQuestion={question} />;
case "date":
return <Date currentQuestion={question} />;
case "number":
return <Number currentQuestion={question} />;
case "file":
return <File currentQuestion={question} />;
case "page":
return <Page currentQuestion={question} />;
case "rating":
return <Rating currentQuestion={question} />;
default:
notReachable(question);
}
switch (question.type) {
case "variant":
return <Variant currentQuestion={question} />;
case "images":
return <Images currentQuestion={question} />;
case "varimg":
return <Varimg currentQuestion={question} />;
case "emoji":
return <Emoji currentQuestion={question} />;
case "text":
return <Text currentQuestion={question} stepNumber={stepNumber} />;
case "select":
return <Select currentQuestion={question} />;
case "date":
return <Date currentQuestion={question} />;
case "number":
return <Number currentQuestion={question} />;
case "file":
return <File currentQuestion={question} />;
case "page":
return <Page currentQuestion={question} />;
case "rating":
return <Rating currentQuestion={question} />;
default:
notReachable(question);
}
}

@ -1,31 +1,54 @@
import { useState } from "react";
import moment from "moment";
import { DatePicker } from "@mui/x-date-pickers";
import { Box, Typography, useTheme } from "@mui/material";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import CalendarIcon from "@icons/CalendarIcon";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@/stores/quizView";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react";
import { useQuizViewStore } from "@/stores/quizView";
import CalendarIcon from "@icons/CalendarIcon";
import type { Moment } from "moment";
import type { QuizQuestionDate } from "@model/questionTypes/date";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export const Date = ({ currentQuestion }: DateProps) => {
const theme = useTheme();
const [isSending, setIsSending] = useState<boolean>(false);
const { settings, quizId, preview } = useQuizData();
const answers = useQuizViewStore((state) => state.answers);
const updateAnswer = useQuizViewStore((state) => state.updateAnswer);
const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const currentAnswer = moment(answer) || moment();
const [isSending, setIsSending] = useState<boolean>(false);
const onDateChange = async (date: Moment | null) => {
if (isSending || !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);
};
return (
<Box>
@ -57,32 +80,9 @@ export const Date = ({ currentQuestion }: DateProps) => {
),
}}
value={currentAnswer}
onChange={async (date) => {
if (isSending || !date) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: moment(date).format("YYYY.MM.DD"),
qid: quizId,
preview,
});
updateAnswer(currentQuestion.id, date, 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}}
onChange={onDateChange}
slotProps={{
openPickerButton: {
sx: {
p: 0,
},
"data-cy": "open-datepicker",
},
openPickerButton: { sx: { p: 0 }, "data-cy": "open-datepicker" },
layout: {
sx: { backgroundColor: theme.palette.background.default },
},
@ -99,14 +99,8 @@ export const Date = ({ currentQuestion }: DateProps) => {
borderRadius: "10px",
maxWidth: "250px",
pr: "30px",
"& input": {
py: "11px",
pl: "20px",
lineHeight: "19px",
},
"& fieldset": {
borderColor: "#9A9AAF",
},
"& input": { py: "11px", pl: "20px", lineHeight: "19px" },
"& fieldset": { borderColor: "#9A9AAF" },
},
}}
/>

@ -1,221 +0,0 @@
import {
Box,
FormControl,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { useQuizData } from "@contexts/QuizDataContext";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { quizThemes } from "@utils/themes/Publication/themePublication";
polyfillCountryFlagEmojis();
import { useState } from "react";
type EmojiProps = {
currentQuestion: QuizQuestionEmoji;
};
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
onChange={({ target }) => {
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer,
currentQuestion.content.variants[Number(target.value)].points || 0
);
}}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}
>
{currentQuestion.content.variants.map((variant, index) => (
<FormControl
key={index}
disabled={isSending}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
background: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
"&:hover": { borderColor: theme.palette.primary.main },
}}
// value={index}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body:
currentQuestion.content.variants[index].extendedText +
" " +
currentQuestion.content.variants[index].answer,
qid: quizId,
preview
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
cursor: "pointer"
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize={"100px"}>
{variant.extendedText}
</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
justifyContent: "center",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
// paddingLeft: "45px",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
}
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-162px",
right: "12px",
}}
/>
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography
sx={{
wordBreak: "break-word",
lineHeight: "normal",
}}
>
{variant.answer}
</Typography>
</Box>
}
/>
</FormControl>
))}
</Box>
</RadioGroup>
</Box>
);
};

@ -0,0 +1,179 @@
import {
Box,
FormControl,
FormControlLabel,
Radio,
Typography,
useTheme,
} from "@mui/material";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { enqueueSnackbar } from "notistack";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionEmoji } from "@model/questionTypes/emoji";
polyfillCountryFlagEmojis();
type EmojiVariantProps = {
currentQuestion: QuizQuestionEmoji;
variant: QuestionVariant;
index: number;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
};
export const EmojiVariant = ({
currentQuestion,
variant,
index,
isSending,
setIsSending,
}: EmojiVariantProps) => {
const { quizId, settings, preview } = useQuizData();
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body:
currentQuestion.content.variants[index].extendedText +
" " +
currentQuestion.content.variants[index].answer,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (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 (
<FormControl
key={index}
disabled={isSending}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
height: "255px",
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)"
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) ||
quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
"&:hover": { borderColor: theme.palette.primary.main },
}}
// value={index}
onClick={onVariantClick}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
cursor: "pointer",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography fontSize="100px">{variant.extendedText}</Typography>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
justifyContent: "center",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
value={index}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
}
label={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>
{variant.answer}
</Typography>
</Box>
}
/>
</FormControl>
);
};

@ -0,0 +1,70 @@
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 { EmojiVariant } from "./EmojiVariant";
polyfillCountryFlagEmojis();
type EmojiProps = {
currentQuestion: QuizQuestionEmoji;
};
export const Emoji = ({ currentQuestion }: EmojiProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
onChange={({ target }) =>
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[Number(target.value)].answer,
currentQuestion.content.variants[Number(target.value)].points || 0
)
}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}
>
{currentQuestion.content.variants.map((variant, index) => (
<EmojiVariant
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
/>
))}
</Box>
</RadioGroup>
</Box>
);
};

@ -1,292 +0,0 @@
import {
Box,
ButtonBase,
IconButton,
Modal,
Skeleton,
Typography,
useTheme
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import CloseBold from "@icons/CloseBold";
import UploadIcon from "@icons/UploadIcon";
import { sendAnswer, sendFile } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import Info from "@icons/Info";
import { enqueueSnackbar } from "notistack";
import { useState } from "react";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionFile } from "../../../model/questionTypes/file";
import { ACCEPT_SEND_FILE_TYPES_MAP, MAX_FILE_SIZE, UPLOAD_FILE_DESCRIPTIONS_MAP } from "../tools/fileUpload";
export type ModalWarningType = "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null;
type FileProps = {
currentQuestion: QuizQuestionFile;
};
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const answers = useQuizViewStore(state => state.answers);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { quizId, preview } = useQuizData();
const [modalWarningType, setModalWarningType] = useState<ModalWarningType>(null);
const [isSending, setIsSending] = useState<boolean>(false);
const [isDropzoneHighlighted, setIsDropzoneHighlighted] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500;
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
const uploadFile = async (file: File | undefined) => {
if (isSending) return;
if (!file) return;
console.log(file.size);
console.log(MAX_FILE_SIZE);
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
const isFileTypeAccepted = ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].some(
fileType => file.name.toLowerCase().endsWith(fileType)
);
if (!isFileTypeAccepted) return setModalWarningType("errorType");
setIsSending(true);
try {
const data = await sendFile({
questionId: currentQuestion.id,
body: {
file: file,
name: file.name,
preview
},
qid: quizId,
});
console.log(data);
await sendAnswer({
questionId: currentQuestion.id,
body: `https://storage.yandexcloud.net/squizanswer/${quizId}/${currentQuestion.id}/${data!.data.fileIDMap[currentQuestion.id]}`,
qid: quizId,
preview
});
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
} catch (e) {
console.log(e);
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
};
const onDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDropzoneHighlighted(false);
const file = event.dataTransfer.files[0];
uploadFile(file);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>{currentQuestion.title}</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "600px",
}}
>
{answer?.split("|")[0] ? (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography>
<Box
sx={{
padding: "5px 5px 5px 16px",
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
color: "#FFFFFF",
display: "flex",
alignItems: "center",
overflow: "hidden",
gap: "15px",
}}
>
<Typography
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{answer?.split("|")[0]}</Typography>
<IconButton
sx={{ p: 0 }}
onClick={async () => {
if (answer.length > 0) {
setIsSending(true);
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
}
console.log(answer);
updateAnswer(currentQuestion.id, "", 0);
setIsSending(false);
}}
>
<CloseBold />
</IconButton>
</Box>
</Box>
) : (
<Box
sx={{
display: "flex",
alignItems: "center"
}}
>
{isSending ?
<Skeleton
variant="rounded"
sx={{
width: "100%",
height: "120px",
maxWidth: "560px",
}}
/>
:
<ButtonBase
component="label"
sx={{ justifyContent: "flex-start", width: "100%" }}
>
<input
onChange={e => uploadFile(e.target.files?.[0])}
hidden
accept={ACCEPT_SEND_FILE_TYPES_MAP[currentQuestion.content.type].join(",")}
multiple
type="file"
/>
<Box
onDragEnter={() => !answer?.split("|")[0] && setIsDropzoneHighlighted(true)}
onDragLeave={() => setIsDropzoneHighlighted(false)}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
sx={{
width: "100%",
height: isMobile ? undefined : "120px",
display: "flex",
gap: "50px",
justifyContent: "flex-start",
alignItems: "center",
padding: "33px 44px 33px 55px",
backgroundColor: "#F2F3F7",
border: `1px solid ${isDropzoneHighlighted ? "red" : "#9A9AAF"}`,
borderRadius: "8px",
}}
>
<UploadIcon />
<Box>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontWeight: 500,
}}
>
{UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].title}
</Typography>
<Typography
sx={{
color: "#9A9AAF",
// color: theme.palette.grey2.main,
fontSize: "16px",
lineHeight: "19px",
}}
>
{UPLOAD_FILE_DESCRIPTIONS_MAP[currentQuestion.content.type].description}
</Typography>
</Box>
</Box>
</ButtonBase>
}
<Info
sx={{ width: "40px", height: "40px" }}
color={theme.palette.primary.main}
onClick={() => setModalWarningType(currentQuestion.content.type)}
/>
</Box>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
alt=""
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
}}
/>
)}
{answer && currentQuestion.content.type === "video" && (
<video
src={answer.split("|")[1]}
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
objectFit: "cover",
}}
/>
)}
</Box>
<Modal
open={modalWarningType !== null}
onClose={() => setModalWarningType(null)}
>
<Box sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: isMobile ? 300 : 400,
bgcolor: 'background.paper',
borderRadius: 3,
boxShadow: 24,
p: 4,
}}>
<CurrentModal status={modalWarningType} />
</Box>
</Modal>
</Box>
);
};
const CurrentModal = ({ status }: { status: ModalWarningType; }) => {
switch (status) {
case null: return null;
case 'errorType': return <Typography>Выбран некорректный тип файла</Typography>;
case 'errorSize': return <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>;
default: return (
<>
<Typography>Допустимые расширения файлов:</Typography>
<Typography>{
ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</>
);
}
};

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

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

@ -0,0 +1,134 @@
import { useState } from "react";
import { Box, Modal, Typography, useTheme } from "@mui/material";
import { UploadFile } from "./UploadFile";
import { UploadedFile } from "./UploadedFile";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { useQuizViewStore } from "@stores/quizView";
import { ACCEPT_SEND_FILE_TYPES_MAP } from "@/components/ViewPublicationPage/tools/fileUpload";
import type { QuizQuestionFile } from "@model/questionTypes/file";
export type ModalWarningType =
| "errorType"
| "errorSize"
| "picture"
| "video"
| "audio"
| "document"
| null;
type FileProps = {
currentQuestion: QuizQuestionFile;
};
export const File = ({ currentQuestion }: FileProps) => {
const theme = useTheme();
const answers = useQuizViewStore((state) => state.answers);
const [modalWarningType, setModalWarningType] =
useState<ModalWarningType>(null);
const [isSending, setIsSending] = useState<boolean>(false);
const isMobile = useRootContainerSize() < 500;
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer as string;
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
maxWidth: answer?.split("|")[0] ? "640px" : "600px",
}}
>
{answer?.split("|")[0] ? (
<UploadedFile
currentQuestion={currentQuestion}
setIsSending={setIsSending}
/>
) : (
<UploadFile
currentQuestion={currentQuestion}
setModalWarningType={setModalWarningType}
isSending={isSending}
setIsSending={setIsSending}
/>
)}
{answer && currentQuestion.content.type === "picture" && (
<img
src={answer.split("|")[1]}
style={{ marginTop: "15px", maxWidth: "300px", maxHeight: "300px" }}
alt=""
/>
)}
{answer && currentQuestion.content.type === "video" && (
<video
src={answer.split("|")[1]}
style={{
marginTop: "15px",
maxWidth: "300px",
maxHeight: "300px",
objectFit: "cover",
}}
/>
)}
</Box>
<Modal
open={modalWarningType !== null}
onClose={() => setModalWarningType(null)}
>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: isMobile ? 300 : 400,
bgcolor: "background.paper",
borderRadius: 3,
boxShadow: 24,
p: 4,
}}
>
<CurrentModal status={modalWarningType} />
</Box>
</Modal>
</Box>
);
};
const CurrentModal = ({ status }: { status: ModalWarningType }) => {
switch (status) {
case null:
return null;
case "errorType":
return <Typography>Выбран некорректный тип файла</Typography>;
case "errorSize":
return (
<Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>
);
default:
return (
<>
<Typography>Допустимые расширения файлов:</Typography>
<Typography>
{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}
</Typography>
</>
);
}
};

@ -1,195 +0,0 @@
import {
Box,
FormControlLabel,
Radio,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type ImagesProps = {
currentQuestion: QuizQuestionImages;
};
export const Images = ({ currentQuestion }: ImagesProps) => {
const { quizId, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const theme = useTheme();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer;
const { settings } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet
? isMobile
? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<Box
key={index}
sx={{
cursor: "pointer",
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id
? theme.palette.primary.main
: "#9A9AAF",
"&:hover": { borderColor: theme.palette.primary.main },
background: settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)" : settings.cfg.design && quizThemes[settings.cfg.theme].isLight || quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
}}
onClick={async (event) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
preview
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
if (answer === currentQuestion.content.variants[index].id) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
}
setIsSending(false);
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "12px 12px 0 0"
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
justifyContent: "center",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
control={
<Radio
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0
}}
/>
}
label={variant.answer}
/>
</Box>
))}
</Box>
</RadioGroup>
</Box>
);
};

@ -0,0 +1,156 @@
import { Box, FormControlLabel, Radio, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { sendAnswer } from "@api/quizRelase";
import { useQuizViewStore } from "@stores/quizView";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionImages } from "@model/questionTypes/images";
type ImagesProps = {
currentQuestion: QuizQuestionImages;
variant: QuestionVariant;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
index: number;
};
export const ImageVariant = ({
currentQuestion,
variant,
isSending,
setIsSending,
index,
}: ImagesProps) => {
const { quizId, preview } = useQuizData();
const { settings } = useQuizData();
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 onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
if (isSending) return;
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (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 (
<Box
sx={{
cursor: "pointer",
borderRadius: "12px",
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
"&:hover": { borderColor: theme.palette.primary.main },
background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
? "rgba(255,255,255, 0.3)"
: (settings.cfg.design && quizThemes[settings.cfg.theme].isLight) ||
quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "transparent",
}}
onClick={onVariantClick}
>
<Box sx={{ display: "flex", alignItems: "center", gap: "10px" }}>
<Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "12px 12px 0 0",
}}
/>
)}
</Box>
</Box>
<FormControlLabel
key={variant.id}
sx={{
textAlign: "center",
color: theme.palette.text.primary,
marginTop: "10px",
marginLeft: 0,
padding: "10px",
display: "flex",
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
justifyContent: "center",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
},
},
}}
value={index}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
}
label={variant.answer}
/>
</Box>
);
};

@ -0,0 +1,73 @@
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";
type ImagesProps = {
currentQuestion: QuizQuestionImages;
};
export const Images = ({ currentQuestion }: ImagesProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const answer = answers.find(
({ questionId }) => questionId === currentQuestion.id
)?.answer;
const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500;
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<Box
sx={{
display: "grid",
gap: "15px",
gridTemplateColumns: isTablet
? isMobile
? "repeat(1, 1fr)"
: "repeat(2, 1fr)"
: "repeat(3, 1fr)",
width: "100%",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<ImageVariant
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
/>
))}
</Box>
</RadioGroup>
</Box>
);
};

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

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

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

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

@ -1,148 +0,0 @@
import {
Box,
Rating as RatingComponent,
Typography,
useTheme
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import { useRootContainerSize } from "../../../contexts/RootContainerWidthContext";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react";
type RatingProps = {
currentQuestion: QuizQuestionRating;
};
const buttonRatingForm = [
{
name: "star",
icon: (color: string, width: number) => <StarIconMini width={width} color={color} />,
},
{
name: "trophie",
icon: (color: string, width: number) => <TropfyIcon width={width} color={color} />,
},
{
name: "flag",
icon: (color: string, width: number) => <FlagIcon width={width} color={color} />,
},
{
name: "heart",
icon: (color: string, width: number) => <HeartIcon width={width} color={color} />,
},
{
name: "like",
icon: (color: string, width: number) => <LikeIcon width={width} color={color} />,
},
{
name: "bubble",
icon: (color: string, width: number) => <LightbulbIcon width={width} color={color} />,
},
{
name: "hashtag",
icon: (color: string, width: number) => <HashtagIcon width={width} color={color} />,
},
];
export const Rating = ({ currentQuestion }: RatingProps) => {
const { quizId, preview } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 750;
const [isSending, setIsSending] = useState<boolean>(false);
const { answer } =
answers.find(
({ questionId }) => questionId === currentQuestion.id
) ?? {};
const form = buttonRatingForm.find(
({ name }) => name === currentQuestion.content.form
);
return (
<Box>
<Typography variant="h5" color={theme.palette.text.primary} sx={{ wordBreak: "break-word" }}>{currentQuestion.title}</Typography>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
flexDirection: "column",
}}
>
<Box
sx={{
display: "inline-block",
width: "100%",
}}
>
<RatingComponent
disabled={isSending}
value={Number(answer || 0)}
onChange={async (_, value) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: String(value) + " из " + currentQuestion.content.steps,
qid: quizId,
preview
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}}
sx={{
height: "50px",
opacity: "1!important",
"& .MuiRating-root.Mui-disabled": { opacity: "1!important" },
"& .MuiRating-icon": {mr: isMobile ? undefined : "15px"}
}}
max={currentQuestion.content.steps}
icon={form?.icon(theme.palette.primary.main, isMobile ? 30 : isTablet ? 40 : 50)}
emptyIcon={form?.icon("#9A9AAF", isMobile ? 30 : isTablet ? 40 : 50)}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
width: "100%",
}}
>
<Typography sx={{
color: "#9A9AAF"
}}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
</Box>
);
};

@ -0,0 +1,165 @@
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 { useQuizData } from "@contexts/QuizDataContext";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "@model/questionTypes/rating";
const RATING_FORM_BUTTONS = [
{
name: "star",
icon: (color: string, width: number) => (
<StarIconMini width={width} color={color} />
),
},
{
name: "trophie",
icon: (color: string, width: number) => (
<TropfyIcon width={width} color={color} />
),
},
{
name: "flag",
icon: (color: string, width: number) => (
<FlagIcon width={width} color={color} />
),
},
{
name: "heart",
icon: (color: string, width: number) => (
<HeartIcon width={width} color={color} />
),
},
{
name: "like",
icon: (color: string, width: number) => (
<LikeIcon width={width} color={color} />
),
},
{
name: "bubble",
icon: (color: string, width: number) => (
<LightbulbIcon width={width} color={color} />
),
},
{
name: "hashtag",
icon: (color: string, width: number) => (
<HashtagIcon width={width} color={color} />
),
},
];
type RatingProps = {
currentQuestion: QuizQuestionRating;
};
export const Rating = ({ currentQuestion }: RatingProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const { quizId, preview } = useQuizData();
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);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "inline-flex",
alignItems: "center",
gap: "20px",
marginTop: "20px",
flexDirection: "column",
}}
>
<Box sx={{ display: "inline-block", width: "100%" }}>
<RatingComponent
disabled={isSending}
value={Number(answer || 0)}
onChange={(_, value) => sendRating(value)}
sx={{
height: "50px",
opacity: "1!important",
"& .MuiRating-root.Mui-disabled": { opacity: "1!important" },
"& .MuiRating-icon": { mr: isMobile ? undefined : "15px" },
}}
max={currentQuestion.content.steps}
icon={form?.icon(
theme.palette.primary.main,
isMobile ? 30 : isTablet ? 40 : 50
)}
emptyIcon={form?.icon(
"#9A9AAF",
isMobile ? 30 : isTablet ? 40 : 50
)}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
width: "100%",
}}
>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingNegativeDescription}
</Typography>
<Typography sx={{ color: "#9A9AAF" }}>
{currentQuestion.content.ratingPositiveDescription}
</Typography>
</Box>
</Box>
</Box>
);
};

@ -1,99 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import { Select as SelectComponent } from "../tools//Select";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { enqueueSnackbar } from "notistack";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import { useQuizData } from "@contexts/QuizDataContext";
import { useState } from "react";
import { quizThemes } from "@utils/themes/Publication/themePublication";
type SelectProps = {
currentQuestion: QuizQuestionSelect;
};
export const Select = ({ currentQuestion }: SelectProps) => {
const theme = useTheme();
const { quizId, settings, preview } = useQuizData();
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore(state => state.answers);
const deleteAnswer = useQuizViewStore(state => state.deleteAnswer);
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<SelectComponent
disabled={isSending}
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
sx={{
"& .MuiSelect-select.MuiSelect-outlined": { zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(255,255,255, 0.3)"
: "transparent",
},
}}
onChange={async (_, value) => {
setIsSending(true);
if (value < 0) {
deleteAnswer(currentQuestion.id);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
return setIsSending(false);
}
try {
await sendAnswer({
questionId: currentQuestion.id,
body: String(
currentQuestion.content.variants[Number(value)].answer
),
qid: quizId,
preview
});
updateAnswer(currentQuestion.id, String(value), 0);
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}}
/>
</Box>
</Box>
);
};

@ -0,0 +1,102 @@
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 { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { QuizQuestionSelect } from "@model/questionTypes/select";
type SelectProps = {
currentQuestion: QuizQuestionSelect;
};
export const Select = ({ currentQuestion }: SelectProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const { quizId, settings, preview } = useQuizData();
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);
}
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);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
marginTop: "20px",
}}
>
<SelectComponent
disabled={isSending}
placeholder={currentQuestion.content.default}
activeItemIndex={answer ? Number(answer) : -1}
items={currentQuestion.content.variants.map(({ answer }) => answer)}
colorMain={theme.palette.primary.main}
sx={{
"& .MuiSelect-select.MuiSelect-outlined": { zIndex: 1 },
"& .MuiOutlinedInput-notchedOutline": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(255,255,255, 0.3)"
: "transparent",
},
}}
onChange={(_, value) => sendSelectedAnswer(value)}
/>
</Box>
</Box>
);
};

@ -1,286 +0,0 @@
import {
Box,
TextField as MuiTextField,
TextFieldProps,
Typography,
useTheme,
} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import {Answer, useQuizViewStore} from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { enqueueSnackbar } from "notistack";
import { ChangeEvent, FC, useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; // temporary fix ts(2590)
type TextProps = {
currentQuestion: QuizQuestionText;
stepNumber: number | null;
};
const Orientation = [
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: false },
{ horizontal: false },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
{ horizontal: true },
];
export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const { settings, preview } = useQuizData();
const spec = settings.cfg.spec;
const { quizId } = useQuizData();
const answers = useQuizViewStore(state => state.answers);
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const [isSending, setIsSending] = useState<boolean>(false);
const inputHC = useDebouncedCallback(async (text) => {
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: text,
qid: quizId,
preview
});
} catch (e) {
enqueueSnackbar("ответ не был засчитан");
}
setIsSending(false);
}, 400);
useEffect(
() => () => {
inputHC.flush();
},
[inputHC]
);
switch (spec) {
case true:
return (
<TextSpecial
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
stepNumber={stepNumber}
/>
);
case undefined:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
default:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
}
};
interface Props {
currentQuestion: QuizQuestionText;
answer?: Answer;
inputHC: (a: string) => void;
stepNumber?: number | null;
}
const TextNormal = ({ currentQuestion, answer, inputHC }: Props) => {
const isMobile = useRootContainerSize() < 650;
const theme = useTheme();
const { settings } = useQuizData();
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: "center",
}}
>
<CustomTextField
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={async ({ target }) => {
updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}}
sx={{
"& .MuiOutlinedInput-root": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(255,255,255, 0.3)"
: "transparent",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "#9A9AAF"
},
"&:focus-visible": { borderColor: theme.palette.primary.main },
}}
/>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box
sx={{
maxWidth: "400px",
width: "100%",
height: "300px",
margin: "15px",
}}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
</Box>
);
};
const TextSpecial = ({
currentQuestion,
answer,
inputHC,
stepNumber,
}: Props) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isHorizontal = Orientation[Number(stepNumber) - 1].horizontal;
const { settings } = useQuizData();
const updateAnswer = useQuizViewStore(state => state.updateAnswer);
return (
<Box
sx={{
display: "flex",
flexDirection: isMobile ? "column" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
gap: "20px",
}}
>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
{isHorizontal &&
currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ margin: "30px", width: "50vw", maxHeight: "550px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
{
<TextField
autoFocus={true}
multiline
maxRows={4}
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
}}
inputProps={{
maxLength: 400,
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(154,154,175, 0.2)"
: "transparent",
}}
sx={{
width: "100%",
"& .MuiOutlinedInput-root": {
backgroundColor: settings.cfg.design
? "rgba(154,154,175, 0.2)"
: "#FFFFFF",
},
"&:focus-visible": {
borderColor: theme.palette.primary.main,
},
}}
/>
}
</Box>
{!isHorizontal &&
currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ margin: "15px", width: "40vw" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
);
};

@ -0,0 +1,91 @@
import { Box, Typography, useTheme } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import { Answer, useQuizViewStore } from "@stores/quizView";
import { useQuizData } from "@contexts/QuizDataContext";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent } from "react";
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) => {
const { settings } = useQuizData();
const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 650;
const theme = useTheme();
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0);
inputHC(target.value);
};
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
width: "100%",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: "center",
}}
>
<CustomTextField
placeholder={currentQuestion.content.placeholder}
value={answer || ""}
onChange={onInputChange}
sx={{
"& .MuiOutlinedInput-root": {
background: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#F2F3F7"
: "rgba(255,255,255, 0.3)"
: "transparent",
},
"& .MuiOutlinedInput-notchedOutline": { borderColor: "#9A9AAF" },
"&:focus-visible": { borderColor: theme.palette.primary.main },
}}
/>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box
sx={{
maxWidth: "400px",
width: "100%",
height: "300px",
margin: "15px",
}}
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
</Box>
);
};

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

@ -0,0 +1,77 @@
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 { useQuizData } from "@contexts/QuizDataContext";
import type { QuizQuestionText } from "@model/questionTypes/text";
type TextProps = {
currentQuestion: QuizQuestionText;
stepNumber: number | null;
};
export const Text = ({ currentQuestion, stepNumber }: TextProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const { settings, preview } = useQuizData();
const { quizId } = useQuizData();
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 (
<TextSpecial
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
stepNumber={stepNumber}
/>
);
case undefined:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
default:
return (
<TextNormal
currentQuestion={currentQuestion}
answer={answer}
inputHC={inputHC}
/>
);
}
};

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

@ -0,0 +1,186 @@
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 { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import { CheckboxIcon } from "@icons/Checkbox";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
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>;
export const VariantItem = ({
currentQuestion,
variant,
answer,
index,
own = false,
isSending,
setIsSending,
}: {
currentQuestion: QuizQuestionVariant;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
isSending: boolean;
setIsSending: (a: boolean) => void;
}) => {
const { settings, quizId, preview } = useQuizData();
const theme = useTheme();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault();
if (isSending) {
return;
}
setIsSending(true);
const variantId = currentQuestion.content.variants[index].id;
if (currentQuestion.content.multi) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
try {
await sendAnswer({
questionId: currentQuestion.id,
body: currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
currentQuestion.content.variants[index].points || 0
);
} catch (error) {
console.log(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.log(error);
enqueueSnackbar("ответ не был засчитан");
}
if (answer === variantId) {
try {
await sendAnswer({
questionId: currentQuestion.id,
body: "",
qid: quizId,
preview,
});
} catch (error) {
console.log(error);
enqueueSnackbar("ответ не был засчитан");
}
deleteAnswer(currentQuestion.id);
}
setIsSending(false);
};
return (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
margin: "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
display: "flex",
maxWidth: "685px",
maxHeight: "85px",
justifyContent: "space-between",
width: "100%",
"&:hover": { borderColor: theme.palette.primary.main },
"&.MuiFormControl-root": { width: "100%" },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
value={index}
labelPlacement="start"
control={
currentQuestion.content.multi ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
checkedIcon={
<CheckboxIcon checked color={theme.palette.primary.main} />
}
icon={<CheckboxIcon />}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
)
}
label={own ? <TextField label="Другое..." /> : variant.answer}
onClick={sendVariant}
/>
);
};

@ -0,0 +1,124 @@
import { useEffect, useState } from "react";
import { isMoment } from "moment";
import {
Box,
FormGroup,
RadioGroup,
Typography,
useTheme,
} from "@mui/material";
import { VariantItem } from "./VariantItem";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
type VariantProps = {
currentQuestion: QuizQuestionVariant;
};
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 { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find(
(variant) => variant.id === currentQuestion.id
);
const Group = currentQuestion.content.multi ? FormGroup : RadioGroup;
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
}, []);
if (isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
gap: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
alignItems: isMobile ? "center" : undefined,
}}
>
<Group
name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
marginTop: "20px",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
width: "100%",
gap: "20px",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VariantItem
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
answer={answer}
index={index}
isSending={isSending}
setIsSending={setIsSending}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
currentQuestion={currentQuestion}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
isSending={isSending}
setIsSending={setIsSending}
/>
)}
</Box>
</Group>
{currentQuestion.content.back &&
currentQuestion.content.back !== " " && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
</Box>
)}
</Box>
</Box>
);
};

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

@ -0,0 +1,129 @@
import { FormControlLabel, Radio, useTheme } from "@mui/material";
import { enqueueSnackbar } from "notistack";
import { useQuizViewStore } from "@stores/quizView";
import { sendAnswer } from "@api/quizRelase";
import { useQuizData } from "@contexts/QuizDataContext";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import type { MouseEvent } from "react";
import type { QuestionVariant } from "@/model/questionTypes/shared";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
type VarimgVariantProps = {
currentQuestion: QuizQuestionVarImg;
variant: QuestionVariant;
index: number;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
};
export const VarimgVariant = ({
currentQuestion,
variant,
index,
isSending,
setIsSending,
}: VarimgVariantProps) => {
const { settings, quizId, preview } = useQuizData();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault();
setIsSending(true);
try {
await sendAnswer({
questionId: currentQuestion.id,
body: `${currentQuestion.content.variants[index].answer} <img style="width:100%; max-width:250px; max-height:250px" src="${currentQuestion.content.variants[index].extendedText}"/>`,
qid: quizId,
preview,
});
updateAnswer(
currentQuestion.id,
currentQuestion.content.variants[index].id,
currentQuestion.content.variants[index].points || 0
);
} catch (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 (
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
border: `1px solid`,
borderColor:
answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: 0,
justifyContent: "space-between",
"&:hover": { borderColor: theme.palette.primary.main },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
labelPlacement="start"
value={index}
onClick={sendVariant}
label={variant.answer}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
}
/>
);
};

@ -0,0 +1,130 @@
import { useState } from "react";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { VarimgVariant } from "./VarimgVariant";
import { useQuizViewStore } from "@stores/quizView";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import BlankImage from "@icons/BlankImage";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
type VarimgProps = {
currentQuestion: QuizQuestionVarImg;
};
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const { answer } =
answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const variant = currentQuestion.content.variants.find(
({ id }) => answer === id
);
return (
<Box>
<Typography
variant="h5"
color={theme.palette.text.primary}
sx={{ wordBreak: "break-word" }}
>
{currentQuestion.title}
</Typography>
<Box
sx={{
display: "flex",
marginTop: "20px",
flexDirection: isMobile ? "column-reverse" : undefined,
gap: "30px",
alignItems: isMobile ? "center" : undefined,
}}
>
<RadioGroup
name={currentQuestion.id}
value={currentQuestion.content.variants.findIndex(
({ id }) => answer === id
)}
sx={{
display: "flex",
flexWrap: "wrap",
flexDirection: "row",
justifyContent: "space-between",
flexBasis: "100%",
width: isMobile ? "100%" : undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
gap: "20px",
"&:focus": { color: theme.palette.text.primary },
"&:active": { color: theme.palette.text.primary },
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VarimgVariant
key={variant.id}
currentQuestion={currentQuestion}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
/>
))}
</Box>
</RadioGroup>
<Box
sx={{
maxWidth: "450px",
width: "100%",
height: "450px",
border: "1px solid #9A9AAF",
borderRadius: "12px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#9A9AAF30",
color: theme.palette.text.primary,
textAlign: "center",
}}
>
{answer ? (
variant?.extendedText ? (
<img
src={variant?.extendedText}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
) : (
<BlankImage />
)
) : currentQuestion.content.back !== " " &&
currentQuestion.content.back !== null &&
currentQuestion.content.back.length > 0 ? (
<img
src={currentQuestion.content.back}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
) : currentQuestion.content.replText !== " " &&
currentQuestion.content.replText.length > 0 ? (
currentQuestion.content.replText
) : variant?.extendedText || isMobile ? (
"Выберите вариант ответа ниже"
) : (
"Выберите вариант ответа слева"
)}
</Box>
</Box>
</Box>
);
};