feat: staged

This commit is contained in:
IlyaDoronin 2024-04-17 10:11:02 +03:00
commit f0f08c4f7c
44 changed files with 1425 additions and 2893 deletions

@ -6,8 +6,8 @@
"@craco/craco": "^7.0.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.78",
"@frontend/squzanswerer": "^1.0.22",
"@frontend/kitui": "^1.0.82",
"@frontend/squzanswerer": "^1.0.37",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",

@ -21,10 +21,10 @@ import {
createUserAccount,
devlog,
getMessageFromFetchError,
makeRequest,
UserAccount,
useUserFetcher,
} from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import type { OriginalUserAccount } from "@root/user";
import {
clearUserData,

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import type {
LoginRequest,

@ -1,5 +1,5 @@
import { UserAccount, makeRequest } from "@frontend/kitui";
import { AxiosError } from "axios";
import { UserAccount } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";

43
src/api/makeRequest.ts Normal file

@ -0,0 +1,43 @@
import * as KIT from "@frontend/kitui";
import { Method, ResponseType, AxiosError } from "axios";
import { clearAuthToken } from "@frontend/kitui";
import { cleanAuthTicketData } from "@root/ticket";
import { clearUserData } from "@root/user";
import { clearQuizData } from "@root/quizes/store";
import { redirect } from "react-router-dom";
interface MakeRequest {
method?: Method | undefined;
url: string;
body?: unknown;
useToken?: boolean | undefined;
contentType?: boolean | undefined;
responseType?: ResponseType | undefined;
signal?: AbortSignal | undefined;
withCredentials?: boolean | undefined;
}
async function makeRequest<TRequest = unknown, TResponse = unknown>(
data: MakeRequest,
): Promise<TResponse> {
try {
const response = await KIT.makeRequest<unknown>(data);
return response as TResponse;
} catch (e) {
const error = e as AxiosError;
//@ts-ignore
if (
error.response?.status === 400 &&
error.response?.data?.message === "refreshToken is empty"
) {
cleanAuthTicketData();
clearAuthToken();
clearUserData();
clearQuizData();
redirect("/");
}
throw e;
}
}
export default makeRequest;

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
@ -15,7 +15,6 @@ export async function activatePromocode(promocode: string) {
contentType: true,
body: { codeword: promocode },
});
console.log(response);
return response.greetings;
} catch (nativeError) {

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { CreateQuestionRequest } from "model/question/create";
import { RawQuestion } from "model/question/question";
import {

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { defaultQuizConfig } from "@model/quizSettings";
import { CopyQuizRequest, CopyQuizResponse } from "model/quiz/copy";
import { CreateQuizRequest } from "model/quiz/create";

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { RawResult } from "@model/result/result";
interface IResultListBody {
@ -45,21 +45,6 @@ function deleteResult(resultId: number) {
});
}
// export const obsolescenceResult = async (idResultArray: string[]) => {
// try {
// const response = await makeRequest<unknown, unknown>({
// url: process.env.REACT_APP_DOMAIN + `/squiz/result/seen`,
// body: {
// answers: idResultArray,
// },
// method: "PATCH",
// });
// return response;
// } catch (e) {
// console.log("ошибка", e);
// }
// };
function obsolescenceResult(idResultArray: number[]) {
return makeRequest<unknown, unknown>({
url: process.env.REACT_APP_DOMAIN + `/squiz/result/seen`,

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";

@ -1,4 +1,4 @@
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "../utils/parse-error";
import { SendTicketMessageRequest } from "@frontend/kitui";

@ -70,6 +70,7 @@ export type QuizTheme =
| "Design10";
export interface QuizConfig {
spec: undefined | true;
type: QuizType;
noStartPage: boolean;
startpageType: QuizStartpageType;
@ -140,6 +141,7 @@ export type FieldSettingsDrawerState = {
};
export const defaultQuizConfig: QuizConfig = {
spec: undefined,
type: null,
noStartPage: false,
startpageType: null,

@ -41,9 +41,6 @@ export default function Analytics() {
const [isOpenEnd, setOpenEnd] = useState<boolean>(false);
const [from, setFrom] = useState<Moment | null>(null);
const [to, setTo] = useState<Moment | null>(moment().add(1, "days"));
console.log(moment(to).unix() - moment(from).unix());
console.log(86400 - (moment(to).unix() - moment(from).unix()));
console.log(86400 - (moment(to).unix() - moment(from).unix()) > 0);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));

@ -1,4 +1,5 @@
import { FC, useEffect, useMemo, useState } from "react";
import type { PaginationRenderItemParams } from "@mui/material";
import {
Box,
ButtonBase,
@ -16,8 +17,7 @@ import { ReactComponent as DoubleCheckIcon } from "@icons/Analytics/doubleCheck.
import { ReactComponent as NextIcon } from "@icons/Analytics/next.svg";
import { ReactComponent as LeftArrowIcon } from "@icons/Analytics/leftArrow.svg";
import { ReactComponent as RightArrowIcon } from "@icons/Analytics/rightArrow.svg";
import type { PaginationRenderItemParams } from "@mui/material";
import { extractOrder } from "@utils/extractOrder";
type AnswerProps = {
title: string;
@ -200,16 +200,17 @@ const Pagination = ({ page, setPage, pagesAmount }: PaginationProps) => {
export const Answers: FC<AnswersProps> = ({ data }) => {
const [page, setPage] = useState<number>(1);
const theme = useTheme();
const answers = useMemo(
() => (data === null ? [] : Object.entries(data ?? {})),
[data],
);
const answers = useMemo(() => {
const unsortedAnswers = data === null ? [] : Object.entries(data ?? {});
return unsortedAnswers.sort(
([titleA], [titleB]) => extractOrder(titleA) - extractOrder(titleB),
);
}, [data]);
const currentAnswer = answers[page - 1];
const percentsSum = Object.values(currentAnswer?.[1] ?? {}).reduce(
(total, item) => (total += item),
0,
);
const currentAnswerExtended =
percentsSum >= 100
? Object.entries(currentAnswer?.[1] ?? {})

@ -80,7 +80,6 @@ export const DesignPage = ({ heightSidebar, mobileSidebar }: Props) => {
follow={followNewPage}
cancel={() => setShowConfirmLeaveModal(false)}
/>
{createPortal(<QuizPreview />, document.body)}
</>
);
};

@ -3,12 +3,10 @@ import { useUserStore } from "@root/user";
import { Link } from "react-router-dom";
export const InfoPrivilege = () => {
const user = useUserStore();
console.log(user);
return (
<Box>
<Link to="/list">К списку квизов</Link>
{Object.values(user?.userAccount?.privileges || {}).map((privilege) => {
console.log(privilege);
return (
<Box
sx={{

@ -27,7 +27,7 @@ export const useGetData = (filterNew: string, filterDate: string): void => {
parseFilters(filterNew, filterDate),
);
if (result.total_count === 0) {
console.log("No results found");
console.error("No results found");
}
setResults(result);
}

@ -1,7 +1,8 @@
import { logout } from "@api/auth";
import { activatePromocode } from "@api/promocode";
import type { Tariff } from "@frontend/kitui";
import { clearAuthToken, makeRequest, useToken } from "@frontend/kitui";
import { useToken } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import type { GetTariffsResponse } from "@model/tariff";
import {
@ -108,8 +109,6 @@ function TariffPage() {
const openModalHC = (tariffInfo: any) => setOpenModal(tariffInfo);
const tryBuy = async ({ id, price }: { id: string; price: number }) => {
console.log("цена", price);
// console.log("мои деньги", (user.wallet.cash / 100))
openModalHC({});
//Если в корзине что-то было - выкладываем содержимое и запоминаем чо там лежало
if (user.cart.length > 0) {
@ -127,7 +126,6 @@ function TariffPage() {
method: "POST",
url: process.env.REACT_APP_DOMAIN + "/customer/cart/pay",
});
console.log(data);
setCash(
currencyFormatter.format(Number(data.wallet.cash) / 100),
Number(data.wallet.cash),
@ -142,6 +140,7 @@ function TariffPage() {
link.href = `https://${isTestServer ? "s" : ""}hub.pena.digital/quizpayment?action=squizpay&dif=${cashDif}&data=${token}&userid=${userId}`;
document.body.appendChild(link);
link.click();
return;
}
//другая ошибка
enqueueSnackbar("Произошла ошибка. Попробуйте позже");
@ -390,7 +389,7 @@ export const inCart = () => {
}
localStorage.setItem("saveCart", JSON.stringify(saveCart));
} catch (e) {
console.log("Я не смог добавить тариф в корзину :( " + id);
console.error("Я не смог добавить тариф в корзину :( " + id);
}
});
} else {
@ -409,7 +408,7 @@ const outCart = (cart: string[]) => {
saveCart = saveCart.push(id);
localStorage.setItem("saveCart", JSON.stringify(saveCart));
} catch (e) {
console.log("Я не смог удалить из корзины тариф :(");
console.error("Я не смог удалить из корзины тариф :(");
}
});
};

@ -41,11 +41,11 @@ export default function ViewPublicationPage() {
);
if (quizesError) {
console.log(`Error fetching quiz ${quizId}`, quizesError);
console.error(`Error fetching quiz ${quizId}`, quizesError);
return null;
}
if (questionsError) {
console.log(`Error fetching questions ${quizId}`, questionsError);
console.error(`Error fetching questions ${quizId}`, questionsError);
return null;
}
@ -78,6 +78,7 @@ export default function ViewPublicationPage() {
rep: quiz.repeatable,
cfg: quiz.config,
},
show_badge: true,
}}
quizId={quizId}
preview

@ -18,8 +18,8 @@ import { object, string } from "yup";
import { useEffect, useState } from "react";
import { useUserStore } from "@root/user";
import { getAuthToken, makeRequest, setAuthToken } from "@frontend/kitui";
import { FormContactFieldName } from "@model/quizSettings";
import makeRequest from "@api/makeRequest";
import { setAuthToken } from "@frontend/kitui";
import { parseAxiosError } from "@utils/parse-error";
interface Values {
password: string;
@ -32,7 +32,10 @@ const initialValues: Values = {
const validationSchema = object({
password: string()
.min(8, "Минимум 8 символов")
.matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы")
.matches(
/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/,
"Некорректные символы",
)
.required("Поле обязательно"),
});

@ -39,7 +39,7 @@ const validationSchema = object({
password: string()
.min(8, "Минимум 8 символов")
.matches(
/^[.,:;\-_+!&()<>\[\]{}№`@"'~*|#$₽%^=?\d\w]+$/,
/^[.,:;\-_+!&()*<>\[\]\{\}`@"#$\%\^\=?\d\w]+$/,
"Некорректные символы",
)
.required("Поле обязательно"),

@ -29,7 +29,6 @@ export default function AvailablePrivilege() {
}
},
});
console.log("это доступные привелегии", userPrivileges);
const DayForm = ["день", "дня", "дней"];
function declOfNum(n: number, text_forms: string[]) {
n = Math.abs(n) % 100;

@ -15,7 +15,7 @@ import {
import { deleteQuiz, setEditQuizId } from "@root/quizes/actions";
import { Link, useNavigate } from "react-router-dom";
import { inCart } from "../../pages/Tariffs/Tariffs";
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { enqueueSnackbar } from "notistack";
import { useDomainDefine } from "@utils/hooks/useDomainDefine";
import CopyIcon from "@icons/CopyIcon";

@ -11,6 +11,5 @@ export const setCash = (
cashCop: number,
cashRub: number,
) => {
console.log("я получил ", cashString);
useWallet.setState({ cashString, cashCop, cashRub });
};

@ -3,14 +3,12 @@ import { devtools } from "zustand/middleware";
interface QuizPreviewStore {
isPreviewShown: boolean;
currentQuestionIndex: number;
}
export const useQuizPreviewStore = create<QuizPreviewStore>()(
devtools(
(set, get) => ({
isPreviewShown: false,
currentQuestionIndex: 0,
}),
{
name: "quizPreview",
@ -29,18 +27,3 @@ export const toggleQuizPreview = () =>
useQuizPreviewStore.setState((state) => ({
isPreviewShown: !state.isPreviewShown,
}));
export const setCurrentQuestionIndex = (step: number) =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: (state.currentQuestionIndex = step),
}));
export const incrementCurrentQuestionIndex = (maxStep: number) =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: Math.min(state.currentQuestionIndex + 1, maxStep),
}));
export const decrementCurrentQuestionIndex = () =>
useQuizPreviewStore.setState((state) => ({
currentQuestionIndex: Math.max(state.currentQuestionIndex - 1, 0),
}));

@ -3,7 +3,7 @@ import { useUserStore } from "@root/user";
import { Box, Button, Modal, Typography } from "@mui/material";
import { mutate } from "swr";
import { enqueueSnackbar } from "notistack";
import { makeRequest } from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
export function CheckFastlink() {

@ -1,10 +1,10 @@
import {
TicketMessage,
makeRequest,
useSSESubscription,
useTicketMessages,
useTicketsFetcher,
} from "@frontend/kitui";
import makeRequest from "@api/makeRequest";
import FloatingSupportChat from "./FloatingSupportChat";
import { useUserStore } from "@root/user";
import { useCallback, useEffect, useState } from "react";
@ -192,7 +192,6 @@ export default () => {
// fileType => file.name.toLowerCase().endsWith(fileType)
// );
// if (!isFileTypeAccepted) return setModalWarningType("errorType");
console.log("тут ошибка", modalWarningType);
let data;
if (!ticket.sessionData?.ticketId) {
try {

@ -1,16 +1,13 @@
import { Box, IconButton, ThemeProvider } from "@mui/material";
import { toggleQuizPreview, useQuizPreviewStore } from "@root/quizPreview";
import { QuizAnswerer } from "@frontend/squzanswerer";
import ResizeIcon from "@icons/ResizeIcon";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ThemeProvider } from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizPreviewStore } from "@root/quizPreview";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { themesPublication } from "@utils/themes/Publication/themePublication";
import { useLayoutEffect, useRef } from "react";
import { Rnd } from "react-rnd";
import { useWindowSize } from "../../utils/hooks/useWindowSize";
import QuizPreviewLayout from "./QuizPreviewLayout";
import ResizeIcon from "@icons/ResizeIcon";
import { themesPublication } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
const DRAG_PARENT_MARGIN = 0;
const NAVBAR_HEIGHT = 0;
const DRAG_PARENT_BOTTOM_MARGIN = 0;
interface RndPositionAndSize {
x: number;
@ -23,6 +20,9 @@ export default function QuizPreview() {
const isPreviewShown = useQuizPreviewStore((state) => state.isPreviewShown);
const rndParentRef = useRef<HTMLDivElement>(null);
const quiz = useCurrentQuiz();
const questions = useQuestionsStore((state) => state.questions).filter(
(q): q is AnyTypedQuizQuestion => q.type !== null,
);
const rndRef = useRef<Rnd | null>(null);
const rndPositionAndSizeRef = useRef<RndPositionAndSize>({
x: 0,
@ -32,6 +32,8 @@ export default function QuizPreview() {
});
const isFirstShowRef = useRef<boolean>(true);
if (!quiz) return null;
useLayoutEffect(
function stickPreviewToBottomRight() {
const rnd = rndRef.current;
@ -68,8 +70,8 @@ export default function QuizPreview() {
data-cy="quiz-preview-container"
sx={{
position: "fixed",
top: NAVBAR_HEIGHT + DRAG_PARENT_MARGIN,
left: DRAG_PARENT_MARGIN,
top: 0,
left: 0,
bottom: 70,
right: 70,
// backgroundColor: "rgba(0, 100, 0, 0.2)",
@ -119,10 +121,31 @@ export default function QuizPreview() {
overflow: "hidden",
pointerEvents: "auto",
boxShadow: "0px 5px 10px 2px rgba(34, 60, 80, 0.2)",
backgroundColor: "white",
}}
cancel=".cancel"
>
<QuizPreviewLayout />
<QuizAnswerer
className="quiz-preview-draghandle"
quizSettings={{
cnt: questions.length,
questions,
recentlyCompleted: false,
settings: {
fp: quiz.fingerprinting,
delay: 0,
due: quiz.due_to,
lim: quiz.limit,
name: quiz.name,
pausable: quiz.pausable,
rep: quiz.repeatable,
cfg: quiz.config,
},
show_badge: true,
}}
quizId={quiz.qid}
preview
/>
</Rnd>
)}
</Box>

@ -1,298 +0,0 @@
import {
Box,
Button,
LinearProgress,
Paper,
Typography,
FormControl,
Select as MuiSelect,
MenuItem,
useTheme,
} from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import {
decrementCurrentQuestionIndex,
incrementCurrentQuestionIndex,
useQuizPreviewStore,
setCurrentQuestionIndex,
} from "@root/quizPreview";
import {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "model/questionTypes/shared";
import { useEffect, useRef, useState } from "react";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Date from "./QuizPreviewQuestionTypes/Date";
import Emoji from "./QuizPreviewQuestionTypes/Emoji";
import File from "./QuizPreviewQuestionTypes/File";
import Images from "./QuizPreviewQuestionTypes/Images";
import Number from "./QuizPreviewQuestionTypes/Number";
import Page from "./QuizPreviewQuestionTypes/Page";
import Rating from "./QuizPreviewQuestionTypes/Rating";
import Select, { ArrowDownTheme } from "./QuizPreviewQuestionTypes/Select";
import Text from "./QuizPreviewQuestionTypes/Text";
import Variant from "./QuizPreviewQuestionTypes/Variant";
import Varimg from "./QuizPreviewQuestionTypes/Varimg";
import { notReachable } from "../../utils/notReachable";
import ArrowDownIcon from "@icons/ArrowDownIcon";
export default function QuizPreviewLayout() {
const theme = useTheme();
const questions = useQuestionsStore((state) => state.questions);
const currentQuizStep = useQuizPreviewStore(
(state) => state.currentQuestionIndex,
);
const [widthPreview, setWidthPreview] = useState(null);
const nonDeletedQuizQuestions = questions.filter(
(question) => !question.deleted && question.type !== "result",
);
const maxCurrentQuizStep =
nonDeletedQuizQuestions.length > 0 ? nonDeletedQuizQuestions.length - 1 : 0;
const currentProgress = Math.floor(
(currentQuizStep / maxCurrentQuizStep) * 100,
);
const PreviewWin = useRef(0);
const currentQuestion = nonDeletedQuizQuestions[currentQuizStep];
useEffect(
function resetCurrentQuizStep() {
if (currentQuizStep > maxCurrentQuizStep) {
decrementCurrentQuestionIndex();
}
},
[currentQuizStep, maxCurrentQuizStep],
);
const observer = useRef(
new ResizeObserver((entries) => {
const { width } = entries[0].contentRect;
setWidthPreview(width);
}),
);
useEffect(() => {
observer.current.observe(PreviewWin.current);
}, [PreviewWin, observer]);
return (
<Paper
ref={PreviewWin}
className="quiz-preview-draghandle"
data-cy="quiz-preview-layout"
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
flexGrow: 1,
borderRadius: "12px",
pointerEvents: "auto",
backgroundColor: theme.palette.background.default,
}}
>
<Box
sx={{
p: "40px 20px 20px",
whiteSpace: "break-spaces",
overflowY: "auto",
flexGrow: 1,
"&::-webkit-scrollbar": { width: 0, display: "none" },
msOverflowStyle: "none",
scrollbarWidth: "none",
}}
>
<QuestionPreviewComponent
question={currentQuestion}
widthPreview={widthPreview}
/>
</Box>
<Box
sx={{
mt: "auto",
p: "16px",
borderTop: "1px solid #E3E3E3",
}}
>
<Box sx={{ marginBottom: "10px" }}>
<FormControl
fullWidth
size="small"
sx={{ width: "100%", minWidth: "200px", height: "48px" }}
className="cancel"
>
<MuiSelect
id="category-select"
variant="outlined"
value={currentQuizStep}
placeholder="Заголовок вопроса"
onChange={({ target }) =>
setCurrentQuestionIndex(window.Number(target.value))
}
sx={{
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${theme.palette.primary.main} !important`,
},
"& .MuiSelect-icon": {
color: theme.palette.primary.main,
},
}}
MenuProps={{
PaperProps: {
sx: {
mt: "8px",
p: "4px",
borderRadius: "8px",
border: "1px solid #EEE4FC",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
backgroundColor: theme.palette.background.default,
},
},
MenuListProps: {
sx: {
py: 0,
display: "flex",
flexDirection: "column",
gap: "8px",
maxWidth: "330px",
"& .Mui-selected": {
backgroundColor: theme.palette.background.default,
color: theme.palette.primary.main,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.primary.main,
display: "block",
px: "9px",
gap: "20px",
width: "87%",
overflow: "hidden",
textOverflow: "ellipsis",
},
}}
IconComponent={ArrowDownTheme}
>
{Object.values(questions.filter((q) => q.type !== "result")).map(
({ id, title }, index) => (
<MenuItem
key={id}
value={index}
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
p: "4px",
borderRadius: "5px",
color: "#9A9AAF",
wordBreak: "break-word",
whiteSpace: "normal",
}}
>
{`${index + 1}. ${title}`}
</MenuItem>
),
)}
</MuiSelect>
</FormControl>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography>
{nonDeletedQuizQuestions.length > 0
? `Вопрос ${currentQuizStep + 1} из ${
nonDeletedQuizQuestions.length
}`
: "Нет вопросов"}
</Typography>
{nonDeletedQuizQuestions.length > 0 && (
<LinearProgress
variant="determinate"
value={currentProgress}
sx={{
"&.MuiLinearProgress-colorPrimary": {
backgroundColor: "fadePurple.main",
},
"& .MuiLinearProgress-barColorPrimary": {
backgroundColor: "brightPurple.main",
},
}}
/>
)}
</Box>
<Box
sx={{
ml: 2,
display: "flex",
gap: 1,
}}
>
<Button
variant="outlined"
onClick={decrementCurrentQuestionIndex}
disabled={currentQuizStep === 0}
sx={{ px: 1, minWidth: 0 }}
className="cancel"
>
<ArrowLeft color={theme.palette.primary.main} />
</Button>
<Button
variant="contained"
onClick={() => incrementCurrentQuestionIndex(maxCurrentQuizStep)}
disabled={currentQuizStep >= maxCurrentQuizStep}
className="cancel"
>
Далее
</Button>
</Box>
</Box>
</Box>
</Paper>
);
}
function QuestionPreviewComponent({
question,
widthPreview,
}: {
question: AnyTypedQuizQuestion | UntypedQuizQuestion | undefined;
widthPreview?: number;
}) {
if (!question || question.type === null) return null;
switch (question.type) {
case "variant":
return <Variant question={question} widthPreview={widthPreview} />;
case "images":
return <Images question={question} widthPreview={widthPreview} />;
case "varimg":
return <Varimg question={question} widthPreview={widthPreview} />;
case "emoji":
return <Emoji question={question} widthPreview={widthPreview} />;
case "text":
return <Text question={question} widthPreview={widthPreview} />;
case "select":
return <Select question={question} widthPreview={widthPreview} />;
case "date":
return <Date question={question} widthPreview={widthPreview} />;
case "number":
return <Number question={question} widthPreview={widthPreview} />;
case "file":
return <File question={question} widthPreview={widthPreview} />;
case "page":
return <Page question={question} widthPreview={widthPreview} />;
case "rating":
return <Rating question={question} widthPreview={widthPreview} />;
default:
notReachable(question);
}
}

@ -1,46 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import LabeledDatePicker from "@ui_kit/LabeledDatePicker";
import type { QuizQuestionDate } from "model/questionTypes/date";
import { modes } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
question: QuizQuestionDate;
}
export default function Date({ question }: Props) {
const theme = useTheme();
const mode = modes;
const quiz = useCurrentQuiz();
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<LabeledDatePicker
sxIcon={{
"& path": { stroke: theme.palette.primary.main },
"& rect": { stroke: theme.palette.primary.main },
}}
sxDate={{
"& .MuiInputBase-root": {
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
borderRadius: "10px",
maxWidth: "250px",
pr: "22px",
},
}}
className="cancel"
/>
</Box>
);
}

@ -1,141 +0,0 @@
import { useState, ChangeEvent } from "react";
import {
Box,
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuizQuestionEmoji } from "model/questionTypes/emoji";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
interface Props {
question: QuizQuestionEmoji;
}
export default function Emoji({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const theme = useTheme();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl fullWidth className="cancel">
<FormLabel
id="quiz-question-radio-group"
data-cy="question-title"
sx={{ fontSize: "24px", fontWeight: 500, marginBottom: "25px" }}
>
{question.title}
</FormLabel>
<RadioGroup
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
sx={{ display: "flex", flexDirection: "row", gap: "42px" }}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<Box
key={index}
sx={{
borderRadius: "12px",
border: `1px solid`,
borderColor:
value === variant.answer
? theme.palette.primary.main
: "#9A9AAF",
overflow: "hidden",
maxWidth: "317px",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
height: "193px",
background: "#ffffff",
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{variant.extendedText && (
<Typography
fontSize={"100px"}
>{`${variant.extendedText}`}</Typography>
)}
</Box>
</Box>
<FormControlLabel
className="cancel"
key={index}
value={variant.answer}
sx={{
margin: 0,
padding: "15px",
color: theme.palette.text.primary,
display: "flex",
gap: "10px",
background: theme.palette.background.default,
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
paddingLeft: "45px",
},
}}
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={
<Box
sx={{ display: "flex", alignItems: "center", gap: 2 }}
data-cy="variant-answer"
>
<Typography
sx={{ wordBreak: "break-word" }}
>{`${variant.answer}`}</Typography>
{variant.hints && (
<Tooltip title="Подсказка" placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
</Box>
))}
</RadioGroup>
</FormControl>
);
}

@ -1,75 +0,0 @@
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Box, Button, Typography } from "@mui/material";
import type {
QuizQuestionFile,
UploadFileType,
} from "model/questionTypes/file";
export const UPLOAD_FILE_TYPES_MAP: Record<UploadFileType, string> = {
all: "file",
picture: "image/*",
video: "video/*",
audio: "audio/*",
document:
".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.pdf",
} as const;
interface Props {
question: QuizQuestionFile;
}
export default function File({ question }: Props) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [file, setFile] = useState<File | null>(null);
const [acceptedType, setAcceptedType] = useState<any>(
UPLOAD_FILE_TYPES_MAP.all,
);
useEffect(() => {
setAcceptedType(UPLOAD_FILE_TYPES_MAP[question.content.type]);
}, [question.content.type]);
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
if (!event.target.files?.[0]) return setFile(null);
setFile(event.target.files[0]);
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<Button
variant="contained"
onClick={() => fileInputRef.current?.click()}
className="cancel"
>
Загрузить файл
<input
ref={fileInputRef}
onChange={handleFileChange}
type="file"
accept={acceptedType}
data-cy="file-upload-input"
style={{
display: "none",
}}
className="cancel"
/>
</Button>
{file && (
<Typography data-cy="chosen-file-name">
Вы загрузили: {file.name}
</Typography>
)}
</Box>
);
}

@ -1,129 +0,0 @@
import { useEffect, useState } from "react";
import {
Box,
ButtonBase,
Divider,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuizQuestionImages } from "model/questionTypes/images";
interface Props {
question: QuizQuestionImages;
widthPreview: number;
}
export default function Images({ question, widthPreview }: Props) {
const theme = useTheme();
const [selectedVariants, setSelectedVariants] = useState<number[]>([]);
function handleVariantClick(index: number) {
if (!question.content.multi) return setSelectedVariants([index]);
const newSelectedVariants = [...selectedVariants];
if (newSelectedVariants.includes(index)) {
newSelectedVariants.splice(newSelectedVariants.indexOf(index), 1);
} else {
newSelectedVariants.push(index);
}
setSelectedVariants(newSelectedVariants);
}
useEffect(
function resetSelectedVariants() {
setSelectedVariants([]);
},
[question.content.multi],
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="h6" color={theme.palette.text.primary}>
{question.title}
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))",
gap: "15px",
maxWidth: "1050px",
}}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<ButtonBase
className="cancel"
key={index}
onClick={() => handleVariantClick(index)}
data-cy="variant-button"
data-checked={selectedVariants.includes(index)}
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
borderRadius: "8px",
overflow: "hidden",
border: "1px solid",
borderColor: selectedVariants.includes(index)
? theme.palette.primary.main
: "#E3E3E3",
maxWidth: "300px",
minHeight: "250px",
maxHeight: "340px",
}}
>
{variant.extendedText ? (
<img
src={variant.extendedText}
alt="question variant"
style={{
width: "100%",
display: "block",
objectFit: "scale-down",
flexGrow: 1,
}}
/>
) : (
<Typography p={2}>Картинка отсутствует</Typography>
)}
<Divider sx={{ width: "100%" }} />
<Box
sx={{
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
justifyContent: "space-between",
overflow: "auto",
height: "80px",
gap: 2,
p: 1,
}}
>
<Typography sx={{ wordBreak: "break-word" }}>
{variant.answer}
</Typography>
{variant.hints && (
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
</ButtonBase>
))}
</Box>
</Box>
);
}

@ -1,131 +0,0 @@
import { useLayoutEffect, useState } from "react";
import { Box, Typography, useTheme } from "@mui/material";
import { CustomSlider } from "@ui_kit/CustomSlider";
import type { QuizQuestionNumber } from "model/questionTypes/number";
import CustomTextField from "@ui_kit/CustomTextField";
import { updateAnswer } from "@root/quizView";
import { modes } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
question: QuizQuestionNumber;
}
export default function Number({ question }: Props) {
const [sliderValues, setSliderValues] = useState<number | number[]>(0);
const theme = useTheme();
const mode = modes;
const quiz = useCurrentQuiz();
const start = question.content.start;
const min = parseInt(question.content.range.split("—")[0]);
const max = parseInt(question.content.range.split("—")[1]);
useLayoutEffect(() => {
if (question.content.chooseRange) {
setSliderValues([start, start + (max - start) / 2]);
} else {
setSliderValues(start);
}
}, [max, question.content.chooseRange, start]);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<Box
sx={{
px: 2,
}}
>
<CustomSlider
className="cancel"
value={sliderValues}
onChange={(e, value) => {
setSliderValues(value);
}}
min={min}
max={max}
defaultValue={start}
valueLabelDisplay={"on"}
step={question.content.step}
sx={{
color: theme.palette.primary.main,
"& .MuiSlider-valueLabel": {
background: theme.palette.primary.main,
},
}}
/>
{!question.content.chooseRange && (
<Box sx={{ mt: "30px", maxWidth: "80px" }}>
<CustomTextField
placeholder="0"
value={sliderValues}
sx={{
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
},
}}
/>
</Box>
)}
{question.content.chooseRange && (
<Box
sx={{
mt: "30px",
display: "flex",
gap: "15px",
alignItems: "center",
"& .MuiFormControl-root": { width: "auto" },
}}
>
<CustomTextField
className="cancel"
placeholder="0"
value={sliderValues[0]}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
},
}}
/>
<Typography color={theme.palette.text.primary}>до</Typography>
<CustomTextField
className="cancel"
placeholder="0"
value={sliderValues[1]}
sx={{
maxWidth: "80px",
borderColor: theme.palette.text.primary,
"& .MuiInputBase-input": {
textAlign: "center",
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
},
}}
/>
</Box>
)}
</Box>
</Box>
);
}

@ -1,51 +0,0 @@
import { Box, Typography } from "@mui/material";
import YoutubeEmbedIframe from "@ui_kit/StartPagePreview/YoutubeEmbedIframe";
import type { QuizQuestionPage } from "model/questionTypes/page";
interface Props {
question: QuizQuestionPage;
}
export default function Page({ question }: Props) {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "start",
gap: 1,
}}
>
<Typography
variant="h6"
data-cy="question-title"
sx={{ paddingBottom: "25px" }}
>
{question.title}
</Typography>
<Typography data-cy="question-text" sx={{ paddingBottom: "20px" }}>
{question.content.text}
</Typography>
{question.content.useImage ? (
<img
src={question.content.back}
alt=""
style={{
display: "block",
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
) : (
<YoutubeEmbedIframe
containerSX={{ width: "100%", height: "50vh" }}
videoUrl={question.content.video}
/>
)}
</Box>
);
}

@ -1,121 +0,0 @@
import { FC, useState } from "react";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import FlagIcon from "@icons/questionsPage/FlagIcon";
import StarIconMini from "@icons/questionsPage/StarIconMini";
import HashtagIcon from "@icons/questionsPage/hashtagIcon";
import HeartIcon from "@icons/questionsPage/heartIcon";
import LightbulbIcon from "@icons/questionsPage/lightbulbIcon";
import LikeIcon from "@icons/questionsPage/likeIcon";
import TropfyIcon from "@icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "model/questionTypes/rating";
type RatingIconType =
| "star"
| "trophie"
| "flag"
| "heart"
| "like"
| "bubble"
| "hashtag";
const ratingIconComponentByType: Record<
RatingIconType,
FC<{ color: string }>
> = {
star: StarIconMini,
trophie: TropfyIcon,
flag: FlagIcon,
heart: HeartIcon,
like: LikeIcon,
bubble: LightbulbIcon,
hashtag: HashtagIcon,
};
interface Props {
question: QuizQuestionRating;
}
export default function Rating({ question }: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [selectedRating, setSelectedRating] = useState<number>(0);
const RatingIconComponent =
ratingIconComponentByType[question.content.form as RatingIconType];
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography
variant="h6"
data-cy="question-title"
color={theme.palette.text.primary}
>
{question.title}
</Typography>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
width: "fit-content",
}}
>
<Box
className="cancel"
data-cy="rating"
data-rating={selectedRating}
sx={{
display: "flex",
gap: "25px",
flexWrap: "wrap",
}}
>
{Array.from(
{ length: question.content.steps },
(_, index) => index,
).map((itemNumber) => (
<Box
key={itemNumber}
onClick={() => setSelectedRating(itemNumber + 1)}
data-cy="rating-button"
sx={{
cursor: "pointer",
transform: "scale(1.5)",
":hover": {
transform: "scale(1.7)",
transition: "0.2s",
},
}}
>
<RatingIconComponent
color={
selectedRating > itemNumber
? theme.palette.primary.main
: "#9A9AAF"
}
/>
</Box>
))}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
gap: 2,
}}
>
<Typography>{question.content.ratingNegativeDescription}</Typography>
<Typography>{question.content.ratingPositiveDescription}</Typography>
</Box>
</Box>
</Box>
);
}

@ -1,172 +0,0 @@
import { useState } from "react";
import {
Box,
FormControl,
MenuItem,
Select,
SelectChangeEvent,
Typography,
useTheme,
} from "@mui/material";
import ArrowDownIcon from "@icons/ArrowDownIcon";
import type { QuizQuestionSelect } from "model/questionTypes/select";
interface Props {
question: QuizQuestionSelect;
}
export default function Text({ question }: Props) {
const theme = useTheme();
const [selectValue, setSelectValue] = useState<string>("");
const arrowDown = <ArrowDownIcon color={"currentColor"} />;
function handleChange(event: SelectChangeEvent<string | null>) {
setSelectValue((event.target as HTMLInputElement).value);
}
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<FormControl
className="cancel"
fullWidth
size="small"
sx={{
width: "100%",
minWidth: "200px",
height: "48px",
}}
>
<Select
id="category-select"
variant="outlined"
value={selectValue}
placeholder={question.content.default}
displayEmpty
onChange={handleChange}
data-cy="select"
sx={{
height: "48px",
borderRadius: "8px",
"& .MuiOutlinedInput-notchedOutline": {
border: `1px solid ${theme.palette.primary.main} !important`,
},
"& .MuiSelect-icon": {
color: theme.palette.primary.main,
},
}}
MenuProps={{
PaperProps: {
sx: {
mt: "8px",
p: "4px",
borderRadius: "8px",
border: "1px solid #EEE4FC",
backgroundColor: theme.palette.background.default,
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
},
},
MenuListProps: {
sx: {
py: 0,
display: "flex",
flexDirection: "column",
gap: "8px",
maxWidth: "330px",
"& .Mui-selected": {
backgroundColor: theme.palette.background.default,
color: theme.palette.primary.main,
},
},
},
}}
inputProps={{
sx: {
color: theme.palette.primary.main,
display: "block",
px: "9px",
gap: "20px",
paddingLeft: "10px",
overflow: "hidden",
textOverflow: "ellipsis",
"& + input": !selectValue && {
backgroundColor: theme.palette.background.default,
border: "none",
transform: "translateY(-50%)",
top: "50%",
opacity: 1,
color: "#333",
fontSize: "16px",
overflow: "hidden",
textOverflow: "ellipsis",
paddingLeft: "10px",
width: "88%",
},
},
}}
IconComponent={ArrowDownTheme}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant) => (
<MenuItem
key={variant.answer}
value={variant.answer}
data-cy="select-option"
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
p: "4px",
borderRadius: "5px",
color: "#9A9AAF",
whiteSpace: "normal",
}}
>
{variant.answer}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
);
}
export function ArrowDownTheme(props: any) {
return (
<Box
{...props}
sx={{
top: "25% !important",
height: "24px",
width: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M19.5 9L12 16.5L4.5 9"
stroke={"currentColor"}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);
}

@ -1,29 +0,0 @@
import { Box, Typography } from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import type { QuizQuestionText } from "model/questionTypes/text";
interface Props {
question: QuizQuestionText;
}
export default function Text({ question }: Props) {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="h6" data-cy="question-title">
{question.title}
</Typography>
<CustomTextField
placeholder={question.content.placeholder}
className="cancel"
/>
</Box>
);
}

@ -1,145 +0,0 @@
import { ChangeEvent, useState } from "react";
import {
Box,
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
useRadioGroup,
Tooltip,
Typography,
useTheme,
Checkbox,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuizQuestionVariant } from "model/questionTypes/variant";
import CustomRadio from "@ui_kit/CustomRadio";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { modes } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
import CheckboxIcon from "@icons/Checkbox";
interface Props {
question: QuizQuestionVariant;
}
export default function Variant({ question }: Props) {
const [value, setValue] = useState<string | null>(null);
const theme = useTheme();
const mode = modes;
const quiz = useCurrentQuiz();
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue((event.target as HTMLInputElement).value);
};
return (
<FormControl className="cancel" fullWidth>
<FormLabel
id="quiz-question-radio-group"
data-cy="question-title"
sx={{
color: theme.palette.text.primary,
marginBottom: "20px",
fontSize: "24px",
fontWeight: 500,
}}
>
{question.title}
</FormLabel>
<RadioGroup
className="cancel"
aria-labelledby="quiz-question-radio-group"
value={value}
onChange={handleChange}
sx={{
flexDirection: "row",
gap: "20px",
}}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<FormControlLabel
className="cancel"
key={index}
value={variant.answer}
data-cy="variant-answer"
labelPlacement="start"
sx={{
borderRadius: "12px",
border:
value == variant.answer
? `1px solid ${theme.palette.primary.main}`
: "1px solid #9A9AAF",
padding: "20px",
justifyContent: "space-between",
maxWidth: "685px",
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
width: "100%",
margin: 0,
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "55px",
overflow: "auto",
paddingLeft: "45px",
},
}}
control={
question.content.multi ? (
<Checkbox
checkedIcon={
<CheckboxIcon
checked
color={theme.palette.primary.main}
/>
}
icon={<CheckboxIcon />}
/>
) : (
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
)
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography
color={theme.palette.text.primary}
sx={{
wordBreak: "break-word",
}}
>
{variant.answer}
</Typography>
{variant.hints && (
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
))}
</RadioGroup>
</FormControl>
);
}

@ -1,192 +0,0 @@
import { useState, ChangeEvent } from "react";
import {
Box,
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import InfoIcon from "@icons/InfoIcon";
import type { QuestionVariant } from "model/questionTypes/shared";
import type { QuizQuestionVarImg } from "model/questionTypes/varimg";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { modes } from "@utils/themes/Publication/themePublication";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
question: QuizQuestionVarImg;
widthPreview: number;
}
export default function Varimg({ question, widthPreview }: Props) {
const [selectedVariantIndex, setSelectedVariantIndex] = useState<number>(-1);
const theme = useTheme();
const mode = modes;
const quiz = useCurrentQuiz();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setSelectedVariantIndex(
question.content.variants.findIndex(
(variant) => variant.answer === event.target.value,
),
);
};
const currentVariant: QuestionVariant | undefined =
question.content.variants[selectedVariantIndex];
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: "40px",
}}
>
<FormControl
className="cancel"
sx={{
//maxWidth: "900px",
width: "100%",
gap: "25px",
}}
>
<FormLabel
id="quiz-question-radio-group"
data-cy="question-title"
sx={{
fontSize: "24px",
fontWeight: 500,
color: theme.palette.text.primary,
}}
>
{question.title}
</FormLabel>
<Box
sx={{
display: "flex",
alignItems: "center",
flexDirection: widthPreview < 650 ? "column-reverse" : undefined,
gap: "30px",
}}
>
<RadioGroup
className="cancel"
aria-labelledby="quiz-question-radio-group"
value={currentVariant?.answer ?? ""}
onChange={handleChange}
sx={{
gap: "20px",
display: "flex",
width: "100%",
}}
>
{question.content.variants
.filter(({ answer }) => answer)
.map((variant, index) => (
<FormControlLabel
key={index}
value={variant.answer}
data-cy="variant-answer"
sx={{
margin: 0,
borderRadius: "5px",
padding: "15px",
color: theme.palette.text.primary,
border: `1px solid`,
borderColor:
selectedVariantIndex == index
? theme.palette.primary.main
: "#E3E3E3",
backgroundColor: mode[quiz?.config.theme || "StandardTheme"]
? "white"
: theme.palette.background.default,
display: "flex",
alignItems:
variant.answer.length <= 60 ? "center" : "flex-start",
position: "relative",
height: "80px",
"& .MuiFormControlLabel-label": {
wordBreak: "break-word",
height: variant.answer.length <= 60 ? undefined : "60px",
overflow: "auto",
paddingLeft: "45px",
},
}}
control={
<Radio
inputProps={{
"data-cy": "variant-radio",
}}
checkedIcon={
<RadioCheck color={theme.palette.primary.main} />
}
icon={<RadioIcon />}
/>
}
label={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Typography sx={{ wordBreak: "break-word" }}>
{variant.answer}
</Typography>
{variant.hints && (
<Tooltip title={variant.hints} placement="right">
<Box>
<InfoIcon />
</Box>
</Tooltip>
)}
</Box>
}
/>
))}
</RadioGroup>
<Box
sx={{
border: "1px solid #E3E3E3",
width: widthPreview < 650 ? "300px" : "400px",
height: widthPreview < 650 ? "300px" : "400px",
display: "flex",
justifyContent: "center",
alignItems: "center",
borderRadius: "12px",
marginTop: widthPreview < 650 ? 0 : "50px",
overflow: "hidden",
}}
>
{currentVariant?.extendedText ? (
<img
src={currentVariant.extendedText}
data-cy="variant-image"
alt="question variant"
style={{
width: "100%",
height: "100%",
display: "block",
flexGrow: 1,
objectFit: "cover",
}}
/>
) : (
<Typography p={2}>
{selectedVariantIndex === -1
? widthPreview < 650
? question?.content.replText || "Выберите вариант ниже"
: question?.content.replText || "Выберите вариант"
: "Картинка отсутствует"}
</Typography>
)}
</Box>
</Box>
</FormControl>
</Box>
);
}

@ -13,7 +13,7 @@ const backendErrorMessage: Record<string, string> = {
const unknownErrorMessage = "Что-то пошло не так. Повторите попытку позже";
export function getMessageFromFetchError(error: any): string | null {
if (process.env.NODE_ENV !== "production") console.log(error);
if (process.env.NODE_ENV !== "production") console.error(error);
const message = backendErrorMessage[error.response?.data?.message];
if (message) return message;

@ -0,0 +1,4 @@
export const extractOrder = (title: string) => {
const match = title.match(/\((\d+)\)$/);
return match ? parseInt(match[1]) : 0;
};

@ -36,7 +36,6 @@ export const parseAxiosError = (nativeError: unknown): [string, number?] => {
) {
SEMessage = serverError?.error.toLowerCase() || "";
}
console.log(serverError);
const translatedMessage = translateMessage[SEMessage || ""]?.toLowerCase();
if (translatedMessage !== undefined) SEMessage = translatedMessage;
return [SEMessage || "", serverError.statusCode];

2569
yarn.lock

File diff suppressed because it is too large Load Diff