Compare commits

...

22 Commits
est ... main

Author SHA1 Message Date
a532eecdca убираю спасибы 2025-08-18 16:12:35 +03:00
326e2c98b3 квизы умеют слушать запрет ..не пускать назад.. 2025-08-18 04:17:10 +03:00
b58042554f ci: production container deployment 2025-08-13 01:48:18 +03:00
147b776550 без логов 2025-08-13 00:56:46 +03:00
0c9b6e5b7a логи 2025-08-12 04:35:49 +03:00
785e56f9b0 fix шильдики отображают правильный цвет 2025-08-12 04:16:29 +03:00
f330c5c05a поле email при несуществовании не влияет на ФК 2025-08-11 17:29:00 +03:00
b1c3ab7314 fix inntr quiz id param 2025-08-09 23:57:33 +03:00
3baaef83fe deploy to docker 2025-08-09 16:36:54 +03:00
9e301e694f fix fields 2025-07-29 18:26:32 +03:00
f13df84fec ci: workaround branch error on registry deploy 2025-07-29 18:26:07 +03:00
4e2e591cd4 Merge branch 'staging' of gitea.pena:SQuiz/frontAnswerer into staging 2025-07-29 00:39:36 +03:00
670d2bcb3f fix lang in lib 2025-07-29 00:23:25 +03:00
c8ebf9cff0 Merge branch 'main' of gitea.pena:SQuiz/frontAnswerer 2025-07-29 00:18:46 +03:00
656ba9473c ФК кнопка отправки заблокирована пока не введут почту 2025-07-29 00:17:56 +03:00
17bd5cd53f ci: debug on pacjage deploy 2025-07-26 18:26:38 +03:00
b495974dba fix lang и номер телефона не даст отправить данные ФК если короче положенного 2025-07-26 01:48:51 +03:00
87f78ff8cf -- 2025-07-26 00:51:47 +03:00
c5e931702b ci: deploy on push to container registry 2025-07-25 15:12:05 +03:00
106d8b6ad7 fix config widget
Some checks failed
Deploy / DeployService (push) Successful in 24s
Deploy / CreateImage (push) Has been cancelled
2025-07-25 14:37:50 +03:00
32e5853ee8 передеплой
Some checks are pending
Deploy / CreateImage (push) Waiting to run
Deploy / DeployService (push) Successful in 24s
2025-07-25 13:55:22 +03:00
bcf2ca0842 fix lang in lib
All checks were successful
Deploy / CreateImage (push) Successful in 4m30s
Deploy / DeployService (push) Successful in 27s
2025-07-24 02:29:22 +03:00
36 changed files with 840 additions and 395 deletions

@ -2,23 +2,29 @@ name: Deploy
run-name: ${{ gitea.actor }} build image and push to container registry run-name: ${{ gitea.actor }} build image and push to container registry
on: on:
push: registry_package:
branches: types: [published]
- "main" #package_name: "gitea.pena/squiz/frontanswerer/main:latest"
jobs: jobs:
CreateImage: # CreateImage:
runs-on: [skeris] # runs-on: [skeris]
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p # uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with: # with:
runner: skeris # runner: skeris
secrets: # secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }} # REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService: DeployService:
if: contains(github.event.package.name, 'main')
runs-on: [frontprod] runs-on: [frontprod]
needs: CreateImage container:
uses: https://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7 image: gitea.pena/penadevops/container-images/node-compose:main
with: env:
runner: hubprod GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
actionid: ${{ gitea.run_id }} 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 run-name: ${{ gitea.actor }} build image and push to container registry
on: on:
push: registry_package:
branches: types: [published]
- "staging"
jobs: jobs:
CreateImage: # CreateImage:
runs-on: [skeris] # runs-on: [skeris]
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p # uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/build-image.yml@v1.1.6-p
with: # with:
runner: hubstaging # runner: hubstaging
secrets: # secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }} # REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} # REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
DeployService: DeployService:
if: contains(github.event.package.name, 'staging')
runs-on: [frontstaging] runs-on: [frontstaging]
needs: CreateImage container:
uses: http://gitea.pena/PenaDevops/actions.git/.gitea/workflows/deploy.yml@v1.1.4-p7 image: gitea.pena:3000/penadevops/container-images/node-compose:main
with: env:
runner: frontstaging GITHUB_RUN_NUMBER: "${{ inputs.actionid }}"
actionid: ${{ gitea.run_id }} 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: respondent:
container_name: respondent container_name: respondent
restart: unless-stopped restart: unless-stopped
image: gitea.pena/squiz/frontanswerer/main:$GITHUB_RUN_NUMBER image: gitea.pena/squiz/frontanswerer/main:latest
hostname: respondent hostname: respondent
tty: true tty: true
pull_policy: always

@ -2,9 +2,7 @@ services:
respondent: respondent:
container_name: respondent container_name: respondent
restart: unless-stopped restart: unless-stopped
labels: image: gitea.pena/squiz/frontanswerer/staging:latest
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
hostname: respondent hostname: respondent
tty: true tty: true
pull_policy: always

@ -32,19 +32,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
needConfig: true, needConfig: true,
}); });
//firstData.settings.status = "ai"; //firstData.settings.status = "ai";
console.log("useQuizData: firstData received:", firstData);
console.log("useQuizData: firstData.settings:", firstData.settings);
initDataManager({ initDataManager({
status: firstData.settings.status, status: firstData.settings.status,
haveRoot: firstData.settings.cfg.haveRoot, haveRoot: firstData.settings.cfg.haveRoot,
}); });
console.log("useQuizData: calling setQuizData with firstData");
setQuizData(firstData); setQuizData(firstData);
// Определяем нужно ли загружать все данные
console.log("Определяем нужно ли загружать все данные");
console.log(firstData.settings.status);
if (!["ai"].includes(firstData.settings.status)) { if (!["ai"].includes(firstData.settings.status)) {
setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа setNeedFullLoad(true); // Триггерит новый запрос через изменение ключа
return firstData; return firstData;
@ -62,7 +56,13 @@ export function useQuizData(quizId: string, preview: boolean = false) {
}); });
addQuestions(data.questions.slice(1)); 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) { if (currentPage >= questions.length) {
@ -74,15 +74,16 @@ export function useQuizData(quizId: string, preview: boolean = false) {
limit: 1, limit: 1,
needConfig: false, 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); addQuestions(data.questions);
changeNextLoading(false); changeNextLoading(false);
return data;
// Возвращаем полную структуру данных с настройками из store
const currentState = useQuizStore.getState();
return {
...currentState,
questions: [...currentState.questions, ...data.questions],
};
} catch (p) { } catch (p) {
console.log(p);
setPage(questions.length); setPage(questions.length);
changeNextLoading(false); changeNextLoading(false);
} }

@ -168,10 +168,7 @@ export async function getAndParceData(props: GetDataProps) {
} }
//Парсим строки в строках //Парсим строки в строках
console.log("до парса_______________________");
const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse)); const quizSettings = replaceSpacesToEmptyLines(parseQuizData(quizDataResponse));
console.log("после парса_______________________");
console.log(quizSettings);
//Единоразово стрингифаим ВСЁ распаршенное и удаляем лишние пробелы //Единоразово стрингифаим ВСЁ распаршенное и удаляем лишние пробелы
const res = JSON.parse( const res = JSON.parse(
JSON.stringify({ data: quizSettings }) JSON.stringify({ data: quizSettings })

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 "moment/dist/locale/ru";
import { useQuizStore, setQuizData, addquizid } from "@/stores/useQuizStore"; import { useQuizStore, setQuizData, addquizid } from "@/stores/useQuizStore";
import { initDataManager, statusOfQuiz } from "@/utils/hooks/useQuestionFlowControl"; import { initDataManager, statusOfQuiz } from "@/utils/hooks/useQuestionFlowControl";
moment.locale("ru"); moment.locale("ru");
const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText; const localeText = ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
@ -55,13 +56,6 @@ function QuizAnswererInner({
addquizid(quizId); addquizid(quizId);
}, []); }, []);
useEffect(() => {
console.log(settings);
console.log(questions);
console.log("r");
console.log(r);
}, [questions, settings]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
vkMetrics.quizOpened(); vkMetrics.quizOpened();
@ -72,7 +66,6 @@ function QuizAnswererInner({
useEffect(() => { useEffect(() => {
//Хук на случай если данные переданы нам сразу, а не "нам нужно их запросить" //Хук на случай если данные переданы нам сразу, а не "нам нужно их запросить"
if (quizSettings !== undefined) { if (quizSettings !== undefined) {
console.log("QuizAnswerer: calling setQuizData with quizSettings");
setQuizData(quizSettings); setQuizData(quizSettings);
initDataManager({ initDataManager({
status: quizSettings.settings.status, status: quizSettings.settings.status,
@ -98,19 +91,26 @@ function QuizAnswererInner({
}; };
}, []); }, []);
console.log("settings"); if (isLoading && !questions.length) {
console.log(settings); return <LoadingSkeleton />;
if (isLoading && !questions.length) return <LoadingSkeleton />; }
console.log("error"); if (error) {
console.log(error); return <ApologyPage error={error} />;
if (error) return <ApologyPage error={error} />; }
if (Object.keys(settings).length == 0) return <ApologyPage error={new Error("quiz data is null")} />; if (Object.keys(settings).length == 0) {
if (questions.length === 0) return <ApologyPage error={new Error("No questions found")} />; 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")} />; 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 = ( const quizContainer = (
<Box <Box

@ -6,11 +6,7 @@ type Props = Partial<FallbackProps>;
export const ApologyPage = ({ error }: Props) => { export const ApologyPage = ({ error }: Props) => {
let message = error.message || error.response?.data || " "; let message = error.message || error.response?.data || " ";
console.log("message");
console.log(message.toLowerCase());
const { t } = useTranslation(); const { t } = useTranslation();
console.log("t");
console.log(t(message.toLowerCase()));
return ( return (
<Box <Box

@ -26,6 +26,7 @@ import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { isProduction } from "@/utils/defineDomain"; import { isProduction } from "@/utils/defineDomain";
import { useQuizStore } from "@/stores/useQuizStore"; import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
type Props = { type Props = {
currentQuestion: AnyTypedQuizQuestion; currentQuestion: AnyTypedQuizQuestion;
@ -45,6 +46,8 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
const [text, setText] = useState(""); const [text, setText] = useState("");
const [adress, setAdress] = useState(""); const [adress, setAdress] = useState("");
const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight); const [screenHeight, setScreenHeight] = useState<number>(window.innerHeight);
const [emailError, setEmailError] = useState("");
const [phoneError, setPhoneError] = useState("");
const fireOnce = useRef(true); const fireOnce = useRef(true);
const [fire, setFire] = useState(false); const [fire, setFire] = useState(false);
@ -120,13 +123,23 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
async function handleShowResultsClick() { async function handleShowResultsClick() {
const FC = settings.cfg.formContact.fields; 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"); return enqueueSnackbar("Incorrect email entered");
} }
if (fireOnce.current) { if (fireOnce.current) {
if (name.length === 0 && email.length === 0 && phone.length === 0 && text.length === 0 && adress.length === 0) // Проверяем, что хотя бы одно видимое поле заполнено
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")); return enqueueSnackbar(t("Please fill in the fields"));
}
//почта валидна, хоть одно поле заполнено //почта валидна, хоть одно поле заполнено
setFire(true); setFire(true);
@ -177,6 +190,115 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 ( return (
<Box <Box
sx={{ sx={{
@ -250,13 +372,17 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
name={name} name={name}
setName={setName} setName={setName}
email={email} email={email}
setEmail={setEmail} setEmail={handleEmailChange}
phone={phone} phone={phone}
setPhone={setPhone} setPhone={handlePhoneChange}
text={text} text={text}
setText={setText} setText={setText}
adress={adress} adress={adress}
setAdress={setAdress} setAdress={setAdress}
emailError={emailError}
phoneError={phoneError}
onEmailBlur={handleEmailBlur}
onPhoneBlur={handlePhoneBlur}
crutch={{ crutch={{
disableEmail: isDisableEmail, disableEmail: isDisableEmail,
}} }}
@ -304,7 +430,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
</Box> </Box>
<Button <Button
disabled={!(ready && !fire)} disabled={!(ready && !fire && isPhoneValid && (isEmailFieldVisible ? isEmailValid : true))}
variant="contained" variant="contained"
onClick={handleShowResultsClick} onClick={handleShowResultsClick}
sx={{ sx={{
@ -336,12 +462,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => {
margitTop: "auto", margitTop: "auto",
}} }}
> >
<NameplateLogo {quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
style={{
fontSize: "20px",
color: quizThemes[settings.cfg.theme].isLight ? "#151515" : "#FFFFFF",
}}
/>
</Box> </Box>
)} )}
</Box> </Box>

@ -17,6 +17,8 @@ type InputProps = {
isPhone?: boolean; isPhone?: boolean;
type?: HTMLInputTypeAttribute; type?: HTMLInputTypeAttribute;
value?: string; value?: string;
onBlur?: () => void;
error?: string;
}; };
const TextField = MuiTextField as unknown as FC<TextFieldProps>; const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -34,7 +36,18 @@ function phoneChange(e: ChangeEvent<HTMLInputElement>, mask: string) {
return a || ""; 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 theme = useTheme();
const isMobile = useRootContainerSize() < 600; const isMobile = useRootContainerSize() < 600;
const { settings } = useQuizStore(); const { settings } = useQuizStore();
@ -57,8 +70,11 @@ export const CustomInput = ({ title, desc, Icon, onChange, onChangePhone, isPhon
onChange={(e: ChangeEvent<HTMLInputElement>) => onChange={(e: ChangeEvent<HTMLInputElement>) =>
isPhone ? onChangePhone?.(phoneChange(e, mask)) : onChange?.(e) isPhone ? onChangePhone?.(phoneChange(e, mask)) : onChange?.(e)
} }
onBlur={onBlur}
type={isPhone ? "tel" : type} type={isPhone ? "tel" : type}
value={value} value={value}
error={!!error}
helperText={error}
sx={{ sx={{
width: isMobile ? "100%" : "390px", width: isMobile ? "100%" : "390px",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,

@ -13,13 +13,17 @@ type InputsProps = {
name: string; name: string;
setName: Dispatch<SetStateAction<string>>; setName: Dispatch<SetStateAction<string>>;
email: string; email: string;
setEmail: Dispatch<SetStateAction<string>>; setEmail: (email: string) => void;
phone: string; phone: string;
setPhone: Dispatch<SetStateAction<string>>; setPhone: (phone: string) => void;
text: string; text: string;
setText: Dispatch<SetStateAction<string>>; setText: Dispatch<SetStateAction<string>>;
adress: string; adress: string;
setAdress: Dispatch<SetStateAction<string>>; setAdress: Dispatch<SetStateAction<string>>;
emailError?: string;
phoneError?: string;
onEmailBlur?: () => void;
onPhoneBlur?: () => void;
crutch: { crutch: {
disableEmail: boolean; disableEmail: boolean;
}; };
@ -39,6 +43,10 @@ export const Inputs = ({
setText, setText,
adress, adress,
setAdress, setAdress,
emailError,
phoneError,
onEmailBlur,
onPhoneBlur,
crutch, crutch,
}: InputsProps) => { }: InputsProps) => {
const { settings } = useQuizStore(); const { settings } = useQuizStore();
@ -64,11 +72,13 @@ export const Inputs = ({
onChange={({ target }) => { onChange={({ target }) => {
setEmail(target.value.replaceAll(/\s/g, "")); setEmail(target.value.replaceAll(/\s/g, ""));
}} }}
onBlur={onEmailBlur}
id={email} id={email}
title={FC["email"].innerText || `${t("Enter")} Email`} title={FC["email"].innerText || `${t("Enter")} Email`}
desc={FC["email"].text || "Email"} desc={FC["email"].text || "Email"}
Icon={EmailIcon} Icon={EmailIcon}
type="email" type="email"
error={emailError}
/> />
); );
const Phone = ( const Phone = (
@ -77,12 +87,14 @@ export const Inputs = ({
onChangePhone={(phone: string) => { onChangePhone={(phone: string) => {
setPhone(phone); setPhone(phone);
}} }}
onBlur={onPhoneBlur}
value={phone} value={phone}
id={phone} id={phone}
title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`} title={FC["phone"].innerText || `${t("Enter")} ${t("Phone number").toLowerCase()}`}
desc={FC["phone"].text || t("Phone number")} desc={FC["phone"].text || t("Phone number")}
Icon={PhoneIcon} Icon={PhoneIcon}
isPhone={true} isPhone={true}
error={phoneError}
/> />
); );
const Text = ( const Text = (

@ -15,7 +15,7 @@ export const Footer = ({ stepNumber, nextButton, prevButton }: FooterProps) => {
const theme = useTheme(); const theme = useTheme();
const { questions, settings } = useQuizStore(); const { questions, settings } = useQuizStore();
const questionsAmount = questions.filter(({ type }) => type !== "result").length; const questionsAmount = questions.filter(({ type }) => type !== "result").length;
const { t } = useTranslation(); const { t, i18n } = useTranslation();
return ( return (
<Box <Box

@ -6,6 +6,23 @@ import { AnyTypedQuizQuestion, QuizQuestionVariant } from "@/index";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useQuizStore } from "@/stores/useQuizStore"; 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 = () => { export const PointSystemResultList = () => {
const theme = useTheme(); const theme = useTheme();
const { questions } = useQuizStore(); const { questions } = useQuizStore();
@ -16,7 +33,12 @@ export const PointSystemResultList = () => {
(q: AnyTypedQuizQuestion): q is QuizQuestionVariant => q.type === "variant" (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 answerIndex = 0;
let currentVariants = currentQuestion.content.variants; let currentVariants = currentQuestion.content.variants;
@ -53,7 +75,7 @@ export const PointSystemResultList = () => {
color: theme.palette.grey[500], color: theme.palette.grey[500],
}} }}
> >
{currentQuestion.page + 1}. {index + 1}.
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{

@ -12,6 +12,7 @@ import { quizThemes } from "@utils/themes/Publication/themePublication";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import type { QuizQuestionResult } from "@/model/questionTypes/result"; import type { QuizQuestionResult } from "@/model/questionTypes/result";
import type { QuizQuestionVariant } from "@/model/questionTypes/variant";
import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe"; import QuizVideo from "@/ui_kit/VideoIframe/VideoIframe";
import { TextAccordion } from "./tools/TextAccordion"; import { TextAccordion } from "./tools/TextAccordion";
import { PointSystemResultList } from "./PointSystemResultList"; import { PointSystemResultList } from "./PointSystemResultList";
@ -20,11 +21,27 @@ import { sendFC, sendResult } from "@/api/quizRelase";
import { isProduction } from "@/utils/defineDomain"; import { isProduction } from "@/utils/defineDomain";
import { useQuizStore } from "@/stores/useQuizStore"; import { useQuizStore } from "@/stores/useQuizStore";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark";
type ResultFormProps = { type ResultFormProps = {
resultQuestion: QuizQuestionResult; 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) => { export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useRootContainerSize() < 650; const isMobile = useRootContainerSize() < 650;
@ -38,6 +55,22 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber); const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
const { t } = useTranslation(); 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(() => { useEffect(() => {
vkMetrics.resultIdShown(resultQuestion.id); vkMetrics.resultIdShown(resultQuestion.id);
yandexMetrics.resultIdShown(resultQuestion.id); yandexMetrics.resultIdShown(resultQuestion.id);
@ -259,7 +292,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
fontWeight: 600, fontWeight: 600,
}} }}
> >
{pointsSum} {t("of")} {questions.filter((e) => e.type != "result").length} {pointsSum} {t("of")} {totalQuestions}
</Typography> </Typography>
<TextAccordion <TextAccordion
headerText={ headerText={
@ -308,12 +341,7 @@ export const ResultForm = ({ resultQuestion }: ResultFormProps) => {
bottom: "90px", bottom: "90px",
}} }}
> >
<NameplateLogo {quizThemes[settings.cfg.theme].isLight ? <NameplateLogoDark /> : <NameplateLogo />}
style={{
fontSize: "23px",
color: quizThemes[settings.cfg.theme].isLight ? "#000000" : "#F5F7FF",
}}
/>
</Box> </Box>
)} )}
<Box <Box

@ -9,6 +9,7 @@ import { useUADevice } from "@utils/hooks/useUADevice";
import { quizThemes } from "@utils/themes/Publication/themePublication"; import { quizThemes } from "@utils/themes/Publication/themePublication";
import { NameplateLogo } from "@icons/NameplateLogo"; import { NameplateLogo } from "@icons/NameplateLogo";
import { NameplateLogoDark } from "@icons/NameplateLogoDark";
import { useQuizViewStore } from "@/stores/quizView"; import { useQuizViewStore } from "@/stores/quizView";
import { DESIGN_LIST } from "@/utils/designList"; import { DESIGN_LIST } from "@/utils/designList";
@ -153,17 +154,13 @@ export const StartPageViewPublication = () => {
: undefined, : undefined,
}} }}
> >
<NameplateLogo {settings.cfg.startpageType === "expanded" ? (
style={{ <NameplateLogo />
fontSize: "23px", ) : quizThemes[settings.cfg.theme].isLight ? (
color: <NameplateLogoDark />
settings.cfg.startpageType === "expanded" ) : (
? "#FFFFFF" <NameplateLogo />
: quizThemes[settings.cfg.theme].isLight )}
? "#151515"
: "#FFFFFF",
}}
/>
</Box> </Box>
); );

@ -70,7 +70,7 @@ export default function ViewPublicationPage() {
if (settings.cfg.antifraud && recentlyCompleted) throw new Error("Quiz already completed"); if (settings.cfg.antifraud && recentlyCompleted) throw new Error("Quiz already completed");
if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question"; if (currentQuizStep === "startpage" && settings.cfg.noStartPage) currentQuizStep = "question";
if (!currentQuestion) if (!currentQuestion) {
return ( return (
<ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}> <ThemeProvider theme={quizThemes[settings.cfg.theme || "StandardTheme"].theme}>
<Typography <Typography
@ -81,6 +81,7 @@ export default function ViewPublicationPage() {
</Typography> </Typography>
</ThemeProvider> </ThemeProvider>
); );
}
const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id); const currentAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id);

@ -36,10 +36,6 @@ export const Number = ({ currentQuestion }: NumberProps) => {
answer || answer ||
(reversed ? max + min - currentQuestion.content.start + "—" + max : currentQuestion.content.start + "—" + max); (reversed ? max + min - currentQuestion.content.start + "—" + max : currentQuestion.content.start + "—" + max);
useEffect(() => {
console.log("reversed:", reversed);
}, [reversed]);
const sendAnswerToBackend = async (value: string, noUpdate = false) => { const sendAnswerToBackend = async (value: string, noUpdate = false) => {
if (!noUpdate) { if (!noUpdate) {
updateAnswer(currentQuestion.id, value, 0); updateAnswer(currentQuestion.id, value, 0);

@ -10,7 +10,7 @@ interface Props {
export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) { export default function NextButton({ isNextButtonEnabled, moveToNextQuestion }: Props) {
const { settings, nextLoading } = useQuizStore(); const { settings, nextLoading } = useQuizStore();
const { t } = useTranslation(); const { t, i18n } = useTranslation();
return nextLoading ? ( return nextLoading ? (
<Skeleton <Skeleton

@ -13,7 +13,8 @@ export default function PrevButton({ isPreviousButtonEnabled, moveToPrevQuestion
const theme = useTheme(); const theme = useTheme();
const { settings } = useQuizStore(); const { settings } = useQuizStore();
const isMobileMini = useRootContainerSize() < 382; const isMobileMini = useRootContainerSize() < 382;
const { t } = useTranslation(); const { t, i18n } = useTranslation();
return ( return (
<Button <Button
disabled={!isPreviousButtonEnabled} disabled={!isPreviousButtonEnabled}

@ -27,7 +27,6 @@ export interface GetQuizDataResponse {
} }
export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> { export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizSettings, "recentlyCompleted"> {
console.log(quizDataResponse);
const readyData = { const readyData = {
cnt: quizDataResponse.cnt, cnt: quizDataResponse.cnt,
show_badge: quizDataResponse.show_badge, show_badge: quizDataResponse.show_badge,
@ -51,7 +50,8 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
answerType: "single", answerType: "single",
onlyNumbers: false, 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 { return {
description: item.desc, description: item.desc,
id: item.id, id: item.id,
@ -66,7 +66,6 @@ export function parseQuizData(quizDataResponse: GetQuizDataResponse): Omit<QuizS
readyData.questions = items; readyData.questions = items;
if (quizDataResponse?.settings !== undefined) { if (quizDataResponse?.settings !== undefined) {
console.log("попытка парсануть сеттингс", quizDataResponse.settings);
readyData.settings = { readyData.settings = {
fp: quizDataResponse.settings.fp, fp: quizDataResponse.settings.fp,
rep: quizDataResponse.settings.rep, rep: quizDataResponse.settings.rep,

@ -119,6 +119,7 @@ export interface QuizConfig {
showfc?: boolean; showfc?: boolean;
yandexMetricsNumber?: number; yandexMetricsNumber?: number;
vkMetricsNumber?: number; vkMetricsNumber?: number;
backBlocked?: boolean;
} }
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address"; export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";

@ -25,22 +25,14 @@ export const useQuizStore = create<QuizStore>(() => ({
})); }));
export const setQuizData = (data: QuizSettings) => { 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(); const currentState = useQuizStore.getState();
console.log("Current state before update:", currentState);
useQuizStore.setState((state: QuizStore) => { useQuizStore.setState((state: QuizStore) => {
const newState = { ...state, ...data }; const newState = { ...state, ...data };
console.log("New state after update:", newState);
return newState; return newState;
}); });
const updatedState = useQuizStore.getState(); const updatedState = useQuizStore.getState();
console.log("State after setState:", updatedState);
}; };
export const addQuestions = (newQuestions: AnyTypedQuizQuestion[]) => export const addQuestions = (newQuestions: AnyTypedQuizQuestion[]) =>

@ -13,6 +13,6 @@ const isProduction = !(
//туризм больше не в исключениях //туризм больше не в исключениях
if (!isProduction) domain = "https://s.hbpn.link"; if (!isProduction) domain = "https://s.hbpn.link";
domain = "https://hbpn.link"; // domain = "https://hbpn.link";
export { domain, isProduction }; export { domain, isProduction };

@ -37,6 +37,6 @@ async function sendErrorsToServer() {
// body: errorsQueue, // body: errorsQueue,
// useToken: true, // useToken: true,
// }); // });
console.log(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue); console.info(`Fake-sending ${errorsQueue.length} errors to server`, errorsQueue);
errorsQueue = []; errorsQueue = [];
} }

@ -14,11 +14,6 @@ export function useAIQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt, quizStep } = useQuizStore(); const { settings, questions, quizId, cnt, quizStep } = useQuizStore();
useEffect(() => {
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах //Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
const answers = useQuizViewStore((state) => state.answers); const answers = useQuizViewStore((state) => state.answers);
@ -29,9 +24,6 @@ export function useAIQuiz() {
const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber); const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber);
const currentQuestion = useMemo(() => { const currentQuestion = useMemo(() => {
console.log("выбор currentQuestion");
console.log("quizStep ", quizStep);
console.log("questions[quizStep] ", questions[quizStep]);
const calcQuestion = questions[quizStep]; const calcQuestion = questions[quizStep];
if (calcQuestion) { if (calcQuestion) {
vkMetrics.questionPassed(calcQuestion.id); vkMetrics.questionPassed(calcQuestion.id);
@ -44,8 +36,6 @@ export function useAIQuiz() {
useEffect(() => { useEffect(() => {
if (currentQuestion.type === "result") showResult(); if (currentQuestion.type === "result") showResult();
if (currentQuestion) changeNextLoading(false); if (currentQuestion) changeNextLoading(false);
console.log("questions");
console.log(questions);
}, [currentQuestion, questions]); }, [currentQuestion, questions]);
//Показать визуалом юзеру результат //Показать визуалом юзеру результат
@ -86,7 +76,7 @@ export function useAIQuiz() {
const setQuestion = useCallback((_: string) => {}, []); const setQuestion = useCallback((_: string) => {}, []);
//Анализ дисаблить ли кнопки навигации //Анализ дисаблить ли кнопки навигации
const isPreviousButtonEnabled = quizStep > 0; const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : quizStep > 0;
//Анализ дисаблить ли кнопки навигации //Анализ дисаблить ли кнопки навигации
const isNextButtonEnabled = useMemo(() => { const isNextButtonEnabled = useMemo(() => {

@ -14,12 +14,6 @@ export function useBranchingQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt } = useQuizStore(); const { settings, questions, quizId, cnt } = useQuizStore();
useEffect(() => {
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
console.log(questions);
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
const sortedQuestions = useMemo(() => { 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(() => { const isNextButtonEnabled = useMemo(() => {
@ -237,9 +231,6 @@ export function useBranchingQuiz() {
return hasAnswer; return hasAnswer;
} }
console.log(linearQuestionIndex);
console.log(questions.length);
console.log(cnt);
if (linearQuestionIndex !== null && questions.length < cnt) return true; if (linearQuestionIndex !== null && questions.length < cnt) return true;
return Boolean(nextQuestion); return Boolean(nextQuestion);
}, [answers, currentQuestion, nextQuestion]); }, [answers, currentQuestion, nextQuestion]);

@ -14,12 +14,6 @@ export function useLinearQuiz() {
//Получаем инфо о квизе и список вопросов. //Получаем инфо о квизе и список вопросов.
const { settings, questions, quizId, cnt } = useQuizStore(); const { settings, questions, quizId, cnt } = useQuizStore();
useEffect(() => {
console.log("useQuestionFlowControl useEffect");
console.log(questions);
}, [questions]);
console.log(questions);
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page.
//За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page
const sortedQuestions = useMemo(() => { 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(() => { const isNextButtonEnabled = useMemo(() => {
@ -237,9 +231,6 @@ export function useLinearQuiz() {
return hasAnswer; return hasAnswer;
} }
console.log(linearQuestionIndex);
console.log(questions.length);
console.log(cnt);
if (linearQuestionIndex !== null && questions.length < cnt) return true; if (linearQuestionIndex !== null && questions.length < cnt) return true;
return Boolean(nextQuestion); return Boolean(nextQuestion);
}, [answers, currentQuestion, nextQuestion]); }, [answers, currentQuestion, nextQuestion]);

@ -26,6 +26,7 @@
"preview": "vite preview", "preview": "vite preview",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"prepublishOnly": "npm run build:package", "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" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {

@ -56,5 +56,8 @@
"Step": "Шаг", "Step": "Шаг",
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите", "questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите",
"Add your image": "Добавьте своё изображение", "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); const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i);
if (langMatch) { if (langMatch) {
//console.log("Язык из URL:", langMatch[1]); const detectedLang = langMatch[1].toLowerCase();
return langMatch[1].toLowerCase(); return detectedLang;
} }
//console.log('Язык не указан в URL, используем "ru"');
return "ru"; // Жёсткий фолбэк return "ru"; // Жёсткий фолбэк
}; };
@ -33,6 +32,9 @@ i18n
backend: { backend: {
loadPath: "/locales/{{lng}}.json", loadPath: "/locales/{{lng}}.json",
allowMultiLoading: false, allowMultiLoading: false,
requestOptions: {
cache: "no-store",
},
}, },
react: { react: {
useSuspense: false, // Отключаем для совместимости с React 18 useSuspense: false, // Отключаем для совместимости с React 18
@ -43,11 +45,11 @@ i18n
caches: [], // Не использовать localStorage caches: [], // Не использовать localStorage
}, },
parseMissingKeyHandler: (key) => { parseMissingKeyHandler: (key) => {
console.warn("Missing translation:", key); console.warn("⚠️ Main i18n: Missing translation:", key);
return key; // Вернёт ключ вместо ошибки return key; // Вернёт ключ вместо ошибки
}, },
missingKeyHandler: (lngs, ns, key) => { missingKeyHandler: (lngs, ns, key) => {
console.error("🚨 Missing i18n key:", { console.error("🚨 Main i18n: Missing i18n key:", {
key, key,
languages: lngs, languages: lngs,
namespace: ns, namespace: ns,
@ -55,21 +57,35 @@ i18n
}); });
}, },
}) })
.then(() => {
//console.log("i18n инициализирован! Текущий язык:", i18n.language);
//console.log("Загруженные переводы:", i18n.store.data);
})
.catch((err) => { .catch((err) => {
console.error("Ошибка i18n:", err); console.error("❌ Main i18n: Initialization failed:", err);
}); });
// 3. Логирование всех событий // 3. Логирование всех событий
i18n.on("languageChanged", (lng) => { 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) => { 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; export default i18n;

@ -5,13 +5,15 @@ import { initReactI18next } from "react-i18next";
const getLanguageFromURL = (): string => { const getLanguageFromURL = (): string => {
const path = window.location.pathname; const path = window.location.pathname;
const langMatch = path.match(/^\/(en|ru|uz)(\/|$)/i); 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. Локали, встроенные прямо в конфиг // 2. Локали, встроенные прямо в конфиг
const r = { const r = {
ru: { ru: {
translation: {
"quiz is inactive": "Квиз не активирован", "quiz is inactive": "Квиз не активирован",
"no questions found": "Нет созданных вопросов", "no questions found": "Нет созданных вопросов",
"quiz is empty": "Квиз пуст", "quiz is empty": "Квиз пуст",
@ -67,12 +69,15 @@ const r = {
"Get results": "Получить результаты", "Get results": "Получить результаты",
"Data sent successfully": "Данные успешно отправлены", "Data sent successfully": "Данные успешно отправлены",
Step: "Шаг", Step: "Шаг",
"questions are not ready yet": "Вопросы для аудитории ещё не созданы. Пожалуйста, подождите", "questions are not ready yet": "Вопросы для аудитории пока не готовы. Подождите",
"Add your image": "Добавьте своё изображение", "Add your image": "Добавьте своё изображение",
"select emoji": "выберите смайлик", "select emoji": "выберите смайлик",
"Please complete the phone number": "Пожалуйста, заполните номер телефона до конца",
"": "", // Пустой ключ для fallback "": "", // Пустой ключ для fallback
}, },
},
en: { en: {
translation: {
"quiz is inactive": "Quiz is inactive", "quiz is inactive": "Quiz is inactive",
"no questions found": "No questions found", "no questions found": "No questions found",
"quiz is empty": "Quiz is empty", "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", "questions are not ready yet": "There are no questions for the audience yet. Please wait",
"Add your image": "Add your image", "Add your image": "Add your image",
"select emoji": "select emoji", "select emoji": "select emoji",
"Please complete the phone number": "Please complete the phone number",
"": "", // Пустой ключ для fallback "": "", // Пустой ключ для fallback
}, },
},
uz: { uz: {
translation: {
"quiz is inactive": "Test faol emas", "quiz is inactive": "Test faol emas",
"no questions found": "Savollar topilmadi", "no questions found": "Savollar topilmadi",
"quiz is empty": "Test boʻsh", "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", "questions are not ready yet": "Tomoshabinlar uchun hozircha savollar yo'q. Iltimos kuting",
"Add your image": "Rasmingizni qo'shing", "Add your image": "Rasmingizni qo'shing",
"select emoji": "emoji tanlang", "select emoji": "emoji tanlang",
"Please complete the phone number": "Iltimos, telefon raqamini to'liq kiriting",
"": "", // Пустой ключ для fallback "": "", // Пустой ключ для 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 // 3. Конфигурация i18n без Backend
i18n i18n
.use(initReactI18next) .use(initReactI18next)
@ -217,11 +246,11 @@ i18n
caches: [], caches: [],
}, },
parseMissingKeyHandler: (key) => { parseMissingKeyHandler: (key) => {
console.warn("Missing translation:", key); console.warn("⚠️ Widget i18n: Missing translation key:", key);
return key; return key;
}, },
missingKeyHandler: (lngs, ns, key) => { missingKeyHandler: (lngs, ns, key) => {
console.error("🚨 Missing i18n key:", { console.error("🚨 Widget i18n: Missing i18n key:", {
key, key,
languages: lngs, languages: lngs,
namespace: ns, namespace: ns,
@ -229,17 +258,30 @@ i18n
}); });
}, },
}) })
.then(() => { .catch((error) => {
console.log("i18n initialized. Current language:", i18n.language); console.error("❌ Widget i18n: Initialization failed:", error);
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));
}); });
}
// 4. Логирование событий // 4. Логирование событий
i18n.on("languageChanged", (lng) => { 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; export default i18n;

@ -2,6 +2,7 @@ import { createRoot } from "react-dom/client";
import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom"; import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom";
import App from "./App"; import App from "./App";
import { StrictMode, lazy } from "react"; import { StrictMode, lazy } from "react";
import "./i18n/i18n"; import "./i18n/i18n";
const routes: RouteObject[] = [ const routes: RouteObject[] = [

@ -2,6 +2,7 @@ import QuizAnswerer from "@/components/QuizAnswerer";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export * from "./widgets"; export * from "./widgets";
import "./i18n/i18nWidget"; import "./i18n/i18nWidget";
// old widget // old widget
@ -16,7 +17,10 @@ const widget = {
changeFaviconAndTitle: boolean; changeFaviconAndTitle: boolean;
}) { }) {
const element = document.getElementById(selector); 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); const root = createRoot(element);

@ -56,7 +56,7 @@ export default function QuizPopup({
if (!quizData) return null; 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 (isQuizCompleted) return null;
if (hideOnMobile && isMobile) return null; if (hideOnMobile && isMobile) return null;

@ -56,7 +56,7 @@ export default function QuizSideButton({
if (hideOnMobile && isMobile) return null; if (hideOnMobile && isMobile) return null;
if (!quizData) 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; const showButtonFlash = !isQuizCompleted && isFlashEnabled;
return createPortal( return createPortal(