Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
a532eecdca | |||
326e2c98b3 | |||
b58042554f | |||
147b776550 | |||
0c9b6e5b7a | |||
785e56f9b0 | |||
f330c5c05a | |||
b1c3ab7314 | |||
3baaef83fe | |||
9e301e694f | |||
f13df84fec | |||
4e2e591cd4 | |||
670d2bcb3f | |||
c8ebf9cff0 | |||
656ba9473c | |||
17bd5cd53f | |||
b495974dba | |||
87f78ff8cf | |||
c5e931702b | |||
106d8b6ad7 | |||
32e5853ee8 | |||
bcf2ca0842 |
@ -2,23 +2,29 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
registry_package:
|
||||
types: [published]
|
||||
#package_name: "gitea.pena/squiz/frontanswerer/main:latest"
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
runs-on: [skeris]
|
||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
with:
|
||||
runner: skeris
|
||||
secrets:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
# CreateImage:
|
||||
# runs-on: [skeris]
|
||||
# uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: skeris
|
||||
# secrets:
|
||||
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'main')
|
||||
runs-on: [frontprod]
|
||||
needs: CreateImage
|
||||
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||
with:
|
||||
runner: hubprod
|
||||
actionid: ${{ gitea.run_id }}
|
||||
container:
|
||||
image: gitea.pena/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/main/docker-compose.yaml up -d
|
||||
|
@ -2,23 +2,28 @@ name: Deploy
|
||||
run-name: ${{ gitea.actor }} build image and push to container registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "staging"
|
||||
registry_package:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
CreateImage:
|
||||
runs-on: [skeris]
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
with:
|
||||
runner: hubstaging
|
||||
secrets:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
# CreateImage:
|
||||
# runs-on: [skeris]
|
||||
# uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
|
||||
# with:
|
||||
# runner: hubstaging
|
||||
# secrets:
|
||||
# REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
# REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
DeployService:
|
||||
if: contains(github.event.package.name, 'staging')
|
||||
runs-on: [frontstaging]
|
||||
needs: CreateImage
|
||||
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7
|
||||
with:
|
||||
runner: frontstaging
|
||||
actionid: ${{ gitea.run_id }}
|
||||
container:
|
||||
image: gitea.pena:3000/penadevops/container-images/node-compose:main
|
||||
env:
|
||||
GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
|
||||
volumes:
|
||||
- /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: http://gitea.pena:3000/PenaDevops/actions.git/checkout@v1
|
||||
- run: compose -f deployments/staging/docker-compose.yaml up -d
|
||||
|
@ -2,6 +2,7 @@ services:
|
||||
respondent:
|
||||
container_name: respondent
|
||||
restart: unless-stopped
|
||||
image: gitea.pena/squiz/frontanswerer/main:$GITHUB_RUN_NUMBER
|
||||
image: gitea.pena/squiz/frontanswerer/main:latest
|
||||
hostname: respondent
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
@ -2,9 +2,7 @@ services:
|
||||
respondent:
|
||||
container_name: respondent
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
com.pena.domains: s.hbpn.link
|
||||
com.pena.front_headers: "Access-Control-Allow-Origin $$http_origin always"
|
||||
image: gitea.pena:3000/squiz/frontanswerer/staging:$GITHUB_RUN_NUMBER
|
||||
image: gitea.pena/squiz/frontanswerer/staging:latest
|
||||
hostname: respondent
|
||||
tty: true
|
||||
pull_policy: always
|
||||
|
@ -32,19 +32,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
needConfig: true,
|
||||
});
|
||||
//firstData.settings.status = "ai";
|
||||
console.log("useQuizData: firstData received:", firstData);
|
||||
console.log("useQuizData: firstData.settings:", firstData.settings);
|
||||
|
||||
initDataManager({
|
||||
status: firstData.settings.status,
|
||||
haveRoot: firstData.settings.cfg.haveRoot,
|
||||
});
|
||||
console.log("useQuizData: calling setQuizData with firstData");
|
||||
setQuizData(firstData);
|
||||
|
||||
// Определяем нужно ли загружать все данные
|
||||
console.log("Определяем нужно ли загружать все данные");
|
||||
console.log(firstData.settings.status);
|
||||
if (!["ai"].includes(firstData.settings.status)) {
|
||||
setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа
|
||||
return firstData;
|
||||
@ -62,7 +56,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
});
|
||||
|
||||
addQuestions(data.questions.slice(1));
|
||||
return data;
|
||||
|
||||
// Возвращаем полную структуру данных с настройками из store
|
||||
const currentState = useQuizStore.getState();
|
||||
return {
|
||||
...currentState,
|
||||
questions: [...currentState.questions, ...data.questions.slice(1)],
|
||||
};
|
||||
}
|
||||
|
||||
if (currentPage >= questions.length) {
|
||||
@ -74,15 +74,16 @@ export function useQuizData(quizId: string, preview: boolean = false) {
|
||||
limit: 1,
|
||||
needConfig: false,
|
||||
});
|
||||
console.log(
|
||||
"AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE AI RESPONSE "
|
||||
);
|
||||
console.log(data);
|
||||
addQuestions(data.questions);
|
||||
changeNextLoading(false);
|
||||
return data;
|
||||
|
||||
// Возвращаем полную структуру данных с настройками из store
|
||||
const currentState = useQuizStore.getState();
|
||||
return {
|
||||
...currentState,
|
||||
questions: [...currentState.questions, ...data.questions],
|
||||
};
|
||||
} catch (p) {
|
||||
console.log(p);
|
||||
setPage(questions.length);
|
||||
changeNextLoading(false);
|
||||
}
|
||||
|
@ -168,10 +168,7 @@ export async function getAndParceData(props: GetDataProps) {
|
||||
}
|
||||
|
||||
//Парсим строки в строках
|
||||
console.log("до парса_______________________");
|
||||
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse));
|
||||
console.log("после парса_______________________");
|
||||
console.log(quizSettings);
|
||||
//Единоразово стрингифаим ВСЁ распаршенное и удаляем лишние пробелы
|
||||
const res = JSON.parse(
|
||||
JSON.stringify({ data: quizSettings })
|
||||
|
216
lib/assets/icons/NameplateLogoDark.tsx
Normal file
216
lib/assets/icons/NameplateLogoDark.tsx
Normal file
File diff suppressed because one or more lines are too long
@ -22,6 +22,7 @@ import { HelmetProvider } from "react-helmet-async";
|
||||
import "moment/dist/locale/ru";
|
||||
import { useQuizStore, setQuizData, addquizid } from "@/stores/useQuizStore";
|
||||
import { initDataManager, statusOfQuiz } from "@/utils/hooks/useQuestionFlowControl";
|
||||
|
||||
moment.locale("ru");
|
||||
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
|
||||
|
||||
@ -55,13 +56,6 @@ function QuizAnswererInner({
|
||||
addquizid(quizId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(settings);
|
||||
console.log(questions);
|
||||
console.log("r");
|
||||
console.log(r);
|
||||
}, [questions, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
vkMetrics.quizOpened();
|
||||
@ -72,7 +66,6 @@ function QuizAnswererInner({
|
||||
useEffect(() => {
|
||||
//Хук на случай если данные переданы нам сразу, а не "нам нужно их запросить"
|
||||
if (quizSettings !== undefined) {
|
||||
console.log("QuizAnswerer: calling setQuizData with quizSettings");
|
||||
setQuizData(quizSettings);
|
||||
initDataManager({
|
||||
status: quizSettings.settings.status,
|
||||
@ -98,19 +91,26 @@ function QuizAnswererInner({
|
||||
};
|
||||
}, []);
|
||||
|
||||
console.log("settings");
|
||||
console.log(settings);
|
||||
if (isLoading && !questions.length) return <LoadingSkeleton />;
|
||||
console.log("error");
|
||||
console.log(error);
|
||||
if (error) return <ApologyPage error={error} />;
|
||||
if (isLoading && !questions.length) {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
if (error) {
|
||||
return <ApologyPage error={error} />;
|
||||
}
|
||||
|
||||
if (Object.keys(settings).length == 0) return <ApologyPage error={new Error("quiz data is null")} />;
|
||||
if (questions.length === 0) return <ApologyPage error={new Error("No questions found")} />;
|
||||
if (Object.keys(settings).length == 0) {
|
||||
return <ApologyPage error={new Error("quiz data is null")} />;
|
||||
}
|
||||
if (questions.length === 0) {
|
||||
return <ApologyPage error={new Error("No questions found")} />;
|
||||
}
|
||||
|
||||
if (questions.length === 1 && settings.cfg.noStartPage && statusOfQuiz != "ai")
|
||||
if (questions.length === 1 && settings.cfg.noStartPage && statusOfQuiz != "ai") {
|
||||
return <ApologyPage error={new Error("quiz is empty")} />;
|
||||
if (!quizId) return <ApologyPage error={new Error("no quiz id")} />;
|
||||
}
|
||||
if (!quizId) {
|
||||
return <ApologyPage error={new Error("no quiz id")} />;
|
||||
}
|
||||
|
||||
const quizContainer = (
|
||||
<Box
|
||||
|
@ -6,11 +6,7 @@ type Props = Partial<FallbackProps>;
|
||||
|
||||
export const ApologyPage = ({ error }: Props) => {
|
||||
let message = error.message || error.response?.data || " ";
|
||||
console.log("message");
|
||||
console.log(message.toLowerCase());
|
||||
const { t } = useTranslation();
|
||||
console.log("t");
|
||||
console.log(t(message.toLowerCase()));
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -26,6 +26,7 @@ import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||
import { isProduction } from "@/utils/defineDomain";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
|
||||
|
||||
type Props = {
|
||||
currentQuestion: AnyTypedQuizQuestion;
|
||||
@ -45,6 +46,8 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
const [text, setText] = useState("");
|
||||
const [adress, setAdress] = useState("");
|
||||
const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight);
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [phoneError, setPhoneError] = useState("");
|
||||
|
||||
const fireOnce = useRef(true);
|
||||
const [fire, setFire] = useState(false);
|
||||
@ -120,13 +123,23 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
async function handleShowResultsClick() {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
|
||||
if (!isDisableEmail && FC["email"].used !== EMAIL_REGEXP.test(email)) {
|
||||
// Проверяем email только если поле отображается
|
||||
if (isEmailFieldVisible && !EMAIL_REGEXP.test(email)) {
|
||||
return enqueueSnackbar("Incorrect email entered");
|
||||
}
|
||||
|
||||
if (fireOnce.current) {
|
||||
if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0)
|
||||
// Проверяем, что хотя бы одно видимое поле заполнено
|
||||
const hasVisibleFieldsFilled =
|
||||
(isNameFieldVisible() && name.length > 0) ||
|
||||
(isEmailFieldVisible && email.length > 0) ||
|
||||
(isPhoneFieldVisible() && phone.length > 0) ||
|
||||
(isTextFieldVisible() && text.length > 0) ||
|
||||
(isAddressFieldVisible() && adress.length > 0);
|
||||
|
||||
if (!hasVisibleFieldsFilled) {
|
||||
return enqueueSnackbar(t("Please fill in the fields"));
|
||||
}
|
||||
|
||||
//почта валидна, хоть одно поле заполнено
|
||||
setFire(true);
|
||||
@ -177,6 +190,115 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Функция валидации телефона
|
||||
const validatePhone = (phoneValue: string) => {
|
||||
// Убираем все нецифровые символы и считаем только цифры
|
||||
const digitsOnly = phoneValue.replace(/\D/g, "");
|
||||
|
||||
// Для российских номеров (начинающихся с +7) нужно 11 цифр
|
||||
// Для остальных стран - минимум 10 цифр
|
||||
const isRussianNumber = phoneValue.startsWith("+7");
|
||||
const minDigits = isRussianNumber ? 11 : 10;
|
||||
|
||||
// Если есть какие-то символы в инпуте, но цифр меньше минимума - это ошибка
|
||||
if (phoneValue.trim().length > 0 && digitsOnly.length < minDigits) {
|
||||
return t("Please complete the phone number");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Проверяем валидность телефона при каждом изменении
|
||||
const digitsOnly = phone.replace(/\D/g, "");
|
||||
const isRussianNumber = phone.startsWith("+7");
|
||||
const minDigits = isRussianNumber ? 11 : 10;
|
||||
const isPhoneValid = phone.trim().length === 0 || digitsOnly.length >= minDigits;
|
||||
|
||||
// Проверяем валидность email - должен быть заполнен и соответствовать формату
|
||||
const validateEmail = (emailValue: string) => {
|
||||
if (emailValue.trim().length === 0) return false;
|
||||
|
||||
// Проверяем наличие @ и .
|
||||
const atIndex = emailValue.indexOf("@");
|
||||
const dotIndex = emailValue.lastIndexOf(".");
|
||||
|
||||
if (atIndex === -1 || dotIndex === -1) return false;
|
||||
|
||||
// Точка должна быть после @
|
||||
if (dotIndex <= atIndex) return false;
|
||||
|
||||
// Между @ и . должно быть минимум 3 символа
|
||||
const domainPart = emailValue.substring(atIndex + 1, dotIndex);
|
||||
if (domainPart.length < 3) return false;
|
||||
|
||||
// После точки должно быть минимум 2 символа
|
||||
const tldPart = emailValue.substring(dotIndex + 1);
|
||||
if (tldPart.length < 2) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isEmailValid = validateEmail(email);
|
||||
|
||||
// Определяем, отображается ли поле email
|
||||
const isEmailFieldVisible = settings.cfg.formContact.fields?.email?.used && !isDisableEmail;
|
||||
|
||||
// Функции для определения видимости полей
|
||||
const isNameFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["name"].used : true;
|
||||
};
|
||||
|
||||
const isPhoneFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["phone"].used : true;
|
||||
};
|
||||
|
||||
const isTextFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["text"].used : false;
|
||||
};
|
||||
|
||||
const isAddressFieldVisible = () => {
|
||||
const FC = settings.cfg.formContact.fields;
|
||||
return Object.values(FC).some((data) => data.used) ? FC["address"].used : false;
|
||||
};
|
||||
|
||||
// Обработчик изменения телефона
|
||||
const handlePhoneChange = (newPhone: string) => {
|
||||
setPhone(newPhone);
|
||||
// Очищаем ошибку если поле стало пустым
|
||||
if (newPhone.trim().length === 0) {
|
||||
setPhoneError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик изменения email
|
||||
const handleEmailChange = (newEmail: string) => {
|
||||
setEmail(newEmail);
|
||||
// Очищаем ошибку если поле стало пустым
|
||||
if (newEmail.trim().length === 0) {
|
||||
setEmailError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик потери фокуса для email
|
||||
const handleEmailBlur = () => {
|
||||
if (email.trim().length > 0 && !validateEmail(email)) {
|
||||
setEmailError(t("Please enter a valid email"));
|
||||
} else {
|
||||
setEmailError("");
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик потери фокуса для телефона
|
||||
const handlePhoneBlur = () => {
|
||||
if (phone.trim().length > 0 && !isPhoneValid) {
|
||||
setPhoneError(t("Please enter a valid phone number"));
|
||||
} else {
|
||||
setPhoneError("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -250,13 +372,17 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
setEmail={handleEmailChange}
|
||||
phone={phone}
|
||||
setPhone={setPhone}
|
||||
setPhone={handlePhoneChange}
|
||||
text={text}
|
||||
setText={setText}
|
||||
adress={adress}
|
||||
setAdress={setAdress}
|
||||
emailError={emailError}
|
||||
phoneError={phoneError}
|
||||
onEmailBlur={handleEmailBlur}
|
||||
onPhoneBlur={handlePhoneBlur}
|
||||
crutch={{
|
||||
disableEmail: isDisableEmail,
|
||||
}}
|
||||
@ -304,7 +430,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
disabled={!(ready && !fire)}
|
||||
disabled={!(ready && !fire && isPhoneValid && (isEmailFieldVisible ? isEmailValid : true))}
|
||||
variant="contained"
|
||||
onClick={handleShowResultsClick}
|
||||
sx={{
|
||||
@ -336,12 +462,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
|
||||
margitTop: "auto",
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "20px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF",
|
||||
}}
|
||||
/>
|
||||
{quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -17,6 +17,8 @@ type InputProps = {
|
||||
isPhone?: boolean;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
value?: string;
|
||||
onBlur?: () => void;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||
@ -34,7 +36,18 @@ function phoneChange(e: ChangeEvent<HTMLInputElement>, mask: string) {
|
||||
return a || "";
|
||||
}
|
||||
|
||||
export const CustomInput = ({ title, desc, Icon, onChange, onChangePhone, isPhone, type, value }: InputProps) => {
|
||||
export const CustomInput = ({
|
||||
title,
|
||||
desc,
|
||||
Icon,
|
||||
onChange,
|
||||
onChangePhone,
|
||||
isPhone,
|
||||
type,
|
||||
value,
|
||||
onBlur,
|
||||
error,
|
||||
}: InputProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 600;
|
||||
const { settings } = useQuizStore();
|
||||
@ -57,8 +70,11 @@ export const CustomInput = ({ title, desc, Icon, onChange, onChangePhone, isPhon
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
isPhone ? onChangePhone?.(phoneChange(e, mask)) : onChange?.(e)
|
||||
}
|
||||
onBlur={onBlur}
|
||||
type={isPhone ? "tel" : type}
|
||||
value={value}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
sx={{
|
||||
width: isMobile ? "100%" : "390px",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
|
@ -13,13 +13,17 @@ type InputsProps = {
|
||||
name: string;
|
||||
setName: Dispatch<SetStateAction<string>>;
|
||||
email: string;
|
||||
setEmail: Dispatch<SetStateAction<string>>;
|
||||
setEmail: (email: string) => void;
|
||||
phone: string;
|
||||
setPhone: Dispatch<SetStateAction<string>>;
|
||||
setPhone: (phone: string) => void;
|
||||
text: string;
|
||||
setText: Dispatch<SetStateAction<string>>;
|
||||
adress: string;
|
||||
setAdress: Dispatch<SetStateAction<string>>;
|
||||
emailError?: string;
|
||||
phoneError?: string;
|
||||
onEmailBlur?: () => void;
|
||||
onPhoneBlur?: () => void;
|
||||
crutch: {
|
||||
disableEmail: boolean;
|
||||
};
|
||||
@ -39,6 +43,10 @@ export const Inputs = ({
|
||||
setText,
|
||||
adress,
|
||||
setAdress,
|
||||
emailError,
|
||||
phoneError,
|
||||
onEmailBlur,
|
||||
onPhoneBlur,
|
||||
crutch,
|
||||
}: InputsProps) => {
|
||||
const { settings } = useQuizStore();
|
||||
@ -64,11 +72,13 @@ export const Inputs = ({
|
||||
onChange={({ target }) => {
|
||||
setEmail(target.value.replaceAll(/\s/g, ""));
|
||||
}}
|
||||
onBlur={onEmailBlur}
|
||||
id={email}
|
||||
title={FC["email"].innerText || `${t("Enter")} Email`}
|
||||
desc={FC["email"].text || "Email"}
|
||||
Icon={EmailIcon}
|
||||
type="email"
|
||||
error={emailError}
|
||||
/>
|
||||
);
|
||||
const Phone = (
|
||||
@ -77,12 +87,14 @@ export const Inputs = ({
|
||||
onChangePhone={(phone: string) => {
|
||||
setPhone(phone);
|
||||
}}
|
||||
onBlur={onPhoneBlur}
|
||||
value={phone}
|
||||
id={phone}
|
||||
title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`}
|
||||
desc={FC["phone"].text || t("Phone number")}
|
||||
Icon={PhoneIcon}
|
||||
isPhone={true}
|
||||
error={phoneError}
|
||||
/>
|
||||
);
|
||||
const Text = (
|
||||
|
@ -15,7 +15,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
|
||||
const theme = useTheme();
|
||||
const { questions, settings } = useQuizStore();
|
||||
const questionsAmount = questions.filter(({ type }) => type !== "result").length;
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -6,6 +6,23 @@ import { AnyTypedQuizQuestion, QuizQuestionVariant } from "@/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
|
||||
const dinocrutch = window.location.pathname === "/413b9e24-996a-400e-9076-c158f64b9bd7";
|
||||
|
||||
// Функция для определения вопроса "спасибо"
|
||||
const isThankYouQuestion = (question: QuizQuestionVariant): boolean => {
|
||||
// Проверяем что у вопроса только один вариант ответа
|
||||
if (question.content.variants.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем что текст варианта полностью состоит из слова "спасибо"
|
||||
const variant = question.content.variants[0];
|
||||
const answerText = variant.answer.toLowerCase().trim();
|
||||
|
||||
// Проверяем точное совпадение со словом "спасибо"
|
||||
return answerText === "спасибо";
|
||||
};
|
||||
|
||||
export const PointSystemResultList = () => {
|
||||
const theme = useTheme();
|
||||
const { questions } = useQuizStore();
|
||||
@ -16,7 +33,12 @@ export const PointSystemResultList = () => {
|
||||
(q: AnyTypedQuizQuestion): q is QuizQuestionVariant => q.type === "variant"
|
||||
);
|
||||
|
||||
return questionsWothoutResult.map((currentQuestion) => {
|
||||
// Фильтруем вопросы "спасибо" только для указанного квиза
|
||||
const filteredQuestions = dinocrutch
|
||||
? questionsWothoutResult.filter((q) => !isThankYouQuestion(q))
|
||||
: questionsWothoutResult;
|
||||
|
||||
return filteredQuestions.map((currentQuestion, index) => {
|
||||
let answerIndex = 0;
|
||||
let currentVariants = currentQuestion.content.variants;
|
||||
|
||||
@ -53,7 +75,7 @@ export const PointSystemResultList = () => {
|
||||
color: theme.palette.grey[500],
|
||||
}}
|
||||
>
|
||||
{currentQuestion.page + 1}.
|
||||
{index + 1}.
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
|
@ -12,6 +12,7 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
|
||||
import type { QuizQuestionResult } from "@/model/questionTypes/result";
|
||||
import type { QuizQuestionVariant } from "@/model/questionTypes/variant";
|
||||
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
|
||||
import { TextAccordion } from "./tools/TextAccordion";
|
||||
import { PointSystemResultList } from "./PointSystemResultList";
|
||||
@ -20,11 +21,27 @@ import { sendFC, sendResult } from "@/api/quizRelase";
|
||||
import { isProduction } from "@/utils/defineDomain";
|
||||
import { useQuizStore } from "@/stores/useQuizStore";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
|
||||
|
||||
type ResultFormProps = {
|
||||
resultQuestion: QuizQuestionResult;
|
||||
};
|
||||
|
||||
// Функция для определения вопроса "спасибо"
|
||||
const isThankYouQuestion = (question: QuizQuestionVariant): boolean => {
|
||||
// Проверяем что у вопроса только один вариант ответа
|
||||
if (question.content.variants.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем что текст варианта полностью состоит из слова "спасибо"
|
||||
const variant = question.content.variants[0];
|
||||
const answerText = variant.answer.toLowerCase().trim();
|
||||
|
||||
// Проверяем точное совпадение со словом "спасибо"
|
||||
return answerText === "спасибо";
|
||||
};
|
||||
|
||||
export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useRootContainerSize() < 650;
|
||||
@ -38,6 +55,22 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Проверяем, является ли это квизом с костылем
|
||||
const dinocrutch = window.location.pathname === "/413b9e24-996a-400e-9076-c158f64b9bd7";
|
||||
|
||||
// Вычисляем общее количество вопросов с учетом фильтрации
|
||||
const totalQuestions = useMemo(() => {
|
||||
if (dinocrutch) {
|
||||
// Для квиза с костылем: исключаем вопросы "спасибо" и вопросы типа "result"
|
||||
const variantQuestions = questions.filter((e) => e.type === "variant") as QuizQuestionVariant[];
|
||||
const filteredQuestions = variantQuestions.filter((q) => !isThankYouQuestion(q));
|
||||
return filteredQuestions.length;
|
||||
}
|
||||
|
||||
// Для обычных квизов: исключаем только вопросы типа "result"
|
||||
return questions.filter((e) => e.type !== "result").length;
|
||||
}, [questions, dinocrutch]);
|
||||
|
||||
useEffect(() => {
|
||||
vkMetrics.resultIdShown(resultQuestion.id);
|
||||
yandexMetrics.resultIdShown(resultQuestion.id);
|
||||
@ -259,7 +292,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{pointsSum} {t("of")} {questions.filter((e) => e.type != "result").length}
|
||||
{pointsSum} {t("of")} {totalQuestions}
|
||||
</Typography>
|
||||
<TextAccordion
|
||||
headerText={
|
||||
@ -308,12 +341,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
|
||||
bottom: "90px",
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "23px",
|
||||
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
|
||||
}}
|
||||
/>
|
||||
{quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
|
@ -9,6 +9,7 @@ import { useUADevice } from "@utils/hooks/useUADevice";
|
||||
import { quizThemes } from "@utils/themes/Publication/themePublication";
|
||||
|
||||
import { NameplateLogo } from "@icons/NameplateLogo";
|
||||
import { NameplateLogoDark } from "@icons/NameplateLogoDark";
|
||||
import { useQuizViewStore } from "@/stores/quizView";
|
||||
import { DESIGN_LIST } from "@/utils/designList";
|
||||
|
||||
@ -153,17 +154,13 @@ export const StartPageViewPublication = () => {
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<NameplateLogo
|
||||
style={{
|
||||
fontSize: "23px",
|
||||
color:
|
||||
settings.cfg.startpageType === "expanded"
|
||||
? "#FFFFFF"
|
||||
: quizThemes[settings.cfg.theme].isLight
|
||||
? "#151515"
|
||||
: "#FFFFFF",
|
||||
}}
|
||||
/>
|
||||
{settings.cfg.startpageType === "expanded" ? (
|
||||
<NameplateLogo />
|
||||
) : quizThemes[settings.cfg.theme].isLight ? (
|
||||
<NameplateLogoDark />
|
||||
) : (
|
||||
<NameplateLogo />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
@ -70,7 +70,7 @@ export default function ViewPublicationPage() {
|
||||
if (settings.cfg.antifraud && recentlyCompleted) throw new Error("Quiz already completed");
|
||||
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question";
|
||||
|
||||
if (!currentQuestion)
|
||||
if (!currentQuestion) {
|
||||
return (
|
||||
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}>
|
||||
<Typography
|
||||
@ -81,6 +81,7 @@ export default function ViewPublicationPage() {
|
||||
</Typography>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);
|
||||
|
||||
|
@ -36,10 +36,6 @@ export const Number = ({ currentQuestion }: NumberProps) => {
|
||||
answer ||
|
||||
(reversed ? max + min - currentQuestion.content.start + "—" + max : currentQuestion.content.start + "—" + max);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("reversed:", reversed);
|
||||
}, [reversed]);
|
||||
|
||||
const sendAnswerToBackend = async (value: string, noUpdate = false) => {
|
||||
if (!noUpdate) {
|
||||
updateAnswer(currentQuestion.id, value, 0);
|
||||
|
@ -10,7 +10,7 @@ interface Props {
|
||||
|
||||
export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) {
|
||||
const { settings, nextLoading } = useQuizStore();
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return nextLoading ? (
|
||||
<Skeleton
|
||||
|
@ -13,7 +13,8 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
|
||||
const theme = useTheme();
|
||||
const { settings } = useQuizStore();
|
||||
const isMobileMini = useRootContainerSize() < 382;
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={!isPreviousButtonEnabled}
|
||||
|
@ -27,7 +27,6 @@ export interface GetQuizDataResponse {
|
||||
}
|
||||
|
||||
export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> {
|
||||
console.log(quizDataResponse);
|
||||
const readyData = {
|
||||
cnt: quizDataResponse.cnt,
|
||||
show_badge: quizDataResponse.show_badge,
|
||||
@ -51,7 +50,8 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
|
||||
answerType: "single",
|
||||
onlyNumbers: false,
|
||||
};
|
||||
if (item.c) content.id = Math.floor(Math.random() * 9999999999) + 1;
|
||||
// Убираю замену ID - оставляю оригинальный с бэкенда
|
||||
// if (item.c) content.id = Math.floor(Math.random() * 9999999999) + 1;
|
||||
return {
|
||||
description: item.desc,
|
||||
id: item.id,
|
||||
@ -66,7 +66,6 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
|
||||
readyData.questions = items;
|
||||
|
||||
if (quizDataResponse?.settings !== undefined) {
|
||||
console.log("попытка парсануть сеттингс", quizDataResponse.settings);
|
||||
readyData.settings = {
|
||||
fp: quizDataResponse.settings.fp,
|
||||
rep: quizDataResponse.settings.rep,
|
||||
|
@ -119,6 +119,7 @@ export interface QuizConfig {
|
||||
showfc?: boolean;
|
||||
yandexMetricsNumber?: number;
|
||||
vkMetricsNumber?: number;
|
||||
backBlocked?: boolean;
|
||||
}
|
||||
|
||||
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
|
||||
|
@ -25,22 +25,14 @@ export const useQuizStore = create<QuizStore>(() => ({
|
||||
}));
|
||||
|
||||
export const setQuizData = (data: QuizSettings) => {
|
||||
console.log("setQuizData called with:");
|
||||
console.log("data:", data);
|
||||
console.log("data.settings:", data.settings);
|
||||
console.log("data.questions:", data.questions);
|
||||
|
||||
const currentState = useQuizStore.getState();
|
||||
console.log("Current state before update:", currentState);
|
||||
|
||||
useQuizStore.setState((state: QuizStore) => {
|
||||
const newState = { ...state, ...data };
|
||||
console.log("New state after update:", newState);
|
||||
return newState;
|
||||
});
|
||||
|
||||
const updatedState = useQuizStore.getState();
|
||||
console.log("State after setState:", updatedState);
|
||||
};
|
||||
|
||||
export const addQuestions = (newQuestions: AnyTypedQuizQuestion[]) =>
|
||||
|
@ -13,6 +13,6 @@ const isProduction = !(
|
||||
|
||||
//туризм больше не в исключениях
|
||||
if (!isProduction) domain = "https://s.hbpn.link";
|
||||
domain = "https://hbpn.link";
|
||||
// domain = "https://hbpn.link";
|
||||
|
||||
export { domain, isProduction };
|
||||
|
@ -37,6 +37,6 @@ async function sendErrorsToServer() {
|
||||
// body: errorsQueue,
|
||||
// useToken: true,
|
||||
// });
|
||||
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||
console.info(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
|
||||
errorsQueue = [];
|
||||
}
|
||||
|
@ -14,11 +14,6 @@ export function useAIQuiz() {
|
||||
//Получаем инфо о квизе и список вопросов.
|
||||
const { settings, questions, quizId, cnt, quizStep } = useQuizStore();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useQuestionFlowControl useEffect");
|
||||
console.log(questions);
|
||||
}, [questions]);
|
||||
|
||||
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
|
||||
const answers = useQuizViewStore((state) => state.answers);
|
||||
|
||||
@ -29,9 +24,6 @@ export function useAIQuiz() {
|
||||
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
|
||||
|
||||
const currentQuestion = useMemo(() => {
|
||||
console.log("выбор currentQuestion");
|
||||
console.log("quizStep ", quizStep);
|
||||
console.log("questions[quizStep] ", questions[quizStep]);
|
||||
const calcQuestion = questions[quizStep];
|
||||
if (calcQuestion) {
|
||||
vkMetrics.questionPassed(calcQuestion.id);
|
||||
@ -44,8 +36,6 @@ export function useAIQuiz() {
|
||||
useEffect(() => {
|
||||
if (currentQuestion.type === "result") showResult();
|
||||
if (currentQuestion) changeNextLoading(false);
|
||||
console.log("questions");
|
||||
console.log(questions);
|
||||
}, [currentQuestion, questions]);
|
||||
|
||||
//Показать визуалом юзеру результат
|
||||
@ -86,7 +76,7 @@ export function useAIQuiz() {
|
||||
const setQuestion = useCallback((_: string) => {}, []);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = quizStep > 0;
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : quizStep > 0;
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
|
@ -14,12 +14,6 @@ export function useBranchingQuiz() {
|
||||
//Получаем инфо о квизе и список вопросов.
|
||||
const { settings, questions, quizId, cnt } = useQuizStore();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useQuestionFlowControl useEffect");
|
||||
console.log(questions);
|
||||
}, [questions]);
|
||||
console.log(questions);
|
||||
|
||||
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
|
||||
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
|
||||
const sortedQuestions = useMemo(() => {
|
||||
@ -227,7 +221,7 @@ export function useBranchingQuiz() {
|
||||
);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = Boolean(prevQuestion);
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : Boolean(prevQuestion);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
@ -237,9 +231,6 @@ export function useBranchingQuiz() {
|
||||
return hasAnswer;
|
||||
}
|
||||
|
||||
console.log(linearQuestionIndex);
|
||||
console.log(questions.length);
|
||||
console.log(cnt);
|
||||
if (linearQuestionIndex !== null && questions.length < cnt) return true;
|
||||
return Boolean(nextQuestion);
|
||||
}, [answers, currentQuestion, nextQuestion]);
|
||||
|
@ -14,12 +14,6 @@ export function useLinearQuiz() {
|
||||
//Получаем инфо о квизе и список вопросов.
|
||||
const { settings, questions, quizId, cnt } = useQuizStore();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useQuestionFlowControl useEffect");
|
||||
console.log(questions);
|
||||
}, [questions]);
|
||||
console.log(questions);
|
||||
|
||||
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
|
||||
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
|
||||
const sortedQuestions = useMemo(() => {
|
||||
@ -227,7 +221,7 @@ export function useLinearQuiz() {
|
||||
);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isPreviousButtonEnabled = Boolean(prevQuestion);
|
||||
const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : Boolean(prevQuestion);
|
||||
|
||||
//Анализ дисаблить ли кнопки навигации
|
||||
const isNextButtonEnabled = useMemo(() => {
|
||||
@ -237,9 +231,6 @@ export function useLinearQuiz() {
|
||||
return hasAnswer;
|
||||
}
|
||||
|
||||
console.log(linearQuestionIndex);
|
||||
console.log(questions.length);
|
||||
console.log(cnt);
|
||||
if (linearQuestionIndex !== null && questions.length < cnt) return true;
|
||||
return Boolean(nextQuestion);
|
||||
}, [answers, currentQuestion, nextQuestion]);
|
||||
|
@ -26,6 +26,7 @@
|
||||
"preview": "vite preview",
|
||||
"cypress:open": "cypress open",
|
||||
"prepublishOnly": "npm run build:package",
|
||||
"deploy": "docker login gitea.pena && docker build -t gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest . && docker push gitea.pena/squiz/frontanswerer/$(git branch --show-current):latest",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -56,5 +56,8 @@
|
||||
"Step": "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик"
|
||||
"select emoji": "выберите смайлик",
|
||||
"Please complete the phone number": "Пожалуйста, завершите номер телефона",
|
||||
"Please enter a valid email": "Пожалуйста, введите корректную почту",
|
||||
"Please enter a valid phone number": "Пожалуйста, введите корректный номер телефона"
|
||||
}
|
||||
|
@ -10,11 +10,10 @@ const getLanguageFromURL = (): string => {
|
||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||
|
||||
if (langMatch) {
|
||||
//console.log("Язык из URL:", langMatch[1]);
|
||||
return langMatch[1].toLowerCase();
|
||||
const detectedLang = langMatch[1].toLowerCase();
|
||||
return detectedLang;
|
||||
}
|
||||
|
||||
//console.log('Язык не указан в URL, используем "ru"');
|
||||
return "ru"; // Жёсткий фолбэк
|
||||
};
|
||||
|
||||
@ -33,6 +32,9 @@ i18n
|
||||
backend: {
|
||||
loadPath: "/locales/{{lng}}.json",
|
||||
allowMultiLoading: false,
|
||||
requestOptions: {
|
||||
cache: "no-store",
|
||||
},
|
||||
},
|
||||
react: {
|
||||
useSuspense: false, // Отключаем для совместимости с React 18
|
||||
@ -43,11 +45,11 @@ i18n
|
||||
caches: [], // Не использовать localStorage
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
console.warn("⚠️ Main i18n: Missing translation:", key);
|
||||
return key; // Вернёт ключ вместо ошибки
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
console.error("🚨 Main i18n: Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
@ -55,21 +57,35 @@ i18n
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
//console.log("i18n инициализирован! Текущий язык:", i18n.language);
|
||||
//console.log("Загруженные переводы:", i18n.store.data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Ошибка i18n:", err);
|
||||
console.error("❌ Main i18n: Initialization failed:", err);
|
||||
});
|
||||
|
||||
// 3. Логирование всех событий
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
console.log("Язык изменён на:", lng);
|
||||
console.log("🔄 Main i18n: Language changed to:", lng);
|
||||
});
|
||||
|
||||
i18n.on("initialized", (options) => {
|
||||
console.log("🎯 Main i18n: Initialized event fired with options:", options);
|
||||
});
|
||||
|
||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
||||
console.error(`Ошибка загрузки ${lng}.json:`, msg);
|
||||
console.error(`💥 Main i18n: Failed loading ${lng}.json:`, msg);
|
||||
|
||||
// Если не удалось загрузить русский, пробуем английский
|
||||
if (lng === "ru") {
|
||||
console.log("🔄 Main i18n: Trying to load English as fallback");
|
||||
i18n.changeLanguage("en");
|
||||
}
|
||||
});
|
||||
|
||||
i18n.on("loaded", (loaded) => {
|
||||
console.log("📦 Main i18n: Translations loaded:", loaded);
|
||||
});
|
||||
|
||||
i18n.on("missingKey", (lngs, namespace, key, res) => {
|
||||
console.warn("⚠️ Main i18n: Missing key event:", { lngs, namespace, key, res });
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
@ -5,241 +5,283 @@ import { initReactI18next } from "react-i18next";
|
||||
const getLanguageFromURL = (): string => {
|
||||
const path = window.location.pathname;
|
||||
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
|
||||
return langMatch ? langMatch[1].toLowerCase() : "ru"; // Фолбэк на 'ru'
|
||||
const detectedLang = langMatch ? langMatch[1].toLowerCase() : "ru"; // Фолбэк на 'ru'
|
||||
return detectedLang;
|
||||
};
|
||||
|
||||
// 2. Локали, встроенные прямо в конфиг
|
||||
|
||||
const r = {
|
||||
ru: {
|
||||
"quiz is inactive": "Квиз не активирован",
|
||||
"no questions found": "Нет созданных вопросов",
|
||||
"quiz is empty": "Квиз пуст",
|
||||
"quiz already completed": "Вы уже прошли этот опрос",
|
||||
"no quiz id": "Отсутствует id квиза",
|
||||
"quiz data is null": "Не были переданы параметры квиза",
|
||||
"invalid request data": "Такого квиза не существует",
|
||||
"default message": "Что-то пошло не так",
|
||||
"The request could not be sent": "Заявка не может быть отправлена",
|
||||
"The number of points could not be sent": "Количество баллов не может быть отправлено",
|
||||
"Your result": "Ваш результат",
|
||||
"Your points": "Ваши баллы",
|
||||
of: "из",
|
||||
"View answers": "Посмотреть ответы",
|
||||
"Find out more": "Узнать подробнее",
|
||||
"Go to website": "Перейти на сайт",
|
||||
"Question title": "Заголовок вопроса",
|
||||
"Question without a title": "Вопрос без названия",
|
||||
"Your answer": "Ваш ответ",
|
||||
"Add image": "Добавить изображение",
|
||||
"Accepts images": "Принимает изображения",
|
||||
"Add video": "Добавить видео",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": "Принимает .mp4 и .mov формат — максимум 50мб",
|
||||
"Add audio file": "Добавить аудиофайл",
|
||||
"Accepts audio files": "Принимает аудиофайлы",
|
||||
"Add document": "Добавить документ",
|
||||
"Accepts documents": "Принимает документы",
|
||||
Next: "Далее",
|
||||
Prev: "Назад",
|
||||
From: "От",
|
||||
До: "До",
|
||||
"Enter your answer": "Введите свой ответ",
|
||||
"Incorrect file type selected": "Выбран некорректный тип файла",
|
||||
"File is too big. Maximum size is 50 MB": "Файл слишком большой. Максимальный размер 50 МБ",
|
||||
"Acceptable file extensions": "Допустимые расширения файлов",
|
||||
"You have uploaded": "Вы загрузили",
|
||||
"The answer was not counted": "Ответ не был засчитан",
|
||||
"Select an answer option below": "Выберите вариант ответа ниже",
|
||||
"Select an answer option on the left": "Выберите вариант ответа слева",
|
||||
"Fill out the form to receive your test results": "Заполните форму, чтобы получить результаты теста",
|
||||
Enter: "Введите",
|
||||
Name: "Имя",
|
||||
"Phone number": "Номер телефона",
|
||||
"Last name": "Фамилия",
|
||||
Address: "Адрес",
|
||||
"Incorrect email entered": "Введена некорректная почта",
|
||||
"Please fill in the fields": "Пожалуйста, заполните поля",
|
||||
"Please try again later": "повторите попытку позже",
|
||||
"Regulation on the processing of personal data": "Положением об обработке персональных данных",
|
||||
"Privacy Policy": "Политикой конфиденциальности",
|
||||
familiarized: "ознакомлен",
|
||||
and: "и",
|
||||
"Get results": "Получить результаты",
|
||||
"Data sent successfully": "Данные успешно отправлены",
|
||||
Step: "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик",
|
||||
"": "", // Пустой ключ для fallback
|
||||
translation: {
|
||||
"quiz is inactive": "Квиз не активирован",
|
||||
"no questions found": "Нет созданных вопросов",
|
||||
"quiz is empty": "Квиз пуст",
|
||||
"quiz already completed": "Вы уже прошли этот опрос",
|
||||
"no quiz id": "Отсутствует id квиза",
|
||||
"quiz data is null": "Не были переданы параметры квиза",
|
||||
"invalid request data": "Такого квиза не существует",
|
||||
"default message": "Что-то пошло не так",
|
||||
"The request could not be sent": "Заявка не может быть отправлена",
|
||||
"The number of points could not be sent": "Количество баллов не может быть отправлено",
|
||||
"Your result": "Ваш результат",
|
||||
"Your points": "Ваши баллы",
|
||||
of: "из",
|
||||
"View answers": "Посмотреть ответы",
|
||||
"Find out more": "Узнать подробнее",
|
||||
"Go to website": "Перейти на сайт",
|
||||
"Question title": "Заголовок вопроса",
|
||||
"Question without a title": "Вопрос без названия",
|
||||
"Your answer": "Ваш ответ",
|
||||
"Add image": "Добавить изображение",
|
||||
"Accepts images": "Принимает изображения",
|
||||
"Add video": "Добавить видео",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": "Принимает .mp4 и .mov формат — максимум 50мб",
|
||||
"Add audio file": "Добавить аудиофайл",
|
||||
"Accepts audio files": "Принимает аудиофайлы",
|
||||
"Add document": "Добавить документ",
|
||||
"Accepts documents": "Принимает документы",
|
||||
Next: "Далее",
|
||||
Prev: "Назад",
|
||||
From: "От",
|
||||
До: "До",
|
||||
"Enter your answer": "Введите свой ответ",
|
||||
"Incorrect file type selected": "Выбран некорректный тип файла",
|
||||
"File is too big. Maximum size is 50 MB": "Файл слишком большой. Максимальный размер 50 МБ",
|
||||
"Acceptable file extensions": "Допустимые расширения файлов",
|
||||
"You have uploaded": "Вы загрузили",
|
||||
"The answer was not counted": "Ответ не был засчитан",
|
||||
"Select an answer option below": "Выберите вариант ответа ниже",
|
||||
"Select an answer option on the left": "Выберите вариант ответа слева",
|
||||
"Fill out the form to receive your test results": "Заполните форму, чтобы получить результаты теста",
|
||||
Enter: "Введите",
|
||||
Name: "Имя",
|
||||
"Phone number": "Номер телефона",
|
||||
"Last name": "Фамилия",
|
||||
Address: "Адрес",
|
||||
"Incorrect email entered": "Введена некорректная почта",
|
||||
"Please fill in the fields": "Пожалуйста, заполните поля",
|
||||
"Please try again later": "повторите попытку позже",
|
||||
"Regulation on the processing of personal data": "Положением об обработке персональных данных",
|
||||
"Privacy Policy": "Политикой конфиденциальности",
|
||||
familiarized: "ознакомлен",
|
||||
and: "и",
|
||||
"Get results": "Получить результаты",
|
||||
"Data sent successfully": "Данные успешно отправлены",
|
||||
Step: "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории пока не готовы. Подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик",
|
||||
"Please complete the phone number": "Пожалуйста, заполните номер телефона до конца",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
en: {
|
||||
"quiz is inactive": "Quiz is inactive",
|
||||
"no questions found": "No questions found",
|
||||
"quiz is empty": "Quiz is empty",
|
||||
"quiz already completed": "You've already completed this quiz",
|
||||
"no quiz id": "Missing quiz ID",
|
||||
"quiz data is null": "No quiz parameters were provided",
|
||||
"invalid request data": "This quiz doesn't exist",
|
||||
"default message": "Something went wrong",
|
||||
"The request could not be sent": "Request could not be sent",
|
||||
"The number of points could not be sent": "Points could not be submitted",
|
||||
"Your result": "Your result",
|
||||
"Your points": "Your points",
|
||||
of: "of",
|
||||
"View answers": "View answers",
|
||||
"Find out more": "Learn more",
|
||||
"Go to website": "Go to website",
|
||||
"Question title": "Question title",
|
||||
"Question without a title": "Untitled question",
|
||||
"Your answer": "Your answer",
|
||||
"Add image": "Add image",
|
||||
"Accepts images": "Accepts images",
|
||||
"Add video": "Add video",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": "Accepts .mp4 and .mov format - maximum 50MB",
|
||||
"Add audio file": "Add audio file",
|
||||
"Accepts audio files": "Accepts audio files",
|
||||
"Add document": "Add document",
|
||||
"Accepts documents": "Accepts documents",
|
||||
Next: "Next",
|
||||
Prev: "Previous",
|
||||
From: "From",
|
||||
До: "To",
|
||||
"Enter your answer": "Enter your answer",
|
||||
"Incorrect file type selected": "Invalid file type selected",
|
||||
"File is too big. Maximum size is 50 MB": "File is too large. Maximum size is 50 MB",
|
||||
"Acceptable file extensions": "Allowed file extensions",
|
||||
"You have uploaded": "You've uploaded",
|
||||
"The answer was not counted": "Answer wasn't counted",
|
||||
"Select an answer option below": "Select an answer option below",
|
||||
"Select an answer option on the left": "Select an answer option on the left",
|
||||
"Fill out the form to receive your test results": "Fill out the form to receive your test results",
|
||||
Enter: "Enter",
|
||||
Name: "Name",
|
||||
"Phone number": "Phone number",
|
||||
"Last name": "Last name",
|
||||
Address: "Address",
|
||||
"Incorrect email entered": "Invalid email entered",
|
||||
"Please fill in the fields": "Please fill in the fields",
|
||||
"Please try again later": "Please try again later",
|
||||
"Regulation on the processing of personal data": "Personal Data Processing Regulation",
|
||||
"Privacy Policy": "Privacy Policy",
|
||||
familiarized: "acknowledged",
|
||||
and: "and",
|
||||
"Get results": "Get results",
|
||||
"Data sent successfully": "Data sent successfully",
|
||||
Step: "Step",
|
||||
"questions are not ready yet": "There are no questions for the audience yet. Please wait",
|
||||
"Add your image": "Add your image",
|
||||
"select emoji": "select emoji",
|
||||
"": "", // Пустой ключ для fallback
|
||||
translation: {
|
||||
"quiz is inactive": "Quiz is inactive",
|
||||
"no questions found": "No questions found",
|
||||
"quiz is empty": "Quiz is empty",
|
||||
"quiz already completed": "You've already completed this quiz",
|
||||
"no quiz id": "Missing quiz ID",
|
||||
"quiz data is null": "No quiz parameters were provided",
|
||||
"invalid request data": "This quiz doesn't exist",
|
||||
"default message": "Something went wrong",
|
||||
"The request could not be sent": "Request could not be sent",
|
||||
"The number of points could not be sent": "Points could not be submitted",
|
||||
"Your result": "Your result",
|
||||
"Your points": "Your points",
|
||||
of: "of",
|
||||
"View answers": "View answers",
|
||||
"Find out more": "Learn more",
|
||||
"Go to website": "Go to website",
|
||||
"Question title": "Question title",
|
||||
"Question without a title": "Untitled question",
|
||||
"Your answer": "Your answer",
|
||||
"Add image": "Add image",
|
||||
"Accepts images": "Accepts images",
|
||||
"Add video": "Add video",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": "Accepts .mp4 and .mov format - maximum 50MB",
|
||||
"Add audio file": "Add audio file",
|
||||
"Accepts audio files": "Accepts audio files",
|
||||
"Add document": "Add document",
|
||||
"Accepts documents": "Accepts documents",
|
||||
Next: "Next",
|
||||
Prev: "Previous",
|
||||
From: "From",
|
||||
До: "To",
|
||||
"Enter your answer": "Enter your answer",
|
||||
"Incorrect file type selected": "Invalid file type selected",
|
||||
"File is too big. Maximum size is 50 MB": "File is too large. Maximum size is 50 MB",
|
||||
"Acceptable file extensions": "Allowed file extensions",
|
||||
"You have uploaded": "You've uploaded",
|
||||
"The answer was not counted": "Answer wasn't counted",
|
||||
"Select an answer option below": "Select an answer option below",
|
||||
"Select an answer option on the left": "Select an answer option on the left",
|
||||
"Fill out the form to receive your test results": "Fill out the form to receive your test results",
|
||||
Enter: "Enter",
|
||||
Name: "Name",
|
||||
"Phone number": "Phone number",
|
||||
"Last name": "Last name",
|
||||
Address: "Address",
|
||||
"Incorrect email entered": "Invalid email entered",
|
||||
"Please fill in the fields": "Please fill in the fields",
|
||||
"Please try again later": "Please try again later",
|
||||
"Regulation on the processing of personal data": "Personal Data Processing Regulation",
|
||||
"Privacy Policy": "Privacy Policy",
|
||||
familiarized: "acknowledged",
|
||||
and: "and",
|
||||
"Get results": "Get results",
|
||||
"Data sent successfully": "Data sent successfully",
|
||||
Step: "Step",
|
||||
"questions are not ready yet": "There are no questions for the audience yet. Please wait",
|
||||
"Add your image": "Add your image",
|
||||
"select emoji": "select emoji",
|
||||
"Please complete the phone number": "Please complete the phone number",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
uz: {
|
||||
"quiz is inactive": "Test faol emas",
|
||||
"no questions found": "Savollar topilmadi",
|
||||
"quiz is empty": "Test boʻsh",
|
||||
"quiz already completed": "Siz bu testni allaqachon topshirgansiz",
|
||||
"no quiz id": "Test IDsi yoʻq",
|
||||
"quiz data is null": "Test parametrlari yuborilmagan",
|
||||
"invalid request data": "Bunday test mavjud emas",
|
||||
"default message": "Xatolik yuz berdi",
|
||||
"The request could not be sent": "Soʻrov yuborib boʻlmadi",
|
||||
"The number of points could not be sent": "Ballar yuborib boʻlmadi",
|
||||
"Your result": "Sizning natijangiz",
|
||||
"Your points": "Sizning ballaringiz",
|
||||
of: "/",
|
||||
"View answers": "Javoblarni koʻrish",
|
||||
"Find out more": "Batafsil maʼlumot",
|
||||
"Go to website": "Veb-saytga oʻtish",
|
||||
"Question title": "Savol sarlavhasi",
|
||||
"Question without a title": "Sarlavhasiz savol",
|
||||
"Your answer": "Sizning javobingiz",
|
||||
"Add image": "Rasm qoʻshish",
|
||||
"Accepts images": "Rasmlarni qabul qiladi",
|
||||
"Add video": "Video qoʻshish",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": ".mp4 va .mov formatlarini qabul qiladi - maksimal 50MB",
|
||||
"Add audio file": "Audio fayl qoʻshish",
|
||||
"Accepts audio files": "Audio fayllarni qabul qiladi",
|
||||
"Add document": "Hujjat qoʻshish",
|
||||
"Accepts documents": "Hujjatlarni qabul qiladi",
|
||||
Next: "Keyingi",
|
||||
Prev: "Oldingi",
|
||||
From: "Dan",
|
||||
До: "Gacha",
|
||||
"Enter your answer": "Javobingizni kiriting",
|
||||
"Incorrect file type selected": "Notoʻgʻri fayl turi tanlandi",
|
||||
"File is too big. Maximum size is 50 MB": "Fayl juda katta. Maksimal hajmi 50 MB",
|
||||
"Acceptable file extensions": "Qabul qilinadigan fayl kengaytmalari",
|
||||
"You have uploaded": "Siz yuklagansiz",
|
||||
"The answer was not counted": "Javob hisobga olinmadi",
|
||||
"Select an answer option below": "Quyidagi javob variantlaridan birini tanlang",
|
||||
"Select an answer option on the left": "Chapdagi javob variantlaridan birini tanlang",
|
||||
"Fill out the form to receive your test results": "Test natijalaringizni olish uchun shaklni toʻldiring",
|
||||
Enter: "Kiriting",
|
||||
Name: "Ism",
|
||||
"Phone number": "Telefon raqami",
|
||||
"Last name": "Familiya",
|
||||
Address: "Manzil",
|
||||
"Incorrect email entered": "Notoʻgʻri elektron pochta kiritildi",
|
||||
"Please fill in the fields": "Iltimos, maydonlarni toʻldiring",
|
||||
"Please try again later": "Iltimos, keyinroq urinib koʻring",
|
||||
"Regulation on the processing of personal data": "Shaxsiy maʼlumotlarni qayta ishlash qoidalari",
|
||||
"Privacy Policy": "Maxfiylik siyosati",
|
||||
familiarized: "tanishdim",
|
||||
and: "va",
|
||||
"Get results": "Natijalarni olish",
|
||||
"Data sent successfully": "Ma'lumotlar muvaffaqiyatli yuborildi",
|
||||
Step: "Qadam",
|
||||
"questions are not ready yet": "Tomoshabinlar uchun hozircha savollar yo'q. Iltimos kuting",
|
||||
"Add your image": "Rasmingizni qo'shing",
|
||||
"select emoji": "emoji tanlang",
|
||||
"": "", // Пустой ключ для fallback
|
||||
translation: {
|
||||
"quiz is inactive": "Test faol emas",
|
||||
"no questions found": "Savollar topilmadi",
|
||||
"quiz is empty": "Test boʻsh",
|
||||
"quiz already completed": "Siz bu testni allaqachon topshirgansiz",
|
||||
"no quiz id": "Test IDsi yoʻq",
|
||||
"quiz data is null": "Test parametrlari yuborilmagan",
|
||||
"invalid request data": "Bunday test mavjud emas",
|
||||
"default message": "Xatolik yuz berdi",
|
||||
"The request could not be sent": "Soʻrov yuborib boʻlmadi",
|
||||
"The number of points could not be sent": "Ballar yuborib boʻlmadi",
|
||||
"Your result": "Sizning natijangiz",
|
||||
"Your points": "Sizning ballaringiz",
|
||||
of: "/",
|
||||
"View answers": "Javoblarni koʻrish",
|
||||
"Find out more": "Batafsil maʼlumot",
|
||||
"Go to website": "Veb-saytga oʻtish",
|
||||
"Question title": "Savol sarlavhasi",
|
||||
"Question without a title": "Sarlavhasiz savol",
|
||||
"Your answer": "Sizning javobingiz",
|
||||
"Add image": "Rasm qoʻshish",
|
||||
"Accepts images": "Rasmlarni qabul qiladi",
|
||||
"Add video": "Video qoʻshish",
|
||||
"Accepts .mp4 and .mov format - maximum 50mb": ".mp4 va .mov formatlarini qabul qiladi - maksimal 50MB",
|
||||
"Add audio file": "Audio fayl qoʻshish",
|
||||
"Accepts audio files": "Audio fayllarni qabul qiladi",
|
||||
"Add document": "Hujjat qoʻshish",
|
||||
"Accepts documents": "Hujjatlarni qabul qiladi",
|
||||
Next: "Keyingi",
|
||||
Prev: "Oldingi",
|
||||
From: "Dan",
|
||||
До: "Gacha",
|
||||
"Enter your answer": "Javobingizni kiriting",
|
||||
"Incorrect file type selected": "Notoʻgʻri fayl turi tanlandi",
|
||||
"File is too big. Maximum size is 50 MB": "Fayl juda katta. Maksimal hajmi 50 MB",
|
||||
"Acceptable file extensions": "Qabul qilinadigan fayl kengaytmalari",
|
||||
"You have uploaded": "Siz yuklagansiz",
|
||||
"The answer was not counted": "Javob hisobga olinmadi",
|
||||
"Select an answer option below": "Quyidagi javob variantlaridan birini tanlang",
|
||||
"Select an answer option on the left": "Chapdagi javob variantlaridan birini tanlang",
|
||||
"Fill out the form to receive your test results": "Test natijalaringizni olish uchun shaklni toʻldiring",
|
||||
Enter: "Kiriting",
|
||||
Name: "Ism",
|
||||
"Phone number": "Telefon raqami",
|
||||
"Last name": "Familiya",
|
||||
Address: "Manzil",
|
||||
"Incorrect email entered": "Notoʻgʻri elektron pochta kiritildi",
|
||||
"Please fill in the fields": "Iltimos, maydonlarni toʻldiring",
|
||||
"Please try again later": "Iltimos, keyinroq urinib koʻring",
|
||||
"Regulation on the processing of personal data": "Shaxsiy maʼlumotlarni qayta ishlash qoidalari",
|
||||
"Privacy Policy": "Maxfiylik siyosati",
|
||||
familiarized: "tanishdim",
|
||||
and: "va",
|
||||
"Get results": "Natijalarni olish",
|
||||
"Data sent successfully": "Ma'lumotlar muvaffaqiyatli yuborildi",
|
||||
Step: "Qadam",
|
||||
"questions are not ready yet": "Tomoshabinlar uchun hozircha savollar yo'q. Iltimos kuting",
|
||||
"Add your image": "Rasmingizni qo'shing",
|
||||
"select emoji": "emoji tanlang",
|
||||
"Please complete the phone number": "Iltimos, telefon raqamini to'liq kiriting",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 3. Конфигурация i18n без Backend
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: r, // Используем встроенные переводы
|
||||
lng: getLanguageFromURL(),
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["en", "ru", "uz"],
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
detection: {
|
||||
order: ["path"],
|
||||
lookupFromPathIndex: 0,
|
||||
caches: [],
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
return key;
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
stack: new Error().stack,
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
console.log("i18n initialized. Current language:", i18n.language);
|
||||
console.log("Available languages:", i18n.languages);
|
||||
console.log("Available keys for ru:", Object.keys(r.ru));
|
||||
console.log("Available keys for en:", Object.keys(r.en));
|
||||
console.log("Available keys for uz:", Object.keys(r.uz));
|
||||
// Проверяем, не инициализирован ли уже i18n
|
||||
if (i18n.isInitialized) {
|
||||
// Добавляем ресурсы к существующему экземпляру
|
||||
(Object.keys(r) as Array<"ru" | "en" | "uz">).forEach((lng) => {
|
||||
if (i18n.store.data[lng] && i18n.store.data[lng].translation) {
|
||||
// Объединяем с существующими переводами
|
||||
i18n.store.data[lng].translation = {
|
||||
...(i18n.store.data[lng].translation as Record<string, string>),
|
||||
...r[lng].translation,
|
||||
};
|
||||
} else {
|
||||
// Добавляем новые переводы
|
||||
i18n.store.data[lng] = {
|
||||
...(i18n.store.data[lng] as Record<string, any>),
|
||||
translation: r[lng].translation,
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 3. Конфигурация i18n без Backend
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: r, // Используем встроенные переводы
|
||||
lng: getLanguageFromURL(),
|
||||
fallbackLng: "ru",
|
||||
supportedLngs: ["en", "ru", "uz"],
|
||||
debug: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
detection: {
|
||||
order: ["path"],
|
||||
lookupFromPathIndex: 0,
|
||||
caches: [],
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("⚠️ Widget i18n: Missing translation key:", key);
|
||||
return key;
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Widget i18n: Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
stack: new Error().stack,
|
||||
});
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ Widget i18n: Initialization failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Логирование событий
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
console.log("Language changed to:", lng);
|
||||
console.log("🔄 Widget i18n: Language changed to:", lng);
|
||||
});
|
||||
|
||||
i18n.on("initialized", (options) => {
|
||||
console.log("🎯 Widget i18n: Initialized event fired with options:", options);
|
||||
});
|
||||
|
||||
i18n.on("loaded", (loaded) => {
|
||||
console.log("📦 Widget i18n: Loaded event fired:", loaded);
|
||||
});
|
||||
|
||||
i18n.on("failedLoading", (lng, ns, msg) => {
|
||||
console.error("💥 Widget i18n: Failed loading:", { lng, ns, msg });
|
||||
});
|
||||
|
||||
i18n.on("missingKey", (lngs, namespace, key, res) => {
|
||||
console.warn("⚠️ Widget i18n: Missing key event:", { lngs, namespace, key, res });
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
@ -2,6 +2,7 @@ import { createRoot } from "react-dom/client";
|
||||
import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import { StrictMode, lazy } from "react";
|
||||
|
||||
import "./i18n/i18n";
|
||||
|
||||
const routes: RouteObject[] = [
|
||||
|
@ -2,6 +2,7 @@ import QuizAnswerer from "@/components/QuizAnswerer";
|
||||
import { createRoot } from "react-dom/client";
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export * from "./widgets";
|
||||
|
||||
import "./i18n/i18nWidget";
|
||||
|
||||
// old widget
|
||||
@ -16,7 +17,10 @@ const widget = {
|
||||
changeFaviconAndTitle: boolean;
|
||||
}) {
|
||||
const element = document.getElementById(selector);
|
||||
if (!element) throw new Error("Element for widget doesn't exist");
|
||||
if (!element) {
|
||||
console.error("❌ Widget: Element for widget doesn't exist:", selector);
|
||||
throw new Error("Element for widget doesn't exist");
|
||||
}
|
||||
|
||||
const root = createRoot(element);
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default function QuizPopup({
|
||||
|
||||
if (!quizData) return null;
|
||||
|
||||
const isQuizCompleted = quizData.settings.cfg.antifraud ? quizData.recentlyCompleted : false;
|
||||
const isQuizCompleted = quizData.settings?.cfg?.antifraud ? quizData.recentlyCompleted : false;
|
||||
if (isQuizCompleted) return null;
|
||||
if (hideOnMobile && isMobile) return null;
|
||||
|
||||
|
@ -56,7 +56,7 @@ export default function QuizSideButton({
|
||||
if (hideOnMobile && isMobile) return null;
|
||||
if (!quizData) return null;
|
||||
|
||||
const isQuizCompleted = quizData.settings.cfg.antifraud ? quizData.recentlyCompleted : false;
|
||||
const isQuizCompleted = quizData.settings?.cfg?.antifraud ? quizData.recentlyCompleted : false;
|
||||
const showButtonFlash = !isQuizCompleted && isFlashEnabled;
|
||||
|
||||
return createPortal(
|
||||
|
Loading…
Reference in New Issue
Block a user