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"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages:
- build
- deploy
- service-discovery
build-app:
tags:
- frontbuild
@ -30,3 +34,6 @@ deploy-to-prod:
tags:
- front
- prod
service-discovery:
extends: .sd_artefacts_template

@ -3,6 +3,9 @@ services:
respondent:
container_name: respondent
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
hostname: respondent
tty: true

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

@ -32,8 +32,6 @@ type Props = {
};
//Костыль для особого квиза. Для него не нужно показывать email адрес
const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8");
console.log("isDisableEmail");
console.log(isDisableEmail);
export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const theme = useTheme();
@ -120,8 +118,6 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
async function handleShowResultsClick() {
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)) {
return enqueueSnackbar("введена некорректная почта");
}

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

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

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { Box, Button, Link, Typography, useTheme } from "@mui/material";
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 (
<Box
sx={{
@ -166,7 +176,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
}}
/>
)}
{resultQuestion?.content.useImage && resultQuestion.content.back && (
{resultQuestion?.content.useImage && choiceImgUrlQuestion && (
<Box
sx={{
width: "100%",
@ -176,7 +186,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
>
<img
alt="resultImage"
src={resultQuestion.content.back}
src={choiceImgUrlQuestion}
style={{
width: "100%",
height: spec ? "auto" : isMobile ? "236px" : "306px",

@ -20,8 +20,9 @@ import NextButton from "./tools/NextButton";
import PrevButton from "./tools/PrevButton";
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 ownVariants = useQuizViewStore((state) => state.ownVariants);
let currentQuizStep = useQuizViewStore((state) => state.currentQuizStep);
const {
currentQuestion,
@ -99,7 +100,7 @@ export default function ViewPublicationPage() {
if (preview) return;
sendQuestionAnswer(quizId, currentQuestion, currentAnswer)?.catch((e) => {
sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => {
enqueueSnackbar("Ошибка при отправке ответа");
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 DateRange from "./DateRange";
import DatePicker from "./DatePicker";
import { Box, Typography, useTheme } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { Moment } from "moment";
import moment from "moment";
type DateProps = {
currentQuestion: QuizQuestionDate;
};
export const Date = ({ currentQuestion }: DateProps) => {
const { 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>
@ -35,52 +19,11 @@ export const Date = ({ currentQuestion }: DateProps) => {
>
{currentQuestion.title}
</Typography>
<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>
{currentQuestion.content.isRange ? (
<DateRange currentQuestion={currentQuestion} />
) : (
<DatePicker currentQuestion={currentQuestion} />
)}
</Box>
);
};

@ -1,6 +1,16 @@
import type { QuestionVariant } from "@/model/questionTypes/shared";
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 RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -14,18 +24,111 @@ type EmojiVariantProps = {
questionId: string;
variant: QuestionVariant;
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 answers = useQuizViewStore((state) => state.answers);
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const theme = useTheme();
const { answer } = answers.find((answer) => answer.questionId === questionId) ?? {};
const onVariantClick = async (event: MouseEvent<HTMLDivElement>) => {
event.preventDefault();
const variantId = variant.id;
if (isMulti) {
const currentAnswer = typeof answer !== "string" ? answer || [] : [];
return updateAnswer(
questionId,
currentAnswer.includes(variantId)
? currentAnswer?.filter((item) => item !== variantId)
: [...currentAnswer, variantId],
variant.points || 0
);
}
updateAnswer(questionId, variant.id, variant.points || 0);
if (answer === variant.id) {
@ -39,7 +142,7 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
borderColor: answer?.includes(variant.id) ? theme.palette.primary.main : "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
@ -74,6 +177,17 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
{variant.extendedText && <Typography fontSize="100px">{variant.extendedText}</Typography>}
</Box>
</Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
Введите свой ответ
</Typography>
)}
<FormControlLabel
key={variant.id}
sx={{
@ -85,6 +199,7 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
alignItems: variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
overflow: "auto",
justifyContent: "center",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
@ -92,8 +207,9 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
overflow: "auto",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
@ -101,16 +217,34 @@ export const EmojiVariant = ({ variant, index, questionId }: EmojiVariantProps)
}}
value={index}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{ position: "absolute", top: "-162px", right: "12px" }}
/>
isMulti ? (
<Checkbox
checked={!!answer?.includes(variant.id)}
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={
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography>
</Box>
own ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
<Box sx={{ display: "flex", gap: "10px" }}>
<Typography sx={{ wordBreak: "break-word", lineHeight: "normal" }}>{variant.answer}</Typography>
</Box>
)
}
/>
</FormControl>

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

@ -61,7 +61,10 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
>
{answer?.split("|")[0]}
</Typography>
<IconButton sx={{ p: 0 }} onClick={deleteFile}>
<IconButton
sx={{ p: 0 }}
onClick={deleteFile}
>
<CloseBold />
</IconButton>
</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 { 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 RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
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 = {
questionId: string;
variant: QuestionVariant;
variant: QuestionVariantWithEditedImages;
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 answers = useQuizViewStore((state) => state.answers);
const { deleteAnswer, updateAnswer } = useQuizViewStore((state) => state);
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>) => {
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);
}
};
const choiceImgUrl = useMemo(() => {
if (variant.editedUrlImagesList !== undefined && variant.editedUrlImagesList !== null) {
return variant.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return variant.extendedText;
}
}, []);
return (
<Box
sx={{
position: "relative",
cursor: "pointer",
borderRadius: "12px",
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 },
background:
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" }}>
{variant.extendedText && (
<img
src={variant.extendedText}
src={choiceImgUrl}
alt=""
style={{
display: "block",
@ -64,6 +169,17 @@ export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
)}
</Box>
</Box>
{own && (
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
Введите свой ответ
</Typography>
)}
<FormControlLabel
key={variant.id}
sx={{
@ -80,29 +196,57 @@ export const ImageVariant = ({ questionId, variant, index }: ImagesProps) => {
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
overflow: "auto",
maxHeight: "100%",
width: "100%",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: "#b8babf",
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
}}
value={index}
control={
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
isMulti ? (
<Checkbox
id="cock"
checked={!!answer?.includes(variant.id)}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
) : (
<Radio
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
sx={{
position: "absolute",
top: "-297px",
right: 0,
}}
/>
)
}
label={
own ? (
<OwnInput
questionId={questionId}
variant={variant}
largeCheck={questionLargeCheck}
ownPlaceholder={ownPlaceholder || "|"}
/>
) : (
variant.answer
)
}
label={variant.answer}
/>
</Box>
);

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

@ -8,7 +8,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
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";
interface TextNormalProps {
@ -21,12 +21,22 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
const { settings } = useQuizSettings();
const { updateAnswer } = useQuizViewStore((state) => state);
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const theme = useTheme();
const onInputChange = async ({ target }: ChangeEvent<HTMLInputElement>) => {
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 (
<Box>
<Typography
@ -61,7 +71,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
"&:focus-visible": { borderColor: theme.palette.primary.main },
}}
/>
{currentQuestion.content.back && currentQuestion.content.back !== " " && (
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box
sx={{
maxWidth: "400px",
@ -72,7 +82,7 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => {
>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>

@ -1,7 +1,17 @@
import { useQuizSettings } from "@contexts/QuizDataContext";
import { CheckboxIcon } from "@icons/Checkbox";
import type { QuestionVariant } from "@model/questionTypes/shared";
import { Checkbox, FormControlLabel, TextField as MuiTextField, Radio, TextFieldProps, useTheme } from "@mui/material";
import {
Checkbox,
FormControlLabel,
Input,
TextField as MuiTextField,
Radio,
TextFieldProps,
TextareaAutosize,
Typography,
useTheme,
} from "@mui/material";
import { useQuizViewStore } from "@stores/quizView";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -10,6 +20,70 @@ import type { FC, MouseEvent } from "react";
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 = ({
questionId,
isMulti,
@ -17,13 +91,17 @@ export const VariantItem = ({
answer,
index,
own = false,
questionLargeCheck,
ownPlaceholder,
}: {
isMulti: boolean;
questionId: string;
variant: QuestionVariant;
answer: string | string[] | undefined;
index: number;
own?: boolean;
own: boolean;
questionLargeCheck: boolean;
ownPlaceholder: string;
}) => {
const { settings } = useQuizSettings();
const theme = useTheme();
@ -57,7 +135,9 @@ export const VariantItem = ({
<FormControlLabel
key={variant.id}
sx={{
position: "relative",
margin: "0",
mt: own ? "10px" : "0",
borderRadius: "12px",
color: theme.palette.text.primary,
padding: "15px",
@ -78,12 +158,15 @@ export const VariantItem = ({
"&:hover": { borderColor: theme.palette.primary.main },
"&.MuiFormControl-root": { width: "100%" },
"& .MuiFormControlLabel-label": {
width: "100%",
maxHeight: "100%",
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
"&::-webkit-scrollbar": { width: "4px" },
"&::-webkit-scrollbar-thumb": { backgroundColor: "#b8babf" },
"&::-webkit-scrollbar-thumb": { backgroundColor: theme.palette.primary.main },
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
@ -93,15 +176,10 @@ export const VariantItem = ({
labelPlacement="start"
control={
isMulti ? (
<Checkbox
<Radio
checked={!!answer?.includes(variant.id)}
checkedIcon={
<CheckboxIcon
checked
color={theme.palette.primary.main}
/>
}
icon={<CheckboxIcon />}
checkedIcon={<RadioCheck color={theme.palette.primary.main} />}
icon={<RadioIcon />}
/>
) : (
<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}
/>
);

@ -1,5 +1,5 @@
import { Box, FormGroup, RadioGroup, Typography, useTheme } from "@mui/material";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { VariantItem } from "./VariantItem";
@ -16,6 +16,7 @@ type VariantProps = {
export const Variant = ({ currentQuestion }: VariantProps) => {
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
@ -32,6 +33,16 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const choiceImgUrlQuestion = useMemo(() => {
if (
currentQuestion.content.editedUrlImagesList !== undefined &&
currentQuestion.content.editedUrlImagesList !== null
) {
return currentQuestion.content.editedUrlImagesList[isMobile ? "mobile" : isTablet ? "tablet" : "desktop"];
} else {
return currentQuestion.content.back;
}
}, [currentQuestion]);
if (moment.isMoment(answer)) throw new Error("Answer is Moment in Variant question");
return (
@ -73,33 +84,31 @@ export const Variant = ({ currentQuestion }: VariantProps) => {
gap: "20px",
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VariantItem
key={variant.id}
questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={variant}
answer={answer}
index={index}
/>
))}
{currentQuestion.content.own && ownVariant && (
<VariantItem
own
questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={ownVariant.variant}
answer={answer}
index={currentQuestion.content.variants.length + 2}
/>
)}
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<VariantItem
key={variant.id}
questionId={currentQuestion.id}
isMulti={currentQuestion.content.multi}
variant={variant}
answer={answer}
index={index}
own={Boolean(variant.isOwn)}
questionLargeCheck={currentQuestion.content.largeCheck}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
/>
))}
</Box>
</Group>
{currentQuestion.content.back && currentQuestion.content.back !== " " && (
{choiceImgUrlQuestion && choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && (
<Box sx={{ maxWidth: "400px", width: "100%", height: "300px" }}>
<img
key={currentQuestion.id}
src={currentQuestion.content.back}
src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
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 { 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 RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { quizThemes } from "@utils/themes/Publication/themePublication";
import type { MouseEvent } from "react";
import { type MouseEvent } from "react";
import { useDebouncedCallback } from "use-debounce";
type VarimgVariantProps = {
questionId: string;
variant: QuestionVariant;
variant: QuestionVariantWithEditedImages;
index: number;
isSending: boolean;
setIsSending: (isSending: boolean) => void;
questionLargeCheck: boolean;
isMulti: boolean;
answer: string | string[] | undefined;
ownPlaceholder: string;
};
export const VarimgVariant = ({ questionId, variant, index, isSending, setIsSending }: VarimgVariantProps) => {
const { settings } = useQuizSettings();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const answers = useQuizViewStore((state) => state.answers);
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",
},
"&::-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 { answer } = answers.find((answer) => answer.questionId === questionId) ?? {};
const { settings } = useQuizSettings();
const { updateAnswer, deleteAnswer } = useQuizViewStore((state) => state);
const sendVariant = async (event: MouseEvent<HTMLLabelElement>) => {
event.preventDefault();
@ -34,50 +121,145 @@ export const VarimgVariant = ({ questionId, variant, index, isSending, setIsSend
}
};
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 />}
if (variant?.isOwn) {
return (
<Box>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: "14px",
pl: "15px",
}}
>
Введите свой ответ
</Typography>
<FormControlLabel
key={variant.id}
disabled={isSending}
sx={{
marginBottom: "15px",
borderRadius: "12px",
padding: "20px",
color: theme.palette.text.primary,
backgroundColor: settings.cfg.design
? quizThemes[settings.cfg.theme].isLight
? "#FFFFFF"
: "rgba(255,255,255, 0.3)"
: quizThemes[settings.cfg.theme].isLight
? "white"
: theme.palette.background.default,
border: `1px solid`,
borderColor: answer === variant.id ? theme.palette.primary.main : "#9A9AAF",
display: "flex",
margin: 0,
justifyContent: "space-between",
"&:hover": { borderColor: theme.palette.primary.main },
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
lineHeight: "normal",
width: "100%",
"&::-webkit-scrollbar": {
width: "4px",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
},
scrollbarColor: theme.palette.primary.main,
},
"& .MuiFormControlLabel-label.Mui-disabled": {
color: theme.palette.text.primary,
},
}}
labelPlacement="start"
value={index}
onClick={sendVariant}
label={
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 { VarimgVariant } from "./VarimgVariant";
@ -9,6 +9,7 @@ import { useRootContainerSize } from "@contexts/RootContainerWidthContext";
import BlankImage from "@icons/BlankImage";
import type { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import moment from "moment";
type VarimgProps = {
currentQuestion: QuizQuestionVarImg;
@ -17,13 +18,46 @@ type VarimgProps = {
export const Varimg = ({ currentQuestion }: VarimgProps) => {
const [isSending, setIsSending] = useState<boolean>(false);
const answers = useQuizViewStore((state) => state.answers);
const ownVariants = useQuizViewStore((state) => state.ownVariants);
const updateOwnVariant = useQuizViewStore((state) => state.updateOwnVariant);
const theme = useTheme();
const isMobile = useRootContainerSize() < 650;
const isTablet = useRootContainerSize() < 850;
const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.id) ?? {};
const ownVariant = ownVariants.find((variant) => variant.id === currentQuestion.id);
const variant = currentQuestion.content.variants.find(({ id }) => answer === id);
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 (
<Box>
<Typography
@ -64,16 +98,25 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
"&:active": { color: theme.palette.text.primary },
}}
>
{currentQuestion.content.variants.map((variant, index) => (
<VarimgVariant
key={variant.id}
questionId={currentQuestion.id}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
/>
))}
{currentQuestion.content.variants
.filter((v) => {
if (!v.isOwn) return true;
return v.isOwn && currentQuestion.content.own;
})
.map((variant, index) => (
<VarimgVariant
key={variant.id}
questionId={currentQuestion.id}
variant={variant}
isSending={isSending}
setIsSending={setIsSending}
index={index}
questionLargeCheck={currentQuestion.content.largeCheck}
ownPlaceholder={currentQuestion.content?.ownPlaceholder || ""}
isMulti={Boolean(currentQuestion.content?.multi)}
answer={answer}
/>
))}
</Box>
</RadioGroup>
<Box
@ -93,21 +136,19 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
}}
>
{answer ? (
variant?.extendedText ? (
choiceImgUrlAnswer ? (
<img
key={variant.extendedText}
src={variant.extendedText}
key={choiceImgUrlAnswer}
src={choiceImgUrlAnswer}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>
) : (
<BlankImage />
)
) : currentQuestion.content.back !== " " &&
currentQuestion.content.back !== null &&
currentQuestion.content.back.length > 0 ? (
) : choiceImgUrlQuestion !== " " && choiceImgUrlQuestion !== null && choiceImgUrlQuestion.length > 0 ? (
<img
src={currentQuestion.content.back}
src={choiceImgUrlQuestion}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
alt=""
/>

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

@ -20,5 +20,7 @@ export interface QuizQuestionEmoji extends QuizQuestionBase {
back: string | null;
originalBack: string | null;
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 {
type: "images";
@ -21,12 +21,14 @@ export interface QuizQuestionImages extends QuizQuestionBase {
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Варианты (картинки) */
variants: QuestionVariant[];
variants: QuestionVariantWithEditedImages[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string | null;
originalBack: string | null;
autofill: 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 {
minScore?: number;
@ -15,6 +15,7 @@ export interface QuizQuestionResult extends QuizQuestionBase {
price: [number] | [number, number];
useImage: boolean;
rule: ResultQuestionBranchingRule;
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint;
autofill: boolean;
usage: boolean;

@ -1,3 +1,4 @@
import { type } from "os";
import type { QuizQuestionDate } from "./date";
import type { QuizQuestionEmoji } from "./emoji";
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 {
children: string[];
//список условий
@ -43,10 +47,15 @@ export type QuestionVariant = {
hints: string;
/** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string;
isOwn?: boolean;
isMulti?: boolean;
/** Оригинал изображения (до кропа) */
originalImageUrl: string;
points?: number;
};
export interface QuestionVariantWithEditedImages extends QuestionVariant {
editedUrlImagesList?: EditedUrlImagesList | null;
}
export type QuestionType =
| "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 {
type: "text";
@ -14,6 +14,7 @@ export interface QuizQuestionText extends QuizQuestionBase {
/** Чекбокс "Автозаполнение адреса" */
autofill: boolean;
answerType: "single" | "multi" | "numberOnly";
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint;
rule: QuestionBranchingRule;
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 {
type: "variant";
@ -14,6 +20,7 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
innerNameCheck: boolean;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
editedUrlImagesList?: EditedUrlImagesList | null;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Варианты ответов */
@ -23,5 +30,6 @@ export interface QuizQuestionVariant extends QuizQuestionBase {
back: string | null;
originalBack: string | null;
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 {
type: "varimg";
@ -12,7 +18,8 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
variants: QuestionVariantWithEditedImages[];
editedUrlImagesList?: EditedUrlImagesList | null;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string | null;
@ -20,5 +27,8 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
autofill: boolean;
largeCheck: boolean;
replText: string;
/** Чекбокс "Можно несколько" */
multi?: boolean;
ownPlaceholder?: string;
};
}

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

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

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

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

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

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

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