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,13 +5,15 @@ 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: {
|
||||
translation: {
|
||||
"quiz is inactive": "Квиз не активирован",
|
||||
"no questions found": "Нет созданных вопросов",
|
||||
"quiz is empty": "Квиз пуст",
|
||||
@ -67,12 +69,15 @@ const r = {
|
||||
"Get results": "Получить результаты",
|
||||
"Data sent successfully": "Данные успешно отправлены",
|
||||
Step: "Шаг",
|
||||
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
|
||||
"questions are not ready yet": "Вопросы для аудитории пока не готовы. Подождите",
|
||||
"Add your image": "Добавьте своё изображение",
|
||||
"select emoji": "выберите смайлик",
|
||||
"Please complete the phone number": "Пожалуйста, заполните номер телефона до конца",
|
||||
"": "", // Пустой ключ для fallback
|
||||
},
|
||||
},
|
||||
en: {
|
||||
translation: {
|
||||
"quiz is inactive": "Quiz is inactive",
|
||||
"no questions found": "No questions found",
|
||||
"quiz is empty": "Quiz is empty",
|
||||
@ -131,9 +136,12 @@ const r = {
|
||||
"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: {
|
||||
translation: {
|
||||
"quiz is inactive": "Test faol emas",
|
||||
"no questions found": "Savollar topilmadi",
|
||||
"quiz is empty": "Test boʻsh",
|
||||
@ -192,10 +200,31 @@ const r = {
|
||||
"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
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Проверяем, не инициализирован ли уже 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)
|
||||
@ -217,11 +246,11 @@ i18n
|
||||
caches: [],
|
||||
},
|
||||
parseMissingKeyHandler: (key) => {
|
||||
console.warn("Missing translation:", key);
|
||||
console.warn("⚠️ Widget i18n: Missing translation key:", key);
|
||||
return key;
|
||||
},
|
||||
missingKeyHandler: (lngs, ns, key) => {
|
||||
console.error("🚨 Missing i18n key:", {
|
||||
console.error("🚨 Widget i18n: Missing i18n key:", {
|
||||
key,
|
||||
languages: lngs,
|
||||
namespace: ns,
|
||||
@ -229,17 +258,30 @@ i18n
|
||||
});
|
||||
},
|
||||
})
|
||||
.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));
|
||||
.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