2025-06-15 09:58:15 +00:00
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" ;
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 ( ( ) = > {
return [ . . . questions ] . sort ( ( a , b ) = > a . page - b . page ) ;
} , [ questions ] ) ;
//React сам будет менять визуал - главное говорить из какого вопроса ему брать инфо. Изменение этой переменной меняет визуал.
const [ currentQuestionId , setCurrentQuestionId ] = useState < string | null > ( getFirstQuestionId ) ;
const [ headAI , setHeadAI ] = useState ( 0 ) ;
//Список ответов на вопрос. Мы записываем ответы локально, параллельно отправляя на бек информацию о ответах
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 ) ;
//Изменение стейта (переменной 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 ] ) ;
//Показать визуалом юзеру результат
const showResult = useCallback ( ( ) = > {
if ( nextQuestion ? . type !== "result" ) throw new Error ( "Current question is not result" ) ;
//Записать в переменную ид текущего вопроса
setCurrentQuestionId ( nextQuestion . id ) ;
//Смотрим по настройкам показывать ли вообще форму контактов. Показывать ли страницу результатов до или после формы контактов (ФК)
if (
settings . cfg . showfc !== false &&
( settings . cfg . resultInfo . showResultForm === "after" || isResultQuestionEmpty ( nextQuestion ) )
)
setCurrentQuizStep ( "contactform" ) ;
} , [ nextQuestion , setCurrentQuizStep , settings . cfg . resultInfo . showResultForm , settings . cfg . showfc ] ) ;
//рычаг управления из визуала в эту функцию
const showResultAfterContactForm = useCallback ( ( ) = > {
if ( currentQuestion ? . type !== "result" ) throw new Error ( "Current question is not result" ) ;
if ( isResultQuestionEmpty ( currentQuestion ) ) {
enqueueSnackbar ( "Данные отправлены" ) ;
return ;
}
setCurrentQuizStep ( "question" ) ;
} , [ currentQuestion , setCurrentQuizStep ] ) ;
//рычаг управления из визуала в эту функцию
const moveToPrevQuestion = useCallback ( ( ) = > {
if ( ! prevQuestion ) throw new Error ( "Previous question not found" ) ;
if ( settings . status === "ai" && headAI > 0 ) setHeadAI ( ( old ) = > old -- ) ;
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 ;
}
2025-06-18 13:05:14 +00:00
} , [ currentQuestion . id , nextQuestion , showResult , vkMetrics , yandexMetrics , linearQuestionIndex , questions ] ) ;
2025-06-15 09:58:15 +00:00
//рычаг управления из визуала в эту функцию
const setQuestion = useCallback (
( questionId : string ) = > {
const question = sortedQuestions . find ( ( q ) = > q . id === questionId ) ;
if ( ! question ) return ;
setCurrentQuestionId ( question . id ) ;
} ,
[ sortedQuestions ]
) ;
//Анализ дисаблить ли кнопки навигации
const isPreviousButtonEnabled = Boolean ( prevQuestion ) ;
//Анализ дисаблить ли кнопки навигации
const isNextButtonEnabled = useMemo ( ( ) = > {
const hasAnswer = answers . some ( ( { questionId } ) = > questionId === currentQuestion . id ) ;
if ( "required" in currentQuestion . content && currentQuestion . content . required ) {
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 ] ) ;
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 ,
} ;
}