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"
- 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:
@ -17,7 +14,7 @@ build-app:
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main"
PRODUCTION_BRANCH: "eng"
deploy-to-staging:
extends: .deploy_template
@ -29,11 +26,12 @@ deploy-to-staging:
deploy-to-prod:
extends: .deploy_template
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "eng"
rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
tags:
- front
- 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"
services:
respondent:
container_name: respondent
respondent_en:
container_name: respondent_en
restart: unless-stopped
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent
image: $CI_REGISTRY_IMAGE/eng:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: respondent_en
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 { HelmetProvider } from "react-helmet-async";
import "moment/dist/locale/ru";
moment.locale("ru");
import "moment/dist/locale/en-ca";
moment.locale("en");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
type Props = {

@ -4,15 +4,7 @@ import { FallbackProps } from "react-error-boundary";
type Props = Partial<FallbackProps>;
export const ApologyPage = ({ error }: Props) => {
let message = "Что-то пошло не так";
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 = "Такого квиза не существует";
let message = error?.message ?? error.response?.data ?? "Something went wrong";
return (
<Box

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

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

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

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

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

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

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

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

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

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

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

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

@ -39,7 +39,7 @@ export const UploadedFile = ({ currentQuestion, setIsSending }: UploadedFileProp
return (
<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
sx={{
padding: "5px 5px 5px 16px",

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

@ -204,7 +204,7 @@ export const ImageVariant = ({
pl: "15px",
}}
>
Введите свой ответ
Enter your answer
</Typography>
)}
<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
placeholder="0"
value={reversed ? String(reversedMaxRange) : maxRange}

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

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

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

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

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

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

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

@ -6,44 +6,33 @@ export type ServerError = {
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?] => {
const error = nativeError as AxiosError;
if (error.response?.data && "statusCode" in (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;
return [serverError.message, serverError.statusCode];
}
switch (error.status) {
case 404:
return ["Не найдено.", error.status];
return ["Not found.", error.status];
case 403:
return ["Доступ ограничен.", error.status];
return ["Access is restricted.", error.status];
case 401:
return ["Ошибка авторизации.", error.status];
return ["Authorization error.", error.status];
case 500:
return ["Внутренняя ошибка сервера.", error.status];
return ["Internal Server Error.", error.status];
case 503:
return ["Сервис недоступен.", error.status];
return ["Service unavailable.", error.status];
default:
return ["Неизвестная ошибка сервера."];
return ["Unknown server error."];
}
};

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

@ -16,8 +16,8 @@ export default function QuizBanner({
quizId,
position,
onWidgetClose,
appealText = "Пройти тест",
quizHeaderText = "Заголовок квиза",
appealText = "Take the test",
quizHeaderText = "Quiz Title",
buttonTextColor,
buttonBackgroundColor,
autoShowQuizTime = null,
@ -178,10 +178,16 @@ export default function QuizBanner({
alignItems: "start",
}}
>
<Typography fontSize="24px" lineHeight="120%">
<Typography
fontSize="24px"
lineHeight="120%"
>
{appealText}
</Typography>
<Typography fontSize="44px" lineHeight="120%">
<Typography
fontSize="44px"
lineHeight="120%"
>
{quizHeaderText}
</Typography>
</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
d="M1.00391 0.757812L6.67266 6.42656M1.00391 6.42656L6.67266 0.757812"
stroke="white"

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

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

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