From 8ff9422842d62e7185ca8f8016c3e0005ee4c5a2 Mon Sep 17 00:00:00 2001 From: Nastya Date: Fri, 14 Nov 2025 00:53:02 +0300 Subject: [PATCH] add crutch fc first --- .../ContactForm/ContactForm.tsx | 3 +- .../ViewPublicationPage/ResultForm.tsx | 3 +- .../questions/Text/TextNormal.tsx | 47 +++- .../hooks/FlowControlLogic/useFirstFCQuiz.ts | 266 ++++++++++++++++++ lib/utils/hooks/useQuestionFlowControl.ts | 12 +- 5 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 lib/utils/hooks/FlowControlLogic/useFirstFCQuiz.ts diff --git a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx index 6ef7431..5ba559b 100644 --- a/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx +++ b/lib/components/ViewPublicationPage/ContactForm/ContactForm.tsx @@ -34,6 +34,7 @@ type Props = { }; //Костыль для особого квиза. Для него не нужно показывать email адрес const isDisableEmail = window.location.pathname.includes("/377c7570-1bee-4320-ac1e-d731b6223ce8"); +let isCrutch13112025 = window.location.pathname === "/d557133f-26b6-4b0b-93da-c538758a65d4"; export const ContactForm = ({ currentQuestion, onShowResult }: Props) => { const theme = useTheme(); @@ -176,7 +177,7 @@ export const ContactForm = ({ currentQuestion, onShowResult }: Props) => { } catch (e) { enqueueSnackbar(t("Please try again later")); } - if (settings.cfg.resultInfo.showResultForm === "after") { + if (settings.cfg.resultInfo.showResultForm === "after" || isCrutch13112025) { onShowResult(); } enqueueSnackbar(t("Data sent successfully")); diff --git a/lib/components/ViewPublicationPage/ResultForm.tsx b/lib/components/ViewPublicationPage/ResultForm.tsx index 243daa5..ec09774 100644 --- a/lib/components/ViewPublicationPage/ResultForm.tsx +++ b/lib/components/ViewPublicationPage/ResultForm.tsx @@ -24,7 +24,8 @@ import { useTranslation } from "react-i18next"; import { NameplateLogoDark } from "@/assets/icons/NameplateLogoDark"; const pathOnly = window.location.pathname; -const isCrutchNoDraw = pathOnly === "/28525cd7-9ddf-4c4a-a55b-e3d2f7d47583"; +let isCrutch13112025 = window.location.pathname === "/d557133f-26b6-4b0b-93da-c538758a65d4"; +const isCrutchNoDraw = pathOnly === "/28525cd7-9ddf-4c4a-a55b-e3d2f7d47583" || isCrutch13112025; type ResultFormProps = { resultQuestion: QuizQuestionResult; }; diff --git a/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx b/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx index 9f40523..fc3bc9c 100644 --- a/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx +++ b/lib/components/ViewPublicationPage/questions/Text/TextNormal.tsx @@ -17,6 +17,45 @@ interface TextNormalProps { stepNumber?: number | null; } +let isCrutch13112025 = window.location.pathname === "/d557133f-26b6-4b0b-93da-c538758a65d4"; + +const answresList = [ + "обогащение", + "конфискация", + "обналичивание", + "протекция", + "честность", + + "договорняк", + "доверие", + "кумовство", + "офшор", + "фаворитизм", + + "мздоимство", + "схема", + "монополия", + "лоббизм", + "комплаенс", + + "санкции", + "судимость", + "контрагент", + "казнокрад", + "откат", + + "штраф", + "непотизм", + "легализация", + "бенефициар", + "анонимность", + + "прачечная", + "аффилированность", + "декларация", + "фальсификация", + "расследование", +]; export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => { const { settings } = useQuizStore(); const { updateAnswer } = useQuizViewStore((state) => state); @@ -25,7 +64,13 @@ export const TextNormal = ({ currentQuestion, answer }: TextNormalProps) => { const theme = useTheme(); const onInputChange = async ({ target }: ChangeEvent) => { - updateAnswer(currentQuestion.id, target.value, 0); + updateAnswer( + currentQuestion.id, + target.value, + isCrutch13112025 + ? Number(target.value.replace(/\s+/g, "").toLowerCase() === answresList[currentQuestion.page]) + : 0 + ); }; const choiceImgUrlQuestion = useMemo(() => { if ( diff --git a/lib/utils/hooks/FlowControlLogic/useFirstFCQuiz.ts b/lib/utils/hooks/FlowControlLogic/useFirstFCQuiz.ts new file mode 100644 index 0000000..d64e1d4 --- /dev/null +++ b/lib/utils/hooks/FlowControlLogic/useFirstFCQuiz.ts @@ -0,0 +1,266 @@ +import { useCallback, useDebugValue, useEffect, useMemo, useState } from "react"; +import { enqueueSnackbar } from "notistack"; +import moment from "moment"; + +import { isResultQuestionEmpty } from "@/components/ViewPublicationPage/tools/checkEmptyData"; +import { useQuizStore } from "@/stores/useQuizStore"; + +import { useQuizViewStore } from "@stores/quizView"; + +import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals"; +import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals"; +import { useQuestionTimer } from "./useQuestionTimer"; + +export function useFirstFCQuiz() { + //Получаем инфо о квизе и список вопросов. + const { settings, questions, quizId, cnt, preview } = useQuizStore(); + + //Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их page. + //За корректность page отвечает конструктор квизов. Интересный факт, если в конструкторе удалить из середины вопрос, то случится куча запросов изменения вопросов с изменением этого page + const sortedQuestions = useMemo(() => { + return [...questions].sort((a, b) => a.page - b.page); + }, [questions]); + //React сам будет менять визуал - главное говорить из какого вопроса ему брать инфо. Изменение этой переменной меняет визуал. + const [currentQuestionId, setCurrentQuestionId] = useState(getFirstQuestionId); + //Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах + const answers = useQuizViewStore((state) => state.answers); + //Список засчитанных баллов для балловых квизов + const pointsSum = useQuizViewStore((state) => state.pointsSum); + //Текущий шаг "startpage" | "question" | "contactform" + const setCurrentQuizStep = useQuizViewStore((state) => state.setCurrentQuizStep); + //Получение возможности управлять состоянием метрик + const vkMetrics = useVkMetricsGoals(settings.cfg.vkMetricsNumber); + const yandexMetrics = useYandexMetricsGoals(settings.cfg.yandexMetricsNumber); + + useEffect(() => { + setCurrentQuizStep("contactform"); + }, []); + + //Изменение стейта (переменной currentQuestionId) ведёт к пересчёту что же за объект сейчас используется. Мы каждый раз просто ищем в списке + const currentQuestion = sortedQuestions.find((question) => question.id === currentQuestionId) ?? sortedQuestions[0]; + + //Индекс текущего вопроса только если квиз линейный + const linearQuestionIndex = //: number | null + currentQuestion && sortedQuestions.every(({ content }) => content.rule.parentId !== "root") // null when branching enabled + ? sortedQuestions.indexOf(currentQuestion) + : null; + + //Индекс первого вопроса + function getFirstQuestionId() { + //: string | null + if (sortedQuestions.length === 0) return null; //Если нету сортированного списка, то и не рыпаемся + + if (settings.cfg.haveRoot) { + // Если есть ветвление, то settings.cfg.haveRoot будет заполнен + //Если заполнен, то дерево растёт с root и это 1 вопрос :) + const nextQuestion = sortedQuestions.find( + //Функция ищет первое совпадение по массиву + (question) => question.id === settings.cfg.haveRoot || question.content.id === settings.cfg.haveRoot + ); + if (!nextQuestion) return null; + + return nextQuestion.id; + } + + //Если не возникло исключительных ситуаций - первый вопрос - нулевой элемент сортированного массива + return sortedQuestions[0].id; + } + + const nextQuestionIdPointsLogic = useCallback(() => { + return sortedQuestions.find((question) => question.type === "result" && question.content.rule.parentId === "line"); + }, [sortedQuestions]); + + //Анализируем какой вопрос должен быть следующим. Это главная логика + const nextQuestionIdMainLogic = useCallback(() => { + //Список ответов данных этому вопросу. Вернёт QuestionAnswer | undefined + const questionAnswer = answers.find(({ questionId }) => questionId === currentQuestion.id); + + //Если questionAnswer не undefined и ответ на вопрос не является временем: + if (questionAnswer && !moment.isMoment(questionAnswer.answer)) { + //Вопрос типизации. Получаем список строк ответов на этот вопрос + const userAnswers = Array.isArray(questionAnswer.answer) ? questionAnswer.answer : [questionAnswer.answer]; + + //цикл. Перебираем список условий .main и обзываем их переменной branchingRule + for (const branchingRule of currentQuestion.content.rule.main) { + // Перебираем список ответов. Если хоть один ответ из списка совпадает с прописанным правилом из условий - этот вопрос нужный нам. Его и дадимкак следующий + if (userAnswers.some((answer) => branchingRule.rules[0].answers.includes(answer))) { + return branchingRule.next; + } + } + } + + //Не помню что это, но чёт при первом взгляде оно true только у результатов + if (!currentQuestion.required) { + //Готовим себе дефолтный путь + const defaultNextQuestionId = currentQuestion.content.rule.default; + + //Если строка не пустая и не пробел. (Обычно при получении данных мы сразу чистим пустые строки только с пробелом на просто пустые строки. Это прост доп защита) + if (defaultNextQuestionId.length > 1 && defaultNextQuestionId !== " ") return defaultNextQuestionId; + //Вопросы типа страница, ползунок, своё поле для ввода и дата не могут иметь больше 1 ребёнка. Пользователь не может настроить там дефолт + //Кинуть на ребёнка надо даже если там нет дефолта + if ( + ["date", "page", "text", "number"].includes(currentQuestion.type) && + currentQuestion.content.rule.children.length === 1 + ) + return currentQuestion.content.rule.children[0]; + } + + //ничё не нашли, ищем резулт + return sortedQuestions.find((q) => { + return q.type === "result" && q.content.rule.parentId === currentQuestion.content.id; + })?.id; + }, [answers, currentQuestion, sortedQuestions]); + + //Анализ следующего вопроса. Это логика для вопроса с баллами + const nextQuestionId = useMemo(() => { + if (settings.cfg.score) { + return nextQuestionIdPointsLogic(); + } + return nextQuestionIdMainLogic(); + }, [nextQuestionIdMainLogic, nextQuestionIdPointsLogic, settings.cfg.score, questions]); + + //Поиск предыдущго вопроса либо по индексу либо по id родителя + const prevQuestion = + linearQuestionIndex !== null + ? sortedQuestions[linearQuestionIndex - 1] + : sortedQuestions.find( + (q) => + q.id === currentQuestion?.content.rule.parentId || q.content.id === currentQuestion?.content.rule.parentId + ); + + //Анализ результата по количеству баллов + const findResultPointsLogic = useCallback(() => { + //Отбираем из массива только тип резулт И результы с информацией о ожидаемых баллах И те результы, чьи суммы баллов меньше или равны насчитанным баллам юзера + + const results = sortedQuestions.filter( + (e) => e.type === "result" && e.content.rule.minScore !== undefined && e.content.rule.minScore <= pointsSum + ); + //Создаём массив строк из результатов. У кого есть инфо о баллах - дают свои, остальные 0 + const numbers = results.map((e) => + e.type === "result" && e.content.rule.minScore !== undefined ? e.content.rule.minScore : 0 + ); + //Извлекаем самое большое число + const indexOfNext = Math.max(...numbers); + //Отдаём индекс нужного нам результата + return results[numbers.indexOf(indexOfNext)]; + }, [pointsSum, sortedQuestions]); + + //Ищем следующий вопрос (не его индекс, или id). Сам вопрос + const nextQuestion = useMemo(() => { + let next; + + if (settings.cfg.score) { + //Ессли квиз балловый + if (linearQuestionIndex !== null) { + next = sortedQuestions[linearQuestionIndex + 1]; //ищем по индексу + if (next?.type === "result" || next == undefined) next = findResultPointsLogic(); //если в поисках пришли к результату - считаем нужный + } + } else { + //иначе + if (linearQuestionIndex !== null) { + //для линейных ищем по индексу + next = + sortedQuestions[linearQuestionIndex + 1] ?? + sortedQuestions.find((question) => question.type === "result" && question.content.rule.parentId === "line"); + } else { + // для нелинейных ищем по вычесленному id + next = sortedQuestions.find((q) => q.id === nextQuestionId || q.content.id === nextQuestionId); + } + } + + return next; + }, [nextQuestionId, findResultPointsLogic, linearQuestionIndex, sortedQuestions, settings.cfg.score]); + + // Таймер авто-перехода между вопросами + useQuestionTimer({ + enabled: Boolean(settings.questionTimerEnabled), + seconds: settings.cfg.time_of_passing ?? 0, + quizId, + preview, + currentQuestion, + onNext: () => { + // Программный переход к следующему вопросу + moveToNextQuestion(); + }, + }); + + //Показать визуалом юзеру результат + const showResult = useCallback(() => { + if (nextQuestion?.type !== "result") throw new Error("Current question is not result"); + + //Записать в переменную ид текущего вопроса + setCurrentQuestionId(nextQuestion.id); + }, [nextQuestion, setCurrentQuizStep, settings.cfg.resultInfo.showResultForm, settings.cfg.showfc]); + + //рычаг управления из визуала в этот контроллер + const showResultAfterContactForm = useCallback(() => { + setCurrentQuizStep("question"); + }, [currentQuestion, setCurrentQuizStep]); + + //рычаг управления из визуала в этот контроллер + const moveToPrevQuestion = useCallback(() => { + if (!prevQuestion) throw new Error("Previous question not found"); + + setCurrentQuestionId(prevQuestion.id); + }, [prevQuestion]); + + //рычаг управления из визуала в этот контроллер + const moveToNextQuestion = useCallback(async () => { + // Если есть следующий вопрос в уже загруженных - используем его + + if (nextQuestion) { + vkMetrics.questionPassed(currentQuestion.id); + yandexMetrics.questionPassed(currentQuestion.id); + + if (nextQuestion.type === "result") return showResult(); + setCurrentQuestionId(nextQuestion.id); + return; + } + }, [currentQuestion.id, nextQuestion, showResult, vkMetrics, yandexMetrics, linearQuestionIndex, questions]); + + //рычаг управления из визуала в этот контроллер + const setQuestion = useCallback( + (questionId: string) => { + const question = sortedQuestions.find((q) => q.id === questionId); + if (!question) return; + + setCurrentQuestionId(question.id); + }, + [sortedQuestions] + ); + + //Анализ дисаблить ли кнопки навигации + const isPreviousButtonEnabled = settings.cfg?.backBlocked ? false : Boolean(prevQuestion); + + //Анализ дисаблить ли кнопки навигации + const isNextButtonEnabled = useMemo(() => { + const hasAnswer = answers.some(({ questionId }) => questionId === currentQuestion.id); + + if ("required" in currentQuestion.content && currentQuestion.content.required) { + return hasAnswer; + } + + if (linearQuestionIndex !== null && questions.length < cnt) return true; + return Boolean(nextQuestion); + }, [answers, currentQuestion, nextQuestion]); + + useDebugValue({ + linearQuestionIndex, + currentQuestion: currentQuestion, + prevQuestion: prevQuestion, + nextQuestion: nextQuestion, + }); + + return { + currentQuestion, + currentQuestionStepNumber: + settings.status === "ai" ? null : linearQuestionIndex === null ? null : linearQuestionIndex + 1, + nextQuestion, + isNextButtonEnabled, + isPreviousButtonEnabled, + moveToPrevQuestion, + moveToNextQuestion, + showResultAfterContactForm, + setQuestion, + }; +} diff --git a/lib/utils/hooks/useQuestionFlowControl.ts b/lib/utils/hooks/useQuestionFlowControl.ts index 67986d1..976a7f8 100644 --- a/lib/utils/hooks/useQuestionFlowControl.ts +++ b/lib/utils/hooks/useQuestionFlowControl.ts @@ -1,6 +1,7 @@ import { useBranchingQuiz } from "./FlowControlLogic/useBranchingQuiz"; import { useLinearQuiz } from "./FlowControlLogic/useLinearQuiz"; import { useAIQuiz } from "./FlowControlLogic/useAIQuiz"; +import { useFirstFCQuiz } from "./FlowControlLogic/useFirstFCQuiz"; import { Status } from "@/model/settingsData"; import { useQuizStore } from "@/stores/useQuizStore"; @@ -11,10 +12,16 @@ interface StatusData { // выбор способа управления в зависимости от статуса let cachedManager: () => ReturnType; -export let statusOfQuiz: "line" | "branch" | "ai"; +export let statusOfQuiz: "line" | "branch" | "ai" | "FC"; let isInitialized = false; function analyicStatus({ status, haveRoot }: StatusData) { + let isCrutch13112025 = window.location.pathname === "/d557133f-26b6-4b0b-93da-c538758a65d4"; + + if (isCrutch13112025) { + statusOfQuiz = "FC"; + return; + } if (status === "ai") { statusOfQuiz = "ai"; return; @@ -40,6 +47,9 @@ export const initDataManager = (data: StatusData) => { case "ai": cachedManager = useAIQuiz; break; + case "FC": + cachedManager = useFirstFCQuiz; + break; } isInitialized = true; };