Merge branch 'staging'

This commit is contained in:
Nastya 2024-11-24 18:48:18 +03:00
commit a461c8dc3d
37 changed files with 1205 additions and 261 deletions

3
CHANGELOG.md Normal file

@ -0,0 +1,3 @@
1.0.2 Страничка результатов способна показать историю и правильность ответов для балловго квиза
1.0.1 Отображение для баллового квиза на страничке результатов списка
1.0.0 Добавлены фичи "мультиответ", "перенос строки в своём ответе", "свой ответ", "плейсхолдер своего ответа"

@ -3,9 +3,13 @@ include:
file: "/templates/docker/build-template.gitlab-ci.yml" file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration" - project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml" file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages: stages:
- build - build
- deploy - deploy
- service-discovery
build-app: build-app:
tags: tags:
- frontbuild - frontbuild
@ -30,3 +34,6 @@ deploy-to-prod:
tags: tags:
- front - front
- prod - prod
service-discovery:
extends: .sd_artefacts_template

@ -3,6 +3,9 @@ services:
respondent: respondent:
container_name: respondent container_name: respondent
restart: unless-stopped restart: unless-stopped
labels:
com.pena.domains: s.hbpn.link
com.pena.front_headers: "Access-Control-Allow-Origin $$http_origin always"
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent hostname: respondent
tty: true tty: true

@ -20,9 +20,13 @@ import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage"; import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import "moment/dist/locale/ru";
moment.locale("ru"); moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
console.log(localeText);
console.log(moment);
type Props = { type Props = {
quizSettings?: QuizSettings; quizSettings?: QuizSettings;
quizId: string; quizId: string;

@ -32,8 +32,6 @@ type Props = {
}; };
//Костыль для особого квиза. Для него не нужно показывать email адрес //Костыль для особого квиза. Для него не нужно показывать email адрес
const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8"); const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8");
console.log("isDisableEmail");
console.log(isDisableEmail);
export const ContactForm = ({ currentQuestion, onShowResult }: Props) => { export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const theme = useTheme(); const theme = useTheme();
@ -120,8 +118,6 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
async function handleShowResultsClick() { async function handleShowResultsClick() {
const FC = settings.cfg.formContact.fields; const FC = settings.cfg.formContact.fields;
console.log("некорректная");
console.log(!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email));
if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) { if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) {
return enqueueSnackbar("введена некорректная почта"); return enqueueSnackbar("введена некорректная почта");
} }

@ -40,9 +40,6 @@ export const Inputs = ({
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const FC = settings.cfg.formContact.fields; const FC = settings.cfg.formContact.fields;
console.log("crutch.disableEmail");
console.log(crutch.disableEmail);
if (!FC) return null; if (!FC) return null;
const Name = ( const Name = (
<CustomInput <CustomInput

@ -43,7 +43,10 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
<Typography sx={{ color: theme.palette.text.primary }}> <Typography sx={{ color: theme.palette.text.primary }}>
Вопрос {stepNumber} из {questionsAmount} Вопрос {stepNumber} из {questionsAmount}
</Typography> </Typography>
<Stepper activeStep={stepNumber} steps={questionsAmount} /> <Stepper
activeStep={stepNumber}
steps={questionsAmount}
/>
</Box> </Box>
)} )}
{prevButton} {prevButton}

@ -1,4 +1,4 @@
import { useEffect } from "react"; import { useEffect, useMemo } from "react";
import { Box, Button, Link, Typography, useTheme } from "@mui/material"; import { Box, Button, Link, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@/stores/quizView"; import { useQuizViewStore } from "@/stores/quizView";
@ -76,6 +76,16 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
})(); })();
}, []); }, []);
const choiceImgUrlQuestion = useMemo(() => {
if (
resultQuestion.content.editedUrlImagesList !== undefined &&
resultQuestion.content.editedUrlImagesList !== null
) {
return resultQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return resultQuestion.content.back;
}
}, [resultQuestion]);
return ( return (
<Box <Box
sx={{ sx={{
@ -166,7 +176,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
}} }}
/> />
)} )}
{resultQuestion?.content.useImage && resultQuestion.content.back && ( {resultQuestion?.content.useImage && choiceImgUrlQuestion && (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
@ -176,7 +186,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
> >
<img <img
alt="resultImage" alt="resultImage"
src={resultQuestion.content.back} src={choiceImgUrlQuestion}
style={{ style={{
width: "100%", width: "100%",
height: spec ? "auto" : isMobile ? "236px" : "306px", height: spec ? "auto" : isMobile ? "236px" : "306px",

@ -20,8 +20,9 @@ import NextButton from "./tools/NextButton";
import PrevButton from "./tools/PrevButton"; import PrevButton from "./tools/PrevButton";
export default function ViewPublicationPage() { export default function ViewPublicationPage() {
const { settings, recentlyCompleted, quizId, preview, changeFaviconAndTitle } = useQuizSettings(); const { settings, recentlyCompleted, quizId, preview, changeFaviconAndTitle, questions } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep); let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep);
const { const {
currentQuestion, currentQuestion,
@ -99,7 +100,7 @@ export default function ViewPublicationPage() {
if (preview) return; if (preview) return;
sendQuestionAnswer(quizId, currentQuestion, currentAnswer)?.catch((e) => { sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => {
enqueueSnackbar("Ошибка при отправке ответа"); enqueueSnackbar("Ошибка при отправке ответа");
console.error("Error sending answer", e); console.error("Error sending answer", e);
}); });

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

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

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

@ -1,6 +1,16 @@
import type { QuestionVariant } from "@/model/questionTypes/shared"; import type { QuestionVariant } from "@/model/questionTypes/shared";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { Box, FormControl, FormControlLabel, Radio, Typography, useTheme } from "@mui/material"; import {
Box,
Checkbox,
FormControl,
FormControlLabel,
Input,
Radio,
TextareaAutosize,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
@ -14,18 +24,111 @@ type EmojiVariantProps = {
questionId: string; questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
index: number; index: number;
isMulti: boolean;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
answer: string | string[] | undefined;
}; };
export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps) => { interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<Box sx={{ overflow: "auto" }}>
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? theme.palette.ownPlaceholder.main : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::placeholder": {
color: theme.palette.ownPlaceholder.main,
opacity: 0.65,
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
overflow: "auto",
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
</Box>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const EmojiVariant = ({
answer,
variant,
index,
questionId,
isMulti,
own,
questionLargeCheck,
ownPlaceholder,
}: EmojiVariantProps) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state); const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {};
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => { const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
const variantId = variant.id;
if (isMulti) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
return updateAnswer(
questionId,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
variant.points || 0
);
}
updateAnswer(questionId, variant.id, variant.points || 0); updateAnswer(questionId, variant.id, variant.points || 0);
if (answer === variant.id) { if (answer === variant.id) {
@ -39,7 +142,7 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
sx={{ sx={{
borderRadius: "12px", borderRadius: "12px",
border: `1px solid`, border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF", borderColor: answer?.includes(variant.id) ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden", overflow: "hidden",
maxWidth: "317px", maxWidth: "317px",
width: "100%", width: "100%",
@ -74,6 +177,17 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
{variant.extendedText && <Typography fontSize="100px">{variant.extendedText}</Typography>} {variant.extendedText && <Typography fontSize="100px">{variant.extendedText}</Typography>}
</Box> </Box>
</Box> </Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
Введите свой ответ
</Typography>
)}
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
sx={{ sx={{
@ -85,6 +199,7 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
alignItems: variant.answer.length <= 60 ? "center" : "flex-start", alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative", position: "relative",
height: "80px", height: "80px",
overflow: "auto",
justifyContent: "center", justifyContent: "center",
"& .MuiFormControlLabel-label": { "& .MuiFormControlLabel-label": {
wordBreak: "break-word", wordBreak: "break-word",
@ -92,8 +207,9 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
overflow: "auto", overflow: "auto",
"&::-webkit-scrollbar": { width: "4px" }, "&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { "&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf", backgroundColor: theme.palette.primary.main,
}, },
scrollbarColor: theme.palette.primary.main,
}, },
"& .MuiFormControlLabel-label.Mui-disabled": { "& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -101,16 +217,34 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
}} }}
value={index} value={index}
control={ control={
<Radio isMulti ? (
checkedIcon={<RadioCheck color={theme.palette.primary.main} />} <Checkbox
icon={<RadioIcon />} checked={!!answer?.includes(variant.id)}
sx={{ position: "absolute", top: "-162px", right: "12px" }} checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
/> icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
)
} }
label={ label={
<Box sx={{ display: "flex", gap: "10px" }}> own ? (
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography> <OwnInput
</Box> questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography>
</Box>
)
} }
/> />
</FormControl> </FormControl>

@ -3,6 +3,7 @@ import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import { EmojiVariant } from "./EmojiVariant"; import { EmojiVariant } from "./EmojiVariant";
import moment from "moment";
polyfillCountryFlagEmojis(); polyfillCountryFlagEmojis();
@ -16,6 +17,8 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
const theme = useTheme(); const theme = useTheme();
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return ( return (
<Box> <Box>
<Typography <Typography
@ -44,14 +47,24 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => {
}} }}
> >
<Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}> <Box sx={{ display: "flex", width: "100%", gap: "42px", flexWrap: "wrap" }}>
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants
<EmojiVariant .filter((v) => {
key={variant.id} if (!v.isOwn) return true;
questionId={currentQuestion.id} return v.isOwn && currentQuestion.content.own;
variant={variant} })
index={index} .map((variant, index) => (
/> <EmojiVariant
))} key={variant.id}
questionId={currentQuestion.id}
variant={variant}
index={index}
isMulti={Boolean(currentQuestion.content.multi)}
own={Boolean(variant.isOwn)}
questionLargeCheck={true}
answer={answer}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box> </Box>
</RadioGroup> </RadioGroup>
</Box> </Box>

@ -61,7 +61,10 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
> >
{answer?.split("|")[0]} {answer?.split("|")[0]}
</Typography> </Typography>
<IconButton sx={{ p: 0 }} onClick={deleteFile}> <IconButton
sx={{ p: 0 }}
onClick={deleteFile}
>
<CloseBold /> <CloseBold />
</IconButton> </IconButton>
</Box> </Box>

@ -1,42 +1,147 @@
import type { QuestionVariant } from "@/model/questionTypes/shared"; import { CheckboxIcon } from "@/assets/icons/Checkbox";
import type { QuestionVariant, QuestionVariantWithEditedImages } from "@/model/questionTypes/shared";
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { Box, FormControlLabel, Radio, useTheme } from "@mui/material"; import { Box, Checkbox, FormControlLabel, Input, Radio, TextareaAutosize, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { MouseEvent } from "react"; import { useMemo, type MouseEvent } from "react";
import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
type ImagesProps = { type ImagesProps = {
questionId: string; questionId: string;
variant: QuestionVariant; variant: QuestionVariantWithEditedImages;
index: number; index: number;
answer: string | string[] | undefined;
isMulti: boolean;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
}; };
export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => { interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<Box sx={{ overflow: "auto" }}>
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? theme.palette.ownPlaceholder.main : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::placeholder": {
color: theme.palette.ownPlaceholder.main,
opacity: 0.65,
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
</Box>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const ImageVariant = ({
questionId,
answer,
isMulti,
variant,
index,
own,
questionLargeCheck,
ownPlaceholder,
}: ImagesProps) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const answers = useQuizViewStore((state) => state.answers);
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state); const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
const theme = useTheme(); const theme = useTheme();
const answer = answers.find((answer) => answer.questionId === questionId)?.answer; const answers = useQuizViewStore((state) => state.answers);
const isMobile = useRootContainerSize() < 450;
const isTablet = useRootContainerSize() < 850;
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => { const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
updateAnswer(questionId, variant.id, variant.points || 0); const variantId = variant.id;
if (isMulti) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
if (answer === variant.id) { return updateAnswer(
questionId,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
variant.points || 0
);
}
updateAnswer(questionId, variantId, variant.points || 0);
if (answer === variantId) {
deleteAnswer(questionId); deleteAnswer(questionId);
} }
}; };
const choiceImgUrl = useMemo(() => {
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return variant.extendedText;
}
}, []);
return ( return (
<Box <Box
sx={{ sx={{
position: "relative",
cursor: "pointer", cursor: "pointer",
borderRadius: "12px", borderRadius: "12px",
border: `1px solid`, border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF", borderColor: !!answer?.includes(variant.id) ? theme.palette.primary.main : "#9A9AAF",
"&:hover": { borderColor: theme.palette.primary.main }, "&:hover": { borderColor: theme.palette.primary.main },
background: background:
settings.cfg.design && !quizThemes[settings.cfg.theme].isLight settings.cfg.design && !quizThemes[settings.cfg.theme].isLight
@ -51,7 +156,7 @@ export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
<Box sx={{ width: "100%", height: "300px" }}> <Box sx={{ width: "100%", height: "300px" }}>
{variant.extendedText && ( {variant.extendedText && (
<img <img
src={variant.extendedText} src={choiceImgUrl}
alt="" alt=""
style={{ style={{
display: "block", display: "block",
@ -64,6 +169,17 @@ export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
)} )}
</Box> </Box>
</Box> </Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
Введите свой ответ
</Typography>
)}
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
sx={{ sx={{
@ -80,29 +196,57 @@ export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
"& .MuiFormControlLabel-label": { "& .MuiFormControlLabel-label": {
wordBreak: "break-word", wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px", height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal", lineHeight: "normal",
overflow: "auto",
maxHeight: "100%",
width: "100%",
"&::-webkit-scrollbar": { "&::-webkit-scrollbar": {
width: "4px", width: "4px",
}, },
"&::-webkit-scrollbar-thumb": { "&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf", backgroundColor: theme.palette.primary.main,
}, },
scrollbarColor: theme.palette.primary.main,
}, },
}} }}
value={index} value={index}
control={ control={
<Radio isMulti ? (
checkedIcon={<RadioCheck color={theme.palette.primary.main} />} <Checkbox
icon={<RadioIcon />} id="cock"
sx={{ checked={!!answer?.includes(variant.id)}
position: "absolute", checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
top: "-297px", icon={<RadioIcon />}
right: 0, sx={{
}} position: "absolute",
/> top: "-297px",
right: 0,
}}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
)
}
label={
own ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
variant.answer
)
} }
label={variant.answer}
/> />
</Box> </Box>
); );

@ -1,8 +1,9 @@
import { useRootContainerSize } from "@contexts/RootContainerWidthContext"; import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import type { QuizQuestionImages } from "@model/questionTypes/images"; import type { QuizQuestionImages } from "@model/questionTypes/images";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material"; import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { createQuizViewStore, useQuizViewStore } from "@stores/quizView";
import { ImageVariant } from "./ImageVariant"; import { ImageVariant } from "./ImageVariant";
import moment from "moment";
type ImagesProps = { type ImagesProps = {
currentQuestion: QuizQuestionImages; currentQuestion: QuizQuestionImages;
@ -15,6 +16,8 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
const isTablet = useRootContainerSize() < 1000; const isTablet = useRootContainerSize() < 1000;
const isMobile = useRootContainerSize() < 500; const isMobile = useRootContainerSize() < 500;
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return ( return (
<Box> <Box>
<Typography <Typography
@ -25,7 +28,7 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
{currentQuestion.title} {currentQuestion.title}
</Typography> </Typography>
<RadioGroup <RadioGroup
name={currentQuestion.id} name={currentQuestion.id.toString()}
value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)} value={currentQuestion.content.variants.findIndex(({ id }) => answer === id)}
sx={{ sx={{
display: "flex", display: "flex",
@ -43,14 +46,24 @@ export const Images = ({ currentQuestion }: ImagesProps) => {
width: "100%", width: "100%",
}} }}
> >
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants
<ImageVariant .filter((v) => {
key={variant.id} if (!v.isOwn) return true;
questionId={currentQuestion.id} return v.isOwn && currentQuestion.content.own;
variant={variant} })
index={index} .map((variant, index) => (
/> <ImageVariant
))} key={variant.id}
questionId={currentQuestion.id}
variant={variant}
index={index}
answer={answer}
isMulti={Boolean(currentQuestion.content.multi)}
own={Boolean(variant.isOwn)}
questionLargeCheck={true}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box> </Box>
</RadioGroup> </RadioGroup>
</Box> </Box>

@ -8,7 +8,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { ChangeEvent } from "react"; import { useMemo, type ChangeEvent } from "react";
import type { QuizQuestionText } from "@model/questionTypes/text"; import type { QuizQuestionText } from "@model/questionTypes/text";
interface TextNormalProps { interface TextNormalProps {
@ -21,12 +21,22 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state); const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const theme = useTheme(); const theme = useTheme();
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => { const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
updateAnswer(currentQuestion.id, target.value, 0); updateAnswer(currentQuestion.id, target.value, 0);
}; };
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [currentQuestion]);
return ( return (
<Box> <Box>
<Typography <Typography
@ -61,7 +71,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
"&:focus-visible": { borderColor: theme.palette.primary.main }, "&:focus-visible": { borderColor: theme.palette.primary.main },
}} }}
/> />
{currentQuestion.content.back && currentQuestion.content.back !== " " && ( {choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box <Box
sx={{ sx={{
maxWidth: "400px", maxWidth: "400px",
@ -72,7 +82,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
> >
<img <img
key={currentQuestion.id} key={currentQuestion.id}
src={currentQuestion.content.back} src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt="" alt=""
/> />

@ -1,7 +1,17 @@
import { useQuizSettings } from "@contexts/QuizDataContext"; import { useQuizSettings } from "@contexts/QuizDataContext";
import { CheckboxIcon } from "@icons/Checkbox"; import { CheckboxIcon } from "@icons/Checkbox";
import type { QuestionVariant } from "@model/questionTypes/shared"; import type { QuestionVariant } from "@model/questionTypes/shared";
import { Checkbox, FormControlLabel, TextField as MuiTextField, Radio, TextFieldProps, useTheme } from "@mui/material"; import {
Checkbox,
FormControlLabel,
Input,
TextField as MuiTextField,
Radio,
TextFieldProps,
TextareaAutosize,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView"; import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck"; import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon"; import RadioIcon from "@ui_kit/RadioIcon";
@ -10,6 +20,70 @@ import type { FC, MouseEvent } from "react";
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
interface OwnInputProps {
questionId: string;
variant: QuestionVariant;
largeCheck: boolean;
ownPlaceholder: string;
}
const OwnInput = ({ questionId, variant, largeCheck, ownPlaceholder }: OwnInputProps) => {
const theme = useTheme();
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const { updateOwnVariant } = useQuizViewStore((state) => state);
const ownAnswer = ownVariants[ownVariants.findIndex((v) => v.id === variant.id)]?.variant.answer || "";
return largeCheck ? (
<TextareaAutosize
placeholder={ownPlaceholder || "|"}
style={{
resize: "none",
width: "100%",
fontSize: "16px",
color: ownAnswer.length === 0 ? theme.palette.ownPlaceholder.main : theme.palette.text.primary,
letterSpacing: "-0.4px",
wordSpacing: "-3px",
outline: "0px none",
backgroundColor: "inherit",
border: "none",
//@ts-ignore
"&::-webkit-scrollbar": {
width: "4px",
},
"&::placeholder": {
color: theme.palette.ownPlaceholder.main,
opacity: "0.65!important",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
}}
value={ownAnswer}
onClick={(e: React.MouseEvent<HTMLTextAreaElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
) : (
<Input
placeholder={ownPlaceholder || "|"}
sx={{
backgroundColor: "inherit",
width: "100%",
fontSize: "18px",
color: theme.palette.text.primary,
}}
value={ownAnswer}
disableUnderline
onClick={(e: React.MouseEvent<HTMLElement>) => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
updateOwnVariant(variant.id, e.target.value);
}}
/>
);
};
export const VariantItem = ({ export const VariantItem = ({
questionId, questionId,
isMulti, isMulti,
@ -17,13 +91,17 @@ export const VariantItem = ({
answer, answer,
index, index,
own = false, own = false,
questionLargeCheck,
ownPlaceholder,
}: { }: {
isMulti: boolean; isMulti: boolean;
questionId: string; questionId: string;
variant: QuestionVariant; variant: QuestionVariant;
answer: string | string[] | undefined; answer: string | string[] | undefined;
index: number; index: number;
own?: boolean; own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
}) => { }) => {
const { settings } = useQuizSettings(); const { settings } = useQuizSettings();
const theme = useTheme(); const theme = useTheme();
@ -57,7 +135,9 @@ export const VariantItem = ({
<FormControlLabel <FormControlLabel
key={variant.id} key={variant.id}
sx={{ sx={{
position: "relative",
margin: "0", margin: "0",
mt: own ? "10px" : "0",
borderRadius: "12px", borderRadius: "12px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: "15px", padding: "15px",
@ -78,12 +158,15 @@ export const VariantItem = ({
"&:hover": { borderColor: theme.palette.primary.main }, "&:hover": { borderColor: theme.palette.primary.main },
"&.MuiFormControl-root": { width: "100%" }, "&.MuiFormControl-root": { width: "100%" },
"& .MuiFormControlLabel-label": { "& .MuiFormControlLabel-label": {
width: "100%",
maxHeight: "100%",
wordBreak: "break-word", wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px", height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto", overflow: "auto",
lineHeight: "normal", lineHeight: "normal",
"&::-webkit-scrollbar": { width: "4px" }, "&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" }, "&::-webkit-scrollbar-thumb": { backgroundColor: theme.palette.primary.main },
scrollbarColor: theme.palette.primary.main,
}, },
"& .MuiFormControlLabel-label.Mui-disabled": { "& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -93,15 +176,10 @@ export const VariantItem = ({
labelPlacement="start" labelPlacement="start"
control={ control={
isMulti ? ( isMulti ? (
<Checkbox <Radio
checked={!!answer?.includes(variant.id)} checked={!!answer?.includes(variant.id)}
checkedIcon={ checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
<CheckboxIcon icon={<RadioIcon />}
checked
color={theme.palette.primary.main}
/>
}
icon={<CheckboxIcon />}
/> />
) : ( ) : (
<Radio <Radio
@ -110,7 +188,30 @@ export const VariantItem = ({
/> />
) )
} }
label={own ? <TextField label="Другое..." /> : variant.answer} label={
own ? (
<>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
position: "absolute",
top: "-23px",
}}
>
Введите свой ответ
</Typography>
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
</>
) : (
variant.answer
)
}
onClick={sendVariant} onClick={sendVariant}
/> />
); );

@ -1,5 +1,5 @@
import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material"; import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material";
import { useEffect } from "react"; import { useEffect, useMemo } from "react";
import { VariantItem } from "./VariantItem"; import { VariantItem } from "./VariantItem";
@ -16,6 +16,7 @@ type VariantProps = {
export const Variant = ({ currentQuestion }: VariantProps) => { export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants); const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant); const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
@ -32,6 +33,16 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [currentQuestion]);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question"); if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return ( return (
@ -73,33 +84,31 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
gap: "20px", gap: "20px",
}} }}
> >
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants
<VariantItem .filter((v) => {
key={variant.id} if (!v.isOwn) return true;
questionId={currentQuestion.id} return v.isOwn && currentQuestion.content.own;
isMulti={currentQuestion.content.multi} })
variant={variant} .map((variant, index) => (
answer={answer} <VariantItem
index={index} key={variant.id}
/> questionId={currentQuestion.id}
))} isMulti={currentQuestion.content.multi}
{currentQuestion.content.own && ownVariant && ( variant={variant}
<VariantItem answer={answer}
own index={index}
questionId={currentQuestion.id} own={Boolean(variant.isOwn)}
isMulti={currentQuestion.content.multi} questionLargeCheck={currentQuestion.content.largeCheck}
variant={ownVariant.variant} ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
answer={answer} />
index={currentQuestion.content.variants.length + 2} ))}
/>
)}
</Box> </Box>
</Group> </Group>
{currentQuestion.content.back && currentQuestion.content.back !== " " && ( {choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}> <Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img <img
key={currentQuestion.id} key={currentQuestion.id}
src={currentQuestion.content.back} src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt="" alt=""
/> />

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

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Box, RadioGroup, Typography, useTheme } from "@mui/material"; import { Box, RadioGroup, Typography, useTheme } from "@mui/material";
import { VarimgVariant } from "./VarimgVariant"; import { VarimgVariant } from "./VarimgVariant";
@ -9,6 +9,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import BlankImage from "@icons/BlankImage"; import BlankImage from "@icons/BlankImage";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg"; import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import moment from "moment";
type VarimgProps = { type VarimgProps = {
currentQuestion: QuizQuestionVarImg; currentQuestion: QuizQuestionVarImg;
@ -17,13 +18,46 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => { export const Varimg = ({ currentQuestion }: VarimgProps) => {
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {}; const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
const variant = currentQuestion.content.variants.find(({ id }) => answer === id); const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
useEffect(() => {
if (!ownVariant) {
updateOwnVariant(currentQuestion.id, "");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const choiceImgUrlAnswer = useMemo(() => {
if (variant !== undefined) {
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return variant.extendedText;
}
}
}, [variant]);
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [variant]);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return ( return (
<Box> <Box>
<Typography <Typography
@ -64,16 +98,25 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
"&:active": { color: theme.palette.text.primary }, "&:active": { color: theme.palette.text.primary },
}} }}
> >
{currentQuestion.content.variants.map((variant, index) => ( {currentQuestion.content.variants
<VarimgVariant .filter((v) => {
key={variant.id} if (!v.isOwn) return true;
questionId={currentQuestion.id} return v.isOwn && currentQuestion.content.own;
variant={variant} })
isSending={isSending} .map((variant, index) => (
setIsSending={setIsSending} <VarimgVariant
index={index} key={variant.id}
/> questionId={currentQuestion.id}
))} variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
questionLargeCheck={currentQuestion.content.largeCheck}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
isMulti={Boolean(currentQuestion.content?.multi)}
answer={answer}
/>
))}
</Box> </Box>
</RadioGroup> </RadioGroup>
<Box <Box
@ -93,21 +136,19 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
}} }}
> >
{answer ? ( {answer ? (
variant?.extendedText ? ( choiceImgUrlAnswer ? (
<img <img
key={variant.extendedText} key={choiceImgUrlAnswer}
src={variant.extendedText} src={choiceImgUrlAnswer}
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt="" alt=""
/> />
) : ( ) : (
<BlankImage /> <BlankImage />
) )
) : currentQuestion.content.back !== " " && ) : choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && choiceImgUrlQuestion.length > 0 ? (
currentQuestion.content.back !== null &&
currentQuestion.content.back.length > 0 ? (
<img <img
src={currentQuestion.content.back} src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }} style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt="" alt=""
/> />

@ -17,5 +17,6 @@ export interface QuizQuestionDate extends QuizQuestionBase {
back: string | null; back: string | null;
originalBack: string | null; originalBack: string | null;
autofill: boolean; autofill: boolean;
isRange?: boolean;
}; };
} }

@ -20,5 +20,7 @@ export interface QuizQuestionEmoji extends QuizQuestionBase {
back: string | null; back: string | null;
originalBack: string | null; originalBack: string | null;
autofill: boolean; autofill: boolean;
ownPlaceholder?: string;
isLargeCheck?: boolean;
}; };
} }

@ -1,4 +1,4 @@
import type { QuestionHint, QuestionVariant, QuizQuestionBase, QuestionBranchingRule } from "./shared"; import type { QuestionHint, QuestionVariantWithEditedImages, QuizQuestionBase, QuestionBranchingRule } from "./shared";
export interface QuizQuestionImages extends QuizQuestionBase { export interface QuizQuestionImages extends QuizQuestionBase {
type: "images"; type: "images";
@ -21,12 +21,14 @@ export interface QuizQuestionImages extends QuizQuestionBase {
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Варианты (картинки) */ /** Варианты (картинки) */
variants: QuestionVariant[]; variants: QuestionVariantWithEditedImages[];
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string | null; back: string | null;
originalBack: string | null; originalBack: string | null;
autofill: boolean; autofill: boolean;
largeCheck: boolean; largeCheck: boolean;
ownPlaceholder?: string;
isLargeCheck?: boolean;
}; };
} }

@ -1,4 +1,4 @@
import type { QuizQuestionBase, QuestionBranchingRule, QuestionHint } from "./shared"; import type { QuizQuestionBase, QuestionBranchingRule, QuestionHint, EditedUrlImagesList } from "./shared";
interface ResultQuestionBranchingRule extends QuestionBranchingRule { interface ResultQuestionBranchingRule extends QuestionBranchingRule {
minScore?: number; minScore?: number;
@ -15,6 +15,7 @@ export interface QuizQuestionResult extends QuizQuestionBase {
price: [number] | [number, number]; price: [number] | [number, number];
useImage: boolean; useImage: boolean;
rule: ResultQuestionBranchingRule; rule: ResultQuestionBranchingRule;
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint; hint: QuestionHint;
autofill: boolean; autofill: boolean;
usage: boolean; usage: boolean;

@ -1,3 +1,4 @@
import { type } from "os";
import type { QuizQuestionDate } from "./date"; import type { QuizQuestionDate } from "./date";
import type { QuizQuestionEmoji } from "./emoji"; import type { QuizQuestionEmoji } from "./emoji";
import type { QuizQuestionFile } from "./file"; import type { QuizQuestionFile } from "./file";
@ -20,6 +21,9 @@ export interface QuestionBranchingRuleMain {
}[]; }[];
} }
export type EditedImagesScreens = "desktop" | "tablet" | "mobile" | "small";
export type EditedUrlImagesList = Record<EditedImagesScreens, string>;
export interface QuestionBranchingRule { export interface QuestionBranchingRule {
children: string[]; children: string[];
//список условий //список условий
@ -43,10 +47,15 @@ export type QuestionVariant = {
hints: string; hints: string;
/** Дополнительное поле для текста, emoji, ссылки на картинку */ /** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string; extendedText: string;
isOwn?: boolean;
isMulti?: boolean;
/** Оригинал изображения (до кропа) */ /** Оригинал изображения (до кропа) */
originalImageUrl: string; originalImageUrl: string;
points?: number; points?: number;
}; };
export interface QuestionVariantWithEditedImages extends QuestionVariant {
editedUrlImagesList?: EditedUrlImagesList | null;
}
export type QuestionType = export type QuestionType =
| "variant" | "variant"

@ -1,4 +1,4 @@
import type { QuizQuestionBase, QuestionHint, QuestionBranchingRule } from "./shared"; import type { QuizQuestionBase, QuestionHint, QuestionBranchingRule, EditedUrlImagesList } from "./shared";
export interface QuizQuestionText extends QuizQuestionBase { export interface QuizQuestionText extends QuizQuestionBase {
type: "text"; type: "text";
@ -14,6 +14,7 @@ export interface QuizQuestionText extends QuizQuestionBase {
/** Чекбокс "Автозаполнение адреса" */ /** Чекбокс "Автозаполнение адреса" */
autofill: boolean; autofill: boolean;
answerType: "single" | "multi" | "numberOnly"; answerType: "single" | "multi" | "numberOnly";
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string | null; back: string | null;

@ -1,4 +1,10 @@
import type { QuizQuestionBase, QuestionVariant, QuestionHint, QuestionBranchingRule } from "./shared"; import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
QuestionBranchingRule,
EditedUrlImagesList,
} from "./shared";
export interface QuizQuestionVariant extends QuizQuestionBase { export interface QuizQuestionVariant extends QuizQuestionBase {
type: "variant"; type: "variant";
@ -14,6 +20,7 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
innerNameCheck: boolean; innerNameCheck: boolean;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
editedUrlImagesList?: EditedUrlImagesList | null;
/** Поле "Внутреннее название вопроса" */ /** Поле "Внутреннее название вопроса" */
innerName: string; innerName: string;
/** Варианты ответов */ /** Варианты ответов */
@ -23,5 +30,6 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
back: string | null; back: string | null;
originalBack: string | null; originalBack: string | null;
autofill: boolean; autofill: boolean;
ownPlaceholder?: string;
}; };
} }

@ -1,4 +1,10 @@
import type { QuestionHint, QuestionVariant, QuizQuestionBase, QuestionBranchingRule } from "./shared"; import type {
QuestionHint,
QuestionVariantWithEditedImages,
EditedUrlImagesList,
QuizQuestionBase,
QuestionBranchingRule,
} from "./shared";
export interface QuizQuestionVarImg extends QuizQuestionBase { export interface QuizQuestionVarImg extends QuizQuestionBase {
type: "varimg"; type: "varimg";
@ -12,7 +18,8 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
innerName: string; innerName: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
variants: QuestionVariant[]; variants: QuestionVariantWithEditedImages[];
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string | null; back: string | null;
@ -20,5 +27,8 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
autofill: boolean; autofill: boolean;
largeCheck: boolean; largeCheck: boolean;
replText: string; replText: string;
/** Чекбокс "Можно несколько" */
multi?: boolean;
ownPlaceholder?: string;
}; };
} }

@ -14,7 +14,7 @@ export type QuestionAnswer = {
answer: Answer; answer: Answer;
}; };
type OwnVariant = { export type OwnVariant = {
id: string; id: string;
variant: QuestionVariant; variant: QuestionVariant;
}; };
@ -99,7 +99,7 @@ export const createQuizViewStore = () =>
state.ownVariants.push({ state.ownVariants.push({
id, id,
variant: { variant: {
id: nanoid(), id: id,
answer, answer,
extendedText: "", extendedText: "",
hints: "", hints: "",

@ -33,6 +33,8 @@ export function useQuestionFlowControl() {
//Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке
const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0];
// console.log(currentQuestion)
//Индекс текущего вопроса только если квиз линейный //Индекс текущего вопроса только если квиз линейный
const linearQuestionIndex = //: number | null const linearQuestionIndex = //: number | null
currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled

@ -1,13 +1,14 @@
import { sendAnswer } from "@/api/quizRelase"; import { sendAnswer } from "@/api/quizRelase";
import { RealTypedQuizQuestion } from "@/model/questionTypes/shared"; import { RealTypedQuizQuestion } from "@/model/questionTypes/shared";
import { QuestionAnswer } from "@/stores/quizView"; import { OwnVariant, QuestionAnswer, createQuizViewStore } from "@/stores/quizView";
import moment from "moment"; import moment from "moment";
import { notReachable } from "./notReachable"; import { notReachable } from "./notReachable";
export function sendQuestionAnswer( export function sendQuestionAnswer(
quizId: string, quizId: string,
question: RealTypedQuizQuestion, question: RealTypedQuizQuestion,
questionAnswer: QuestionAnswer | undefined questionAnswer: QuestionAnswer | undefined,
ownVariants: OwnVariant[]
) { ) {
if (!questionAnswer) { if (!questionAnswer) {
return sendAnswer({ return sendAnswer({
@ -18,15 +19,67 @@ export function sendQuestionAnswer(
} }
switch (question.type) { switch (question.type) {
case "date": { case "date": {
if (!moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send answer in date question"); let answer = "";
if (question.content.isRange) {
if (!Array.isArray(questionAnswer.answer)) throw new Error("Cannot send answer in range question");
let from = Number(questionAnswer.answer[0]);
let to = Number(questionAnswer.answer[1]);
if (
from !== 0 &&
to !== 0 &&
from !== Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]))
) {
from = Math.min(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
to = Math.max(Number(questionAnswer.answer[0]), Number(questionAnswer.answer[1]));
}
answer = `${!from ? "_" : moment(from).format("YYYY.MM.DD")} - ${!to ? "_" : moment(to).format("YYYY.MM.DD")}`;
} else {
if (!moment.isMoment(questionAnswer.answer)) throw new Error("Cannot send answer in date question");
answer = moment(questionAnswer.answer).format("YYYY.MM.DD");
}
return sendAnswer({ return sendAnswer({
questionId: question.id, questionId: question.id,
body: moment(questionAnswer.answer).format("YYYY.MM.DD"), body: answer,
qid: quizId, qid: quizId,
}); });
} }
case "emoji": { case "emoji": {
if (question.content.multi) {
const answer = questionAnswer.answer;
const ownVariant = Array.isArray(answer)
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant || ""
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant || "";
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
//Оставляем только выбранные варианты
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
let answerString = ``;
selectedVariants.forEach((e) => {
if (e.isOwn) {
if (question.content.own && selectedVariants.some((v) => v.isOwn)) {
answerString += `\`${e.extendedText} ${ownVariant?.answer ?? ""}\`,`;
}
} else {
answerString += `\`${e.extendedText} ${e.answer ?? ""}\`,`;
}
});
answerString = answerString.slice(0, -1);
return sendAnswer({
questionId: question.id,
body: answerString,
qid: quizId,
});
}
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer); const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`); if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
@ -40,7 +93,40 @@ export function sendQuestionAnswer(
return; return;
} }
case "images": { case "images": {
if (question.content.multi) {
const answer = questionAnswer.answer;
const ownAnswer = Array.isArray(answer)
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
?.answer || ""
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
"";
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
//Оставляем только выбранные варианты
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
let answerString = ``;
selectedVariants.forEach((e) => {
if (!e.isOwn || (e.isOwn && question.content.own)) {
const body = {
Image: e.extendedText,
Description: e.isOwn ? ownAnswer : e.answer,
};
answerString += `\`${JSON.stringify(body)}\`,`;
}
});
answerString = answerString.slice(0, -1);
return sendAnswer({
questionId: question.id,
body: answerString,
qid: quizId,
});
}
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer); const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`); if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
const body = { const body = {
Image: variant.extendedText, Image: variant.extendedText,
@ -99,13 +185,30 @@ export function sendQuestionAnswer(
case "variant": { case "variant": {
if (question.content.multi) { if (question.content.multi) {
const answer = questionAnswer.answer; const answer = questionAnswer.answer;
if (!Array.isArray(answer)) throw new Error("Cannot send answer in select question"); if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
const ownAnswer = Array.isArray(answer)
? ownVariants[ownVariants.findIndex((variant) => answer.some((a: string) => a === variant.id))]?.variant
?.answer || ""
: ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer ||
"";
//Оставляем только выбранные варианты
const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id)); const selectedVariants = question.content.variants.filter((v) => answer.includes(v.id));
let answerString = ``;
selectedVariants.forEach((e) => {
if (!e.isOwn) answerString += `\`${e.answer}\`,`;
});
if (question.content.own && selectedVariants.some((v) => v.isOwn)) {
answerString += `\`${ownAnswer}\`,`;
}
answerString = answerString.slice(0, -1);
return sendAnswer({ return sendAnswer({
questionId: question.id, questionId: question.id,
body: selectedVariants.map((v) => v.answer).join(", "), body: answerString,
qid: quizId, qid: quizId,
}); });
} }
@ -121,16 +224,19 @@ export function sendQuestionAnswer(
} }
case "varimg": { case "varimg": {
const variant = question.content.variants.find((v) => v.id === questionAnswer.answer); const variant = question.content.variants.find((v) => v.id === questionAnswer.answer);
const ownAnswer =
ownVariants[ownVariants.findIndex((variant) => variant.id === questionAnswer.answer)]?.variant?.answer || "";
if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`); if (!variant) throw new Error(`Cannot find variant with id ${questionAnswer.answer} in question ${question.id}`);
const body = { const body = {
Image: variant.extendedText, Image: variant.extendedText,
Description: variant.answer, Description: question.content.own ? ownAnswer : variant.answer,
}; };
if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`); if (!body) throw new Error(`Body of answer in question ${question.id} is undefined`);
return sendAnswer({ return sendAnswer({
questionId: question.id, questionId: question.id,
body: JSON.stringify(body), body: `\`${JSON.stringify(body)}\``,
qid: quizId, qid: quizId,
}); });
} }

@ -42,6 +42,9 @@ const darkTheme = createTheme({
navbarbg: { navbarbg: {
main: "#333647", main: "#333647",
}, },
ownPlaceholder: {
main: "(51, 54, 71, 0.65)",
},
}, },
}); });

@ -46,6 +46,9 @@ const lightTheme = createTheme({
orange: { orange: {
main: "#FB5607", main: "#FB5607",
}, },
ownPlaceholder: {
main: "1,1,1,0.65",
},
navbarbg: { navbarbg: {
main: "#FFFFFF", main: "#FFFFFF",
}, },

@ -12,6 +12,7 @@ declare module "@mui/material/styles" {
grey4: Palette["primary"]; grey4: Palette["primary"];
orange: Palette["primary"]; orange: Palette["primary"];
navbarbg: Palette["primary"]; navbarbg: Palette["primary"];
ownPlaceholder: Palette["primary"];
} }
interface PaletteOptions { interface PaletteOptions {
lightPurple?: PaletteOptions["primary"]; lightPurple?: PaletteOptions["primary"];
@ -24,6 +25,7 @@ declare module "@mui/material/styles" {
grey4?: PaletteOptions["primary"]; grey4?: PaletteOptions["primary"];
orange?: PaletteOptions["primary"]; orange?: PaletteOptions["primary"];
navbarbg?: PaletteOptions["primary"]; navbarbg?: PaletteOptions["primary"];
ownPlaceholder?: PaletteOptions["primary"];
} }
interface TypographyVariants { interface TypographyVariants {
infographic: React.CSSProperties; infographic: React.CSSProperties;

@ -1,6 +1,6 @@
{ {
"name": "@frontend/squzanswerer", "name": "@frontend/squzanswerer",
"version": "1.0.55", "version": "1.0.56",
"type": "module", "type": "module",
"main": "./dist-package/index.js", "main": "./dist-package/index.js",
"module": "./dist-package/index.js", "module": "./dist-package/index.js",