@ -11,29 +11,44 @@ import { useVkMetricsGoals } from "@/utils/hooks/metrics/useVkMetricsGoals";
import { useYandexMetricsGoals } from "@/utils/hooks/metrics/useYandexMetricsGoals" ;
export function useQuestionFlowControl() {
//Получаем инфо о квизе и список вопросов.
const { settings , questions } = useQuizSettings ( ) ;
//Когда квиз линейный, не ветвящийся, мы идём по вопросам по их порядковому номеру. Это их 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 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 =
//Индекс текущего вопроса только если квиз линейный
const linearQuestionIndex = //: number | null
currentQuestion && sortedQuestions . every ( ( { content } ) = > content . rule . parentId !== "root" ) // null when branching enabled
? sortedQuestions . indexOf ( currentQuestion )
: null ;
//Индекс первого вопроса
function getFirstQuestionId() {
if ( sortedQuestions . length === 0 ) return null ;
//: 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 ;
@ -41,6 +56,7 @@ export function useQuestionFlowControl() {
return nextQuestion . id ;
}
//Если не возникло исключительных ситуаций - первый вопрос - нулевой элемент сортированного массива
return sortedQuestions [ 0 ] . id ;
}
@ -48,21 +64,31 @@ export function useQuestionFlowControl() {
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 ребёнка. Пользователь не может настроить там дефолт
//Кинуть на ребёнка надо даже если там нет дефолта
@ -79,6 +105,7 @@ export function useQuestionFlowControl() {
} ) ? . id ;
} , [ answers , currentQuestion , sortedQuestions ] ) ;
//Анализ следующего вопроса. Это логика для вопроса с баллами
const nextQuestionId = useMemo ( ( ) = > {
if ( settings . cfg . score ) {
return nextQuestionIdPointsLogic ( ) ;
@ -86,6 +113,7 @@ export function useQuestionFlowControl() {
return nextQuestionIdMainLogic ( ) ;
} , [ nextQuestionIdMainLogic , nextQuestionIdPointsLogic , settings . cfg . score ] ) ;
//Поиск предыдущго вопроса либо по индексу либо по id родителя
const prevQuestion =
linearQuestionIndex !== null
? sortedQuestions [ linearQuestionIndex - 1 ]
@ -94,31 +122,42 @@ export function useQuestionFlowControl() {
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 ( ) ;
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 ) ;
}
}
@ -126,10 +165,13 @@ export function useQuestionFlowControl() {
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 ) )
@ -137,6 +179,7 @@ export function useQuestionFlowControl() {
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 ) ) {
@ -147,12 +190,14 @@ export function useQuestionFlowControl() {
setCurrentQuizStep ( "question" ) ;
} , [ currentQuestion , setCurrentQuizStep ] ) ;
//рычаг управления из визуала в эту функцию
const moveToPrevQuestion = useCallback ( ( ) = > {
if ( ! prevQuestion ) throw new Error ( "Previous question not found" ) ;
setCurrentQuestionId ( prevQuestion . id ) ;
} , [ prevQuestion ] ) ;
//рычаг управления из визуала в эту функцию
const moveToNextQuestion = useCallback ( ( ) = > {
if ( ! nextQuestion ) throw new Error ( "Next question not found" ) ;
@ -165,6 +210,7 @@ export function useQuestionFlowControl() {
setCurrentQuestionId ( nextQuestion . id ) ;
} , [ currentQuestion . id , nextQuestion , showResult , vkMetrics , yandexMetrics ] ) ;
//рычаг управления из визуала в эту функцию
const setQuestion = useCallback (
( questionId : string ) = > {
const question = sortedQuestions . find ( ( q ) = > q . id === questionId ) ;
@ -175,8 +221,10 @@ export function useQuestionFlowControl() {
[ sortedQuestions ]
) ;
//Анализ дисаблить ли кнопки навигации
const isPreviousButtonEnabled = Boolean ( prevQuestion ) ;
//Анализ дисаблить ли кнопки навигации
const isNextButtonEnabled = useMemo ( ( ) = > {
const hasAnswer = answers . some ( ( { questionId } ) = > questionId === currentQuestion . id ) ;