Compare commits

...

7 Commits
main ... eng

Author SHA1 Message Date
77def75671 resolve phone name 2025-02-25 06:23:22 +03:00
c61aa5a5a7 Merge remote-tracking branch 'refs/remotes/origin/eng' into eng 2025-02-24 22:26:30 +03:00
e5610b3346 agree policy 2025-02-24 22:25:14 +03:00
ad3929dd9f debug 2025-02-24 20:32:19 +03:00
0c13ccf313 assetsDir 2025-02-24 20:21:06 +03:00
c97085b394 deploy 2025-02-24 19:21:54 +03:00
63fd61537c hardcode trans 2025-02-24 01:38:33 +03:00
35 changed files with 116 additions and 112 deletions

@ -3,12 +3,9 @@ include:
file: "/templates/docker/build-template.gitlab-ci.yml" file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration" - project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml" file: "/templates/docker/deploy-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/service-discovery.gitlab-ci.yml"
stages: stages:
- build - build
- deploy - deploy
- service-discovery
build-app: build-app:
tags: tags:
@ -17,7 +14,7 @@ build-app:
variables: variables:
DOCKER_BUILD_PATH: "./Dockerfile" DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging" STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main" PRODUCTION_BRANCH: "eng"
deploy-to-staging: deploy-to-staging:
extends: .deploy_template extends: .deploy_template
@ -29,11 +26,12 @@ deploy-to-staging:
deploy-to-prod: deploy-to-prod:
extends: .deploy_template extends: .deploy_template
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "eng"
rules: rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH" - if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
tags: tags:
- front - front
- prod - prod
service-discovery:
extends: .sd_artefacts_template

@ -0,0 +1,13 @@
version: "3"
services:
respen:
container_name: respen
restart: unless-stopped
image: $CI_REGISTRY_IMAGE/eng:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respen
tty: true
networks:
- main_default
networks:
main_default:
external: true

@ -1,9 +1,8 @@
version: "3" version: "3"
services: services:
respondent: respondent_en:
container_name: respondent container_name: respondent_en
restart: unless-stopped restart: unless-stopped
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID image: $CI_REGISTRY_IMAGE/eng:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent hostname: respondent_en
tty: true tty: true

BIN
dist-package.zip Normal file

Binary file not shown.

@ -20,8 +20,8 @@ import { ApologyPage } from "./ViewPublicationPage/ApologyPage";
import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage"; import ViewPublicationPage from "./ViewPublicationPage/ViewPublicationPage";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import "moment/dist/locale/ru"; import "moment/dist/locale/en-ca";
moment.locale("ru"); moment.locale("en");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
type Props = { type Props = {

@ -4,15 +4,7 @@ import { FallbackProps } from "react-error-boundary";
type Props = Partial<FallbackProps>; type Props = Partial<FallbackProps>;
export const ApologyPage = ({ error }: Props) => { export const ApologyPage = ({ error }: Props) => {
let message = "Что-то пошло не так"; let message = error?.message ?? error.response?.data ?? "Something went wrong";
if (error.response?.data === "quiz is inactive") message = "Квиз не активирован";
if (error.message === "No questions found") message = "Нет созданных вопросов";
if (error.message === "Quiz is empty") message = "Квиз пуст";
if (error.message === "Quiz already completed") message = "Вы уже прошли этот опрос";
if (error.message === "No quiz id") message = "Отсутствует id квиза";
if (error.message === "Quiz data is null") message = "Не были переданы параметры квиза";
if (error.response?.data === "Invalid request data") message = "Такого квиза не существует";
return ( return (
<Box <Box

@ -85,7 +85,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
if (email.length > 0) body.email = email; if (email.length > 0) body.email = email;
if (phone.length > 0) body.phone = phone; if (phone.length > 0) body.phone = phone;
if (adress.length > 0) body.address = adress; if (adress.length > 0) body.address = adress;
if (text.length > 0) body.customs = { [FC.text.text || "Фамилия"]: text }; if (text.length > 0) body.customs = { [FC.text.text || "Surname"]: text };
if (Object.keys(body).length > 0) { if (Object.keys(body).length > 0) {
try { try {
@ -99,7 +99,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() })); localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) { } catch (e) {
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("The answer was not counted");
} }
} }
}; };
@ -119,12 +119,12 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const FC = settings.cfg.formContact.fields; const FC = settings.cfg.formContact.fields;
if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) { if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) {
return enqueueSnackbar("введена некорректная почта"); return enqueueSnackbar("Incorrect email entered");
} }
if (fireOnce.current) { if (fireOnce.current) {
if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0) if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0)
return enqueueSnackbar("Пожалуйста, заполните поля"); return enqueueSnackbar("Please fill in the fields");
//почта валидна, хоть одно поле заполнено //почта валидна, хоть одно поле заполнено
setFire(true); setFire(true);
@ -159,7 +159,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
yandexMetrics.contactsFormField("address"); yandexMetrics.contactsFormField("address");
} }
} catch (e) { } catch (e) {
enqueueSnackbar("повторите попытку позже"); enqueueSnackbar("please try again later");
} }
if (settings.cfg.resultInfo.showResultForm === "after") { if (settings.cfg.resultInfo.showResultForm === "after") {
onShowResult(); onShowResult();
@ -281,22 +281,22 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
}} }}
fontSize={"16px"} fontSize={"16px"}
> >
С&ensp; I agree with the&ensp;
<Link <Link
href={"https://shub.pena.digital/ppdd"} href={"https://shub.pena.digital/ppdd"}
target="_blank" target="_blank"
> >
Положением об обработке персональных данных{" "} Regulation on the processing of personal data{" "}
</Link> </Link>
&ensp;и&ensp; &ensp;and the&ensp;
<Link <Link
href={"https://shub.pena.digital/docs/privacy"} href={"https://shub.pena.digital/docs/privacy"}
target="_blank" target="_blank"
> >
{" "} {" "}
Политикой конфиденциальности{" "} Privacy Policy{" "}
</Link> </Link>
&ensp;ознакомлен &ensp;agree
</Typography> </Typography>
</Box> </Box>
@ -315,7 +315,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
}, },
}} }}
> >
{settings.cfg.formContact?.button || "Получить результаты"} {settings.cfg.formContact?.button || "Get results"}
</Button> </Button>
</Box> </Box>
{show_badge && ( {show_badge && (

@ -45,7 +45,7 @@ export const ContactTextBlock: FC<ContactTextBlockProps> = ({ settings }) => {
wordBreak: "break-word", wordBreak: "break-word",
}} }}
> >
{settings.cfg.formContact.title || "Заполните форму, чтобы получить результаты теста"} {settings.cfg.formContact.title || "Fill out the form to receive your test results"}
</Typography> </Typography>
{settings.cfg.formContact.desc && ( {settings.cfg.formContact.desc && (
<Typography <Typography

@ -45,8 +45,8 @@ export const Inputs = ({
<CustomInput <CustomInput
onChange={({ target }) => setName(target.value)} onChange={({ target }) => setName(target.value)}
id={name} id={name}
title={FC["name"].innerText || "Введите имя"} title={FC["name"].innerText || "Enter your name"}
desc={FC["name"].text || "Имя"} desc={FC["name"].text || "Name"}
Icon={NameIcon} Icon={NameIcon}
/> />
); );
@ -56,7 +56,7 @@ export const Inputs = ({
setEmail(target.value.replaceAll(/\s/g, "")); setEmail(target.value.replaceAll(/\s/g, ""));
}} }}
id={email} id={email}
title={FC["email"].innerText || "Введите Email"} title={FC["email"].innerText || "Enter your Email"}
desc={FC["email"].text || "Email"} desc={FC["email"].text || "Email"}
Icon={EmailIcon} Icon={EmailIcon}
type="email" type="email"
@ -70,8 +70,8 @@ export const Inputs = ({
}} }}
value={phone} value={phone}
id={phone} id={phone}
title={FC["phone"].innerText || "Введите номер телефона"} title={FC["phone"].innerText || "Enter your phone number"}
desc={FC["phone"].text || "Номер телефона"} desc={FC["phone"].text || "Phone number"}
Icon={PhoneIcon} Icon={PhoneIcon}
isPhone={true} isPhone={true}
/> />
@ -80,8 +80,8 @@ export const Inputs = ({
<CustomInput <CustomInput
onChange={({ target }) => setText(target.value)} onChange={({ target }) => setText(target.value)}
id={text} id={text}
title={FC["text"].text || "Введите фамилию"} title={FC["text"].text || "Enter your surname"}
desc={FC["text"].innerText || "Фамилия"} desc={FC["text"].innerText || "Surname"}
Icon={TextIcon} Icon={TextIcon}
/> />
); );
@ -89,8 +89,8 @@ export const Inputs = ({
<CustomInput <CustomInput
onChange={({ target }) => setAdress(target.value)} onChange={({ target }) => setAdress(target.value)}
id={adress} id={adress}
title={FC["address"].innerText || "Введите адрес"} title={FC["address"].innerText || "Enter your address"}
desc={FC["address"].text || "Адрес"} desc={FC["address"].text || "Address"}
Icon={AddressIcon} Icon={AddressIcon}
/> />
); );

@ -41,7 +41,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
{stepNumber !== null && ( {stepNumber !== null && (
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Typography sx={{ color: theme.palette.text.primary }}> <Typography sx={{ color: theme.palette.text.primary }}>
Вопрос {stepNumber} из {questionsAmount} Question {stepNumber} of {questionsAmount}
</Typography> </Typography>
<Stepper <Stepper
activeStep={stepNumber} activeStep={stepNumber}

@ -58,7 +58,7 @@ export const PointSystemResultList = () => {
color: theme.palette.text.primary, color: theme.palette.text.primary,
}} }}
> >
{currentQuestion.title || "Вопрос без названия"} {currentQuestion.title || "Question without a title"}
</Typography> </Typography>
</Box> </Box>
<Typography <Typography
@ -81,7 +81,7 @@ export const PointSystemResultList = () => {
color: theme.palette.grey[500], color: theme.palette.grey[500],
}} }}
> >
Ваш ответ: Your answer:
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -135,7 +135,7 @@ const Line = ({ checkTrue, text }: LineProps) => {
color: theme.palette.grey[500], color: theme.palette.grey[500],
}} }}
> >
{text || "не выбрано"} {text || "not selected"}
</Typography> </Typography>
</Box> </Box>
); );

@ -35,7 +35,7 @@ export default function QuestionSelect({ selectedQuestion, setQuestion }: Props)
id="category-select" id="category-select"
variant="outlined" variant="outlined"
value={selectedQuestion.id} value={selectedQuestion.id}
placeholder="Заголовок вопроса" placeholder="Question title"
onChange={({ target }) => { onChange={({ target }) => {
setQuestion(target.value); setQuestion(target.value);
}} }}

@ -55,7 +55,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() })); localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) { } catch (e) {
enqueueSnackbar("Заявка не может быть отправлена"); enqueueSnackbar("The request could not be sent");
} }
} }
if (Boolean(settings.cfg.score)) { if (Boolean(settings.cfg.score)) {
@ -70,7 +70,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const sessions = JSON.parse(localStorage.getItem("sessions") || "{}"); const sessions = JSON.parse(localStorage.getItem("sessions") || "{}");
localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() })); localStorage.setItem("sessions", JSON.stringify({ ...sessions, [quizId]: new Date().getTime() }));
} catch (e) { } catch (e) {
enqueueSnackbar("Количество баллов не может быть отправлено"); enqueueSnackbar("The number of points could not be sent");
} }
} }
})(); })();
@ -154,7 +154,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
wordBreak: "break-word", wordBreak: "break-word",
}} }}
> >
Ваш результат: Your result:
</Typography> </Typography>
</Box> </Box>
<Box <Box
@ -248,7 +248,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
fontWeight: 600, fontWeight: 600,
}} }}
> >
Ваши баллы Your points
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
@ -269,7 +269,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
}, },
}} }}
> >
Посмотреть ответы View answers
</Typography> </Typography>
} }
sx={{ sx={{
@ -339,7 +339,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
height: "50px", height: "50px",
}} }}
> >
{resultQuestion.content.hint.text || "Узнать подробнее"} {resultQuestion.content.hint.text || "More information"}
</Button> </Button>
)} )}
{settings.cfg.resultInfo.showResultForm === "after" && resultQuestion.content.redirect && ( {settings.cfg.resultInfo.showResultForm === "after" && resultQuestion.content.redirect && (
@ -361,7 +361,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
width: "auto", width: "auto",
}} }}
> >
{resultQuestion.content.hint.text || "Перейти на сайт"} {resultQuestion.content.hint.text || "Go to website"}
</Button> </Button>
)} )}
</Box> </Box>

@ -302,7 +302,7 @@ export const StartPageViewPublication = () => {
}} }}
onClick={onQuizStart} onClick={onQuizStart}
> >
{settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Пройти тест"} {settings.cfg.startpage.button.trim() ? settings.cfg.startpage.button : "Take the test"}
</Button> </Button>
</Box> </Box>
</Box> </Box>

@ -74,7 +74,7 @@ export default function ViewPublicationPage() {
textAlign={"center"} textAlign={"center"}
mt="50px" mt="50px"
> >
Вопрос не выбран Question not selected
</Typography> </Typography>
</ThemeProvider> </ThemeProvider>
); );
@ -113,7 +113,7 @@ export default function ViewPublicationPage() {
if (preview) return; if (preview) return;
sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => { sendQuestionAnswer(quizId, currentQuestion, currentAnswer, ownVariants)?.catch((e) => {
enqueueSnackbar("Ошибка при отправке ответа"); enqueueSnackbar("Error sending answer");
console.error("Error sending answer", e); console.error("Error sending answer", e);
}); });
}} }}

@ -50,7 +50,7 @@ export default ({ currentQuestion }: DateProps) => {
}} }}
> >
<Box> <Box>
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>От</span> <span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>From</span>
<DateCalendar <DateCalendar
sx={{ sx={{
"& .MuiInputBase-root": { "& .MuiInputBase-root": {
@ -73,7 +73,7 @@ export default ({ currentQuestion }: DateProps) => {
/> />
</Box> </Box>
<Box> <Box>
<span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>До</span> <span style={{ marginLeft: "25px", color: theme.palette.text.primary }}>To</span>
<DateCalendar <DateCalendar
sx={{ sx={{
"& .MuiInputBase-root": { "& .MuiInputBase-root": {

@ -181,7 +181,7 @@ export const EmojiVariant = ({
pl: "15px", pl: "15px",
}} }}
> >
Введите свой ответ Enter your answer
</Typography> </Typography>
)} )}
<FormControlLabel <FormControlLabel

@ -68,7 +68,7 @@ export const UploadFile = ({ currentQuestion, setModalWarningType, isSending, se
updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0); updateAnswer(currentQuestion.id, `${file.name}|${URL.createObjectURL(file)}`, 0);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
enqueueSnackbar("ответ не был засчитан"); enqueueSnackbar("the answer was not counted");
} }
setIsSending(false); setIsSending(false);

@ -39,7 +39,7 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
return ( return (
<Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}> <Box sx={{ display: "flex", alignItems: "center", gap: "15px" }}>
<Typography color={theme.palette.text.primary}>Вы загрузили:</Typography> <Typography color={theme.palette.text.primary}>You have uploaded:</Typography>
<Box <Box
sx={{ sx={{
padding: "5px 5px 5px 16px", padding: "5px 5px 5px 16px",

@ -105,14 +105,14 @@ const CurrentModal = ({ status }: { status: ModalWarningType }) => {
case null: case null:
return null; return null;
case "errorType": case "errorType":
return <Typography>Выбран некорректный тип файла</Typography>; return <Typography>Incorrect file type selected</Typography>;
case "errorSize": case "errorSize":
return <Typography>Файл слишком большой. Максимальный размер 50 МБ</Typography>; return <Typography>File is too big. Maximum size is 50 MB</Typography>;
default: default:
return ( return (
<> <>
<Typography>Допустимые расширения файлов:</Typography> <Typography>Acceptable file extensions:</Typography>
<Typography>{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography> <Typography>{ACCEPT_SEND_FILE_TYPES_MAP[status].join(" ")}</Typography>
</> </>
); );

@ -204,7 +204,7 @@ export const ImageVariant = ({
pl: "15px", pl: "15px",
}} }}
> >
Введите свой ответ Enter your answer
</Typography> </Typography>
)} )}
<FormControlLabel <FormControlLabel

@ -368,7 +368,7 @@ export const Number = ({ currentQuestion }: NumberProps) => {
}, },
}} }}
/> />
<Typography color={theme.palette.text.primary}>до</Typography> <Typography color={theme.palette.text.primary}>to</Typography>
<CustomTextField <CustomTextField
placeholder="0" placeholder="0"
value={reversed ? String(reversedMaxRange) : maxRange} value={reversed ? String(reversedMaxRange) : maxRange}

@ -195,7 +195,7 @@ export const VariantItem = ({
top: "-23px", top: "-23px",
}} }}
> >
Введите свой ответ Enter your answer
</Typography> </Typography>
<OwnInput <OwnInput
questionId={questionId} questionId={questionId}

@ -127,7 +127,7 @@ export const VarimgVariant = ({
pl: "15px", pl: "15px",
}} }}
> >
Введите свой ответ Enter your answer
</Typography> </Typography>
<FormControlLabel <FormControlLabel

@ -156,9 +156,9 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => {
) : currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0 ? ( ) : currentQuestion.content.replText !== " " && currentQuestion.content.replText.length > 0 ? (
currentQuestion.content.replText currentQuestion.content.replText
) : variant?.extendedText || isMobile ? ( ) : variant?.extendedText || isMobile ? (
"Выберите вариант ответа ниже" "Select an answer option below"
) : ( ) : (
"Выберите вариант ответа слева" "Select an answer option on the left"
)} )}
</Box> </Box>
</Box> </Box>

@ -23,7 +23,7 @@ export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }:
}} }}
onClick={moveToNextQuestion} onClick={moveToNextQuestion}
> >
Далее Next
</Button> </Button>
); );
} }

@ -35,7 +35,7 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
}} }}
onClick={moveToPrevQuestion} onClick={moveToPrevQuestion}
> >
{isMobileMini ? "←" : "← Назад"} {isMobileMini ? "←" : "← Back"}
</Button> </Button>
); );
} }

@ -4,15 +4,15 @@ export const MAX_FILE_SIZE = 419430400;
export const UPLOAD_FILE_DESCRIPTIONS_MAP = { export const UPLOAD_FILE_DESCRIPTIONS_MAP = {
picture: { picture: {
title: "Добавить изображение", title: "Add image",
description: "Принимает изображения", description: "Accepts images",
}, },
video: { video: {
title: "Добавить видео", title: "Add video",
description: "Принимает .mp4 и .mov формат — максимум 50мб", description: "Accepts .mp4 and .mov format - maximum 50mb",
}, },
audio: { title: "Добавить аудиофайл", description: "Принимает аудиофайлы" }, audio: { title: "Add audio file", description: "Accepts audio files" },
document: { title: "Добавить документ", description: "Принимает документы" }, document: { title: "Add document", description: "Accepts documents" },
} as const satisfies Record<UploadFileType, { title: string; description: string }>; } as const satisfies Record<UploadFileType, { title: string; description: string }>;
export const ACCEPT_SEND_FILE_TYPES_MAP = { export const ACCEPT_SEND_FILE_TYPES_MAP = {

@ -1,10 +1,10 @@
import type { QuizQuestionBase, QuestionHint, QuestionBranchingRule } from "./shared"; import type { QuizQuestionBase, QuestionHint, QuestionBranchingRule } from "./shared";
export const UPLOAD_FILE_TYPES_MAP = { export const UPLOAD_FILE_TYPES_MAP = {
picture: "Изображения", picture: "Image",
video: "Видео", video: "Video",
audio: "Аудио", audio: "Audio",
document: "Документ", document: "Document",
} as const; } as const;
export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP; export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP;

@ -6,44 +6,33 @@ export type ServerError = {
message: string; message: string;
}; };
const translateMessage: Record<string, string> = {
"user not found": "Пользователь не найден",
"invalid password": "Неправильный пароль",
"field <password> is empty": 'Поле "Пароль" не заполнено',
"field <login> is empty": 'Поле "Логин" не заполнено',
"field <email> is empty": 'Поле "E-mail" не заполнено',
"field <phoneNumber> is empty": 'Поле "Номер телефона" не заполнено',
"user with this email or login is exist": "Пользователь уже существует",
"user with this login is exist": "Пользователь с таким логином уже существует",
};
export const parseAxiosError = (nativeError: unknown): [string, number?] => { export const parseAxiosError = (nativeError: unknown): [string, number?] => {
const error = nativeError as AxiosError; const error = nativeError as AxiosError;
if (error.response?.data && "statusCode" in (error.response.data as ServerError)) { if (error.response?.data && "statusCode" in (error.response.data as ServerError)) {
const serverError = error.response.data as ServerError; const serverError = error.response.data as ServerError;
const translatedMessage = translateMessage[serverError.message]; const translatedMessage = serverError.message;
if (translatedMessage !== undefined) serverError.message = translatedMessage; if (translatedMessage !== undefined) serverError.message = translatedMessage;
return [serverError.message, serverError.statusCode]; return [serverError.message, serverError.statusCode];
} }
switch (error.status) { switch (error.status) {
case 404: case 404:
return ["Не найдено.", error.status]; return ["Not found.", error.status];
case 403: case 403:
return ["Доступ ограничен.", error.status]; return ["Access is restricted.", error.status];
case 401: case 401:
return ["Ошибка авторизации.", error.status]; return ["Authorization error.", error.status];
case 500: case 500:
return ["Внутренняя ошибка сервера.", error.status]; return ["Internal Server Error.", error.status];
case 503: case 503:
return ["Сервис недоступен.", error.status]; return ["Service unavailable.", error.status];
default: default:
return ["Неизвестная ошибка сервера."]; return ["Unknown server error."];
} }
}; };

@ -5,14 +5,14 @@ import { StrictMode, lazy } from "react";
const routes: RouteObject[] = [ const routes: RouteObject[] = [
{ {
path: "/", path: "/en/",
children: [ children: [
{ {
index: true, index: true,
element: <App />, element: <App />,
}, },
{ {
path: ":quizId", path: "/en/:quizId",
element: <App />, element: <App />,
}, },
], ],

@ -16,8 +16,8 @@ export default function QuizBanner({
quizId, quizId,
position, position,
onWidgetClose, onWidgetClose,
appealText = "Пройти тест", appealText = "Take the test",
quizHeaderText = "Заголовок квиза", quizHeaderText = "Quiz Title",
buttonTextColor, buttonTextColor,
buttonBackgroundColor, buttonBackgroundColor,
autoShowQuizTime = null, autoShowQuizTime = null,
@ -178,10 +178,16 @@ export default function QuizBanner({
alignItems: "start", alignItems: "start",
}} }}
> >
<Typography fontSize="24px" lineHeight="120%"> <Typography
fontSize="24px"
lineHeight="120%"
>
{appealText} {appealText}
</Typography> </Typography>
<Typography fontSize="44px" lineHeight="120%"> <Typography
fontSize="44px"
lineHeight="120%"
>
{quizHeaderText} {quizHeaderText}
</Typography> </Typography>
</Box> </Box>
@ -202,7 +208,11 @@ export default function QuizBanner({
}, },
}} }}
> >
<svg viewBox="0 0 7 7" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
viewBox="0 0 7 7"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M1.00391 0.757812L6.67266 6.42656M1.00391 6.42656L6.67266 0.757812" d="M1.00391 0.757812L6.67266 6.42656M1.00391 6.42656L6.67266 0.757812"
stroke="white" stroke="white"

@ -17,7 +17,7 @@ export default function OpenQuizButton({
buttonFlash = false, buttonFlash = false,
withShadow = false, withShadow = false,
rounded = false, rounded = false,
buttonText = "Пройти квиз", buttonText = "Take the quiz",
buttonTextColor, buttonTextColor,
buttonBackgroundColor, buttonBackgroundColor,
fullScreen = false, fullScreen = false,

@ -120,7 +120,7 @@ export default function QuizSideButton({
}, },
]} ]}
> >
{buttonText || "Пройти квиз"} {buttonText || "Take the quiz"}
{showButtonFlash && <RunningStripe />} {showButtonFlash && <RunningStripe />}
</Button> </Button>
</Fade> </Fade>

@ -16,6 +16,9 @@ export const alias = {
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
build: {
assetsDir: "en/assets/",
},
resolve: { resolve: {
alias, alias,
}, },